// Copyright 2013 The LevelDB-Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package leveldb

import (
	"os"
	"sync"

	"github.com/golang/leveldb/db"
	"github.com/golang/leveldb/table"
)

type tableCache struct {
	dirname string
	fs      db.FileSystem
	opts    *db.Options
	size    int

	mu    sync.Mutex
	nodes map[uint64]*tableCacheNode
	dummy tableCacheNode
}

func (c *tableCache) init(dirname string, fs db.FileSystem, opts *db.Options, size int) {
	c.dirname = dirname
	c.fs = fs
	c.opts = opts
	c.size = size
	c.nodes = make(map[uint64]*tableCacheNode)
	c.dummy.next = &c.dummy
	c.dummy.prev = &c.dummy
}

func (c *tableCache) find(fileNum uint64, ikey internalKey) (db.Iterator, error) {
	// Calling findNode gives us the responsibility of decrementing n's
	// refCount. If opening the underlying table resulted in error, then we
	// decrement this straight away. Otherwise, we pass that responsibility
	// to the tableCacheIter, which decrements when it is closed.
	n := c.findNode(fileNum)
	x := <-n.result
	if x.err != nil {
		c.mu.Lock()
		n.refCount--
		if n.refCount == 0 {
			go n.release()
		}
		c.mu.Unlock()

		// Try loading the table again; the error may be transient.
		go n.load(c)
		return nil, x.err
	}
	n.result <- x
	return &tableCacheIter{
		Iterator: x.reader.Find(ikey, nil),
		cache:    c,
		node:     n,
	}, nil
}

// releaseNode releases a node from the tableCache.
//
// c.mu must be held when calling this.
func (c *tableCache) releaseNode(n *tableCacheNode) {
	delete(c.nodes, n.fileNum)
	n.next.prev = n.prev
	n.prev.next = n.next
	n.refCount--
	if n.refCount == 0 {
		go n.release()
	}
}

// findNode returns the node for the table with the given file number, creating
// that node if it didn't already exist. The caller is responsible for
// decrementing the returned node's refCount.
func (c *tableCache) findNode(fileNum uint64) *tableCacheNode {
	c.mu.Lock()
	defer c.mu.Unlock()

	n := c.nodes[fileNum]
	if n == nil {
		n = &tableCacheNode{
			fileNum:  fileNum,
			refCount: 1,
			result:   make(chan tableReaderOrError, 1),
		}
		c.nodes[fileNum] = n
		if len(c.nodes) > c.size {
			// Release the tail node.
			c.releaseNode(c.dummy.prev)
		}
		go n.load(c)
	} else {
		// Remove n from the doubly-linked list.
		n.next.prev = n.prev
		n.prev.next = n.next
	}
	// Insert n at the front of the doubly-linked list.
	n.next = c.dummy.next
	n.prev = &c.dummy
	n.next.prev = n
	n.prev.next = n
	// The caller is responsible for decrementing the refCount.
	n.refCount++
	return n
}

func (c *tableCache) evict(fileNum uint64) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if n := c.nodes[fileNum]; n != nil {
		c.releaseNode(n)
	}
}

func (c *tableCache) Close() error {
	c.mu.Lock()
	defer c.mu.Unlock()

	for n := c.dummy.next; n != &c.dummy; n = n.next {
		n.refCount--
		if n.refCount == 0 {
			go n.release()
		}
	}
	c.nodes = nil
	c.dummy.next = nil
	c.dummy.prev = nil
	return nil
}

type tableReaderOrError struct {
	reader *table.Reader
	err    error
}

type tableCacheNode struct {
	fileNum uint64
	result  chan tableReaderOrError

	// The remaining fields are protected by the tableCache mutex.

	next, prev *tableCacheNode
	refCount   int
}

func (n *tableCacheNode) load(c *tableCache) {
	// Try opening the fileTypeTable first. If that file doesn't exist,
	// fall back onto the fileTypeOldFashionedTable.
	f, err := c.fs.Open(dbFilename(c.dirname, fileTypeTable, n.fileNum))
	if os.IsNotExist(err) {
		f, err = c.fs.Open(dbFilename(c.dirname, fileTypeOldFashionedTable, n.fileNum))
	}
	if err != nil {
		n.result <- tableReaderOrError{err: err}
		return
	}
	n.result <- tableReaderOrError{reader: table.NewReader(f, c.opts)}
}

func (n *tableCacheNode) release() {
	x := <-n.result
	if x.err != nil {
		return
	}
	x.reader.Close()
}

type tableCacheIter struct {
	db.Iterator
	cache    *tableCache
	node     *tableCacheNode
	closeErr error
	closed   bool
}

func (i *tableCacheIter) Close() error {
	if i.closed {
		return i.closeErr
	}
	i.closed = true

	i.cache.mu.Lock()
	i.node.refCount--
	if i.node.refCount == 0 {
		go i.node.release()
	}
	i.cache.mu.Unlock()

	i.closeErr = i.Iterator.Close()
	return i.closeErr
}
