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

// Well known handles
// 1: root
// 2: id

package ql

import (
	"crypto/sha1"
	"fmt"
	"io"
	"io/ioutil"
	"math/big"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/cznic/lldb"
	"github.com/cznic/mathutil"
	"github.com/cznic/ql/vendored/github.com/camlistore/go4/lock"
)

const (
	magic = "\x60\xdbql"
)

var (
	_ btreeIndex    = (*fileIndex)(nil)
	_ btreeIterator = (*fileBTreeIterator)(nil)
	_ indexIterator = (*fileIndexIterator)(nil)
	_ storage       = (*file)(nil)
	_ temp          = (*fileTemp)(nil)
)

type chunk struct { // expanded to blob types lazily
	f *file
	b []byte
}

func (c chunk) expand() (v interface{}, err error) {
	return c.f.loadChunks(c.b)
}

func expand1(data interface{}, e error) (v interface{}, err error) {
	if e != nil {
		return nil, e
	}

	c, ok := data.(chunk)
	if !ok {
		return data, nil
	}

	return c.expand()
}

func expand(data []interface{}) (err error) {
	for i, v := range data {
		if data[i], err = expand1(v, nil); err != nil {
			return
		}
	}
	return
}

// OpenFile returns a DB backed by a named file. The back end limits the size
// of a record to about 64 kB.
func OpenFile(name string, opt *Options) (db *DB, err error) {
	var f lldb.OSFile
	if f = opt.OSFile; f == nil {
		f, err = os.OpenFile(name, os.O_RDWR, 0666)
		if err != nil {
			if !os.IsNotExist(err) {
				return nil, err
			}

			if !opt.CanCreate {
				return nil, err
			}

			f, err = os.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0666)
			if err != nil {
				return nil, err
			}
		}
	}

	fi, err := newFileFromOSFile(f, opt.Headroom) // always ACID
	if err != nil {
		return
	}

	if fi.tempFile = opt.TempFile; fi.tempFile == nil {
		fi.tempFile = func(dir, prefix string) (f lldb.OSFile, err error) {
			f0, err := ioutil.TempFile(dir, prefix)
			return f0, err
		}
	}

	fi.removeEmptyWAL = opt.RemoveEmptyWAL

	return newDB(fi)
}

// Options amend the behavior of OpenFile.
//
// CanCreate
//
// The CanCreate option enables OpenFile to create the DB file if it does not
// exists.
//
// OSFile
//
// OSFile allows to pass an os.File like back end providing, for example,
// encrypted storage. If this field is nil then OpenFile uses the file named by
// the 'name' parameter instead.
//
// TempFile
//
// TempFile provides a temporary file used for evaluating the GROUP BY, ORDER
// BY, ... clauses. The hook is intended to be used by encrypted DB back ends
// to avoid leaks of unecrypted data to such temp files by providing temp files
// which are encrypted as well. Note that *os.File satisfies the lldb.OSFile
// interface.
//
// If TempFile is nil it defaults to ioutil.TempFile.
//
// Headroom
//
// Headroom selects the minimum size a WAL file will have. The "extra"
// allocated file space serves as a headroom. Commits that fit into the
// headroom should not fail due to 'not enough space on the volume' errors. The
// headroom parameter is first rounded-up to a non negative multiple of the
// size of the lldb.Allocator atom.
//
// RemoveEmptyWAL
//
// RemoveEmptyWAL controls whether empty WAL files should be deleted on
// clean exit.
type Options struct {
	CanCreate      bool
	OSFile         lldb.OSFile
	TempFile       func(dir, prefix string) (f lldb.OSFile, err error)
	Headroom       int64
	RemoveEmptyWAL bool
}

type fileBTreeIterator struct {
	en *lldb.BTreeEnumerator
	t  *fileTemp
}

func (it *fileBTreeIterator) Next() (k, v []interface{}, err error) {
	bk, bv, err := it.en.Next()
	if err != nil {
		return
	}

	if k, err = lldb.DecodeScalars(bk); err != nil {
		return
	}

	for i, val := range k {
		b, ok := val.([]byte)
		if !ok {
			continue
		}

		c := chunk{it.t.file, b}
		if k[i], err = c.expand(); err != nil {
			return nil, nil, err
		}
	}

	if err = enforce(k, it.t.colsK); err != nil {
		return
	}

	if v, err = lldb.DecodeScalars(bv); err != nil {
		return
	}

	for i, val := range v {
		b, ok := val.([]byte)
		if !ok {
			continue
		}

		c := chunk{it.t.file, b}
		if v[i], err = c.expand(); err != nil {
			return nil, nil, err
		}
	}

	err = enforce(v, it.t.colsV)
	return
}

func enforce(val []interface{}, cols []*col) (err error) {
	for i, v := range val {
		if val[i], err = convert(v, cols[i].typ); err != nil {
			return
		}
	}
	return
}

//NTYPE
func infer(from []interface{}, to *[]*col) {
	if len(*to) == 0 {
		*to = make([]*col, len(from))
		for i := range *to {
			(*to)[i] = &col{}
		}
	}
	for i, c := range *to {
		if f := from[i]; f != nil {
			switch x := f.(type) {
			//case nil:
			case idealComplex:
				c.typ = qComplex128
				from[i] = complex128(x)
			case idealFloat:
				c.typ = qFloat64
				from[i] = float64(x)
			case idealInt:
				c.typ = qInt64
				from[i] = int64(x)
			case idealRune:
				c.typ = qInt32
				from[i] = int32(x)
			case idealUint:
				c.typ = qUint64
				from[i] = uint64(x)
			case bool:
				c.typ = qBool
			case complex128:
				c.typ = qComplex128
			case complex64:
				c.typ = qComplex64
			case float64:
				c.typ = qFloat64
			case float32:
				c.typ = qFloat32
			case int8:
				c.typ = qInt8
			case int16:
				c.typ = qInt16
			case int32:
				c.typ = qInt32
			case int64:
				c.typ = qInt64
			case string:
				c.typ = qString
			case uint8:
				c.typ = qUint8
			case uint16:
				c.typ = qUint16
			case uint32:
				c.typ = qUint32
			case uint64:
				c.typ = qUint64
			case []byte:
				c.typ = qBlob
			case *big.Int:
				c.typ = qBigInt
			case *big.Rat:
				c.typ = qBigRat
			case time.Time:
				c.typ = qTime
			case time.Duration:
				c.typ = qDuration
			case chunk:
				vals, err := lldb.DecodeScalars(x.b)
				if err != nil {
					panic(err)
				}

				if len(vals) == 0 {
					panic("internal error 040")
				}

				i, ok := vals[0].(int64)
				if !ok {
					panic("internal error 041")
				}

				c.typ = int(i)
			case map[string]interface{}: // map of ids of a cross join
			default:
				panic("internal error 042")
			}
		}
	}
}

type fileTemp struct {
	*file
	colsK []*col
	colsV []*col
	t     *lldb.BTree
}

func (t *fileTemp) BeginTransaction() error {
	return nil
}

func (t *fileTemp) Get(k []interface{}) (v []interface{}, err error) {
	if err = expand(k); err != nil {
		return
	}

	if err = t.flatten(k); err != nil {
		return nil, err
	}

	bk, err := lldb.EncodeScalars(k...)
	if err != nil {
		return
	}

	bv, err := t.t.Get(nil, bk)
	if err != nil {
		return
	}

	return lldb.DecodeScalars(bv)
}

func (t *fileTemp) Drop() (err error) {
	if t.f0 == nil {
		return
	}

	fn := t.f0.Name()
	if err = t.f0.Close(); err != nil {
		return
	}

	if fn == "" {
		return
	}

	return os.Remove(fn)
}

func (t *fileTemp) SeekFirst() (it btreeIterator, err error) {
	en, err := t.t.SeekFirst()
	if err != nil {
		return
	}

	return &fileBTreeIterator{t: t, en: en}, nil
}

func (t *fileTemp) Set(k, v []interface{}) (err error) {
	if err = expand(k); err != nil {
		return
	}

	if err = expand(v); err != nil {
		return
	}

	infer(k, &t.colsK)
	infer(v, &t.colsV)

	if err = t.flatten(k); err != nil {
		return
	}

	bk, err := lldb.EncodeScalars(k...)
	if err != nil {
		return
	}

	if err = t.flatten(v); err != nil {
		return
	}

	bv, err := lldb.EncodeScalars(v...)
	if err != nil {
		return
	}

	return t.t.Set(bk, bv)
}

type file struct {
	a              *lldb.Allocator
	codec          *gobCoder
	f              lldb.Filer
	f0             lldb.OSFile
	id             int64
	lck            io.Closer
	mu             sync.Mutex
	name           string
	tempFile       func(dir, prefix string) (f lldb.OSFile, err error)
	wal            *os.File
	removeEmptyWAL bool // Whether empty WAL files should be removed on close
}

func newFileFromOSFile(f lldb.OSFile, headroom int64) (fi *file, err error) {
	nm := lockName(f.Name())
	lck, err := lock.Lock(nm)
	if err != nil {
		if lck != nil {
			lck.Close()
		}
		return nil, err
	}

	close := true
	defer func() {
		if close && lck != nil {
			lck.Close()
		}
	}()

	var w *os.File
	closew := false
	wn := walName(f.Name())
	w, err = os.OpenFile(wn, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0666)
	closew = true
	defer func() {
		if w != nil && closew {
			nm := w.Name()
			w.Close()
			os.Remove(nm)
			w = nil
		}
	}()

	if err != nil {
		if !os.IsExist(err) {
			return nil, err
		}

		closew = false
		w, err = os.OpenFile(wn, os.O_RDWR, 0666)
		if err != nil {
			return nil, err
		}

		closew = true
		st, err := w.Stat()
		if err != nil {
			return nil, err
		}

		closew = st.Size() == 0
	}

	info, err := f.Stat()
	if err != nil {
		return nil, err
	}

	switch sz := info.Size(); {
	case sz == 0:
		b := make([]byte, 16)
		copy(b, []byte(magic))
		if _, err := f.Write(b); err != nil {
			return nil, err
		}

		filer := lldb.Filer(lldb.NewOSFiler(f))
		filer = lldb.NewInnerFiler(filer, 16)
		if filer, err = lldb.NewACIDFiler(filer, w, lldb.MinWAL(headroom)); err != nil {
			return nil, err
		}

		a, err := lldb.NewAllocator(filer, &lldb.Options{})
		if err != nil {
			return nil, err
		}

		a.Compress = true
		s := &file{
			a:     a,
			codec: newGobCoder(),
			f0:    f,
			f:     filer,
			lck:   lck,
			name:  f.Name(),
			wal:   w,
		}
		if err = s.BeginTransaction(); err != nil {
			return nil, err
		}

		h, err := s.Create()
		if err != nil {
			return nil, err
		}

		if h != 1 { // root
			panic("internal error 043")
		}

		if h, err = s.a.Alloc(make([]byte, 8)); err != nil {
			return nil, err
		}

		if h != 2 { // id
			panic("internal error 044")
		}

		close, closew = false, false
		return s, s.Commit()
	default:
		b := make([]byte, 16)
		if _, err := f.Read(b); err != nil {
			return nil, err
		}

		if string(b[:len(magic)]) != magic {
			return nil, fmt.Errorf("(file-002) unknown file format")
		}

		filer := lldb.Filer(lldb.NewOSFiler(f))
		filer = lldb.NewInnerFiler(filer, 16)
		if filer, err = lldb.NewACIDFiler(filer, w, lldb.MinWAL(headroom)); err != nil {
			return nil, err
		}

		a, err := lldb.NewAllocator(filer, &lldb.Options{})
		if err != nil {
			return nil, err
		}

		bid, err := a.Get(nil, 2) // id
		if err != nil {
			return nil, err
		}

		if len(bid) != 8 {
			return nil, fmt.Errorf("(file-003) corrupted DB: id |% x|", bid)
		}

		id := int64(0)
		for _, v := range bid {
			id = (id << 8) | int64(v)
		}

		a.Compress = true
		s := &file{
			a:     a,
			codec: newGobCoder(),
			f0:    f,
			f:     filer,
			id:    id,
			lck:   lck,
			name:  f.Name(),
			wal:   w,
		}

		close, closew = false, false
		return s, nil
	}
}

func (s *file) OpenIndex(unique bool, handle int64) (btreeIndex, error) {
	t, err := lldb.OpenBTree(s.a, s.collate, handle)
	if err != nil {
		return nil, err
	}

	return &fileIndex{s, handle, t, unique, newGobCoder()}, nil
}

func (s *file) CreateIndex(unique bool) ( /* handle */ int64, btreeIndex, error) {
	t, h, err := lldb.CreateBTree(s.a, s.collate)
	if err != nil {
		return -1, nil, err
	}

	return h, &fileIndex{s, h, t, unique, newGobCoder()}, nil
}

func (s *file) Acid() bool { return s.wal != nil }

func errSet(p *error, errs ...error) (err error) {
	err = *p
	for _, e := range errs {
		if err != nil {
			return
		}
		*p, err = e, e
	}
	return
}

func (s *file) lock() func() {
	s.mu.Lock()
	return s.mu.Unlock
}

func (s *file) Close() (err error) {
	defer s.lock()()

	es := s.f0.Sync()
	ef := s.f0.Close()
	var ew, estat, eremove error
	if s.wal != nil {
		remove := false
		wn := s.wal.Name()
		if s.removeEmptyWAL {
			var stat os.FileInfo
			stat, estat = s.wal.Stat()
			remove = stat.Size() == 0
		}
		ew = s.wal.Close()
		if remove {
			eremove = os.Remove(wn)
		}
	}
	el := s.lck.Close()
	return errSet(&err, es, ef, ew, el, estat, eremove)
}

func (s *file) Name() string { return s.name }

func (s *file) Verify() (allocs int64, err error) {
	defer s.lock()()
	var stat lldb.AllocStats
	if err = s.a.Verify(lldb.NewMemFiler(), nil, &stat); err != nil {
		return
	}

	allocs = stat.AllocAtoms
	return
}

func (s *file) expandBytes(d []interface{}) (err error) {
	for i, v := range d {
		b, ok := v.([]byte)
		if !ok {
			continue
		}

		d[i], err = s.loadChunks(b)
		if err != nil {
			return
		}
	}
	return
}

func (s *file) collate(a, b []byte) int { //TODO w/ error return
	da, err := lldb.DecodeScalars(a)
	if err != nil {
		panic(err)
	}

	if err = s.expandBytes(da); err != nil {
		panic(err)
	}

	db, err := lldb.DecodeScalars(b)
	if err != nil {
		panic(err)
	}

	if err = s.expandBytes(db); err != nil {
		panic(err)
	}

	//dbg("da: %v, db: %v", da, db)
	return collate(da, db)
}

func (s *file) CreateTemp(asc bool) (bt temp, err error) {
	f, err := s.tempFile("", "ql-tmp-")
	if err != nil {
		return nil, err
	}

	fn := f.Name()
	filer := lldb.NewOSFiler(f)
	a, err := lldb.NewAllocator(filer, &lldb.Options{})
	if err != nil {
		f.Close()
		os.Remove(fn)
		return nil, err
	}

	k := 1
	if !asc {
		k = -1
	}

	t, _, err := lldb.CreateBTree(a, func(a, b []byte) int { //TODO w/ error return
		return k * s.collate(a, b)
	})
	if err != nil {
		f.Close()
		if fn != "" {
			os.Remove(fn)
		}
		return nil, err
	}

	x := &fileTemp{file: &file{
		a:     a,
		codec: newGobCoder(),
		f0:    f,
	},
		t: t}
	return x, nil
}

func (s *file) BeginTransaction() (err error) {
	defer s.lock()()
	return s.f.BeginUpdate()
}

func (s *file) Rollback() (err error) {
	defer s.lock()()
	return s.f.Rollback()
}

func (s *file) Commit() (err error) {
	defer s.lock()()
	return s.f.EndUpdate()
}

func (s *file) Create(data ...interface{}) (h int64, err error) {
	if err = expand(data); err != nil {
		return
	}

	if err = s.flatten(data); err != nil {
		return
	}

	b, err := lldb.EncodeScalars(data...)
	if err != nil {
		return
	}

	defer s.lock()()
	return s.a.Alloc(b)
}

func (s *file) Delete(h int64, blobCols ...*col) (err error) {
	switch len(blobCols) {
	case 0:
		defer s.lock()()
		return s.a.Free(h)
	default:
		return s.free(h, blobCols)
	}
}

func (s *file) ResetID() (err error) {
	s.id = 0
	return
}

func (s *file) ID() (int64, error) {
	defer s.lock()()

	s.id++
	b := make([]byte, 8)
	id := s.id
	for i := 7; i >= 0; i-- {
		b[i] = byte(id)
		id >>= 8
	}

	return s.id, s.a.Realloc(2, b)
}

func (s *file) free(h int64, blobCols []*col) (err error) {
	b, err := s.a.Get(nil, h) //LATER +bufs
	if err != nil {
		return
	}

	rec, err := lldb.DecodeScalars(b)
	if err != nil {
		return
	}

	for _, col := range blobCols {
		if col.index >= len(rec) {
			return fmt.Errorf("(file-004) file.free: corrupted DB (record len)")
		}
		if col.index+2 >= len(rec) {
			continue
		}

		switch x := rec[col.index+2].(type) {
		case nil:
			// nop
		case []byte:
			if err = s.freeChunks(x); err != nil {
				return
			}
		}
	}
	defer s.lock()()
	return s.a.Free(h)
}

func (s *file) Read(dst []interface{}, h int64, cols ...*col) (data []interface{}, err error) { //NTYPE
	b, err := s.a.Get(nil, h) //LATER +bufs
	if err != nil {
		return
	}

	rec, err := lldb.DecodeScalars(b)
	if err != nil {
		return
	}

	for _, col := range cols {
		i := col.index + 2
		if i >= len(rec) || rec[i] == nil {
			continue
		}

		switch col.typ {
		case 0:
		case qBool:
		case qComplex64:
			rec[i] = complex64(rec[i].(complex128))
		case qComplex128:
		case qFloat32:
			rec[i] = float32(rec[i].(float64))
		case qFloat64:
		case qInt8:
			rec[i] = int8(rec[i].(int64))
		case qInt16:
			rec[i] = int16(rec[i].(int64))
		case qInt32:
			rec[i] = int32(rec[i].(int64))
		case qInt64:
		case qString:
		case qUint8:
			rec[i] = uint8(rec[i].(uint64))
		case qUint16:
			rec[i] = uint16(rec[i].(uint64))
		case qUint32:
			rec[i] = uint32(rec[i].(uint64))
		case qUint64:
		case qBlob, qBigInt, qBigRat, qTime, qDuration:
			switch x := rec[i].(type) {
			case []byte:
				rec[i] = chunk{f: s, b: x}
			default:
				return nil, fmt.Errorf("(file-006) corrupted DB: non nil chunk type is not []byte")
			}
		default:
			panic("internal error 045")
		}
	}

	if cols != nil {
		for n, dn := len(cols)+2, len(rec); dn < n; dn++ {
			rec = append(rec, nil)
		}
	}

	return rec, nil
}

func (s *file) freeChunks(enc []byte) (err error) {
	items, err := lldb.DecodeScalars(enc)
	if err != nil {
		return
	}

	var ok bool
	var next int64
	switch len(items) {
	case 2:
		return
	case 3:
		if next, ok = items[1].(int64); !ok || next == 0 {
			return fmt.Errorf("(file-007) corrupted DB: first chunk link")
		}
	default:
		return fmt.Errorf("(file-008) corrupted DB: first chunk")
	}

	for next != 0 {
		b, err := s.a.Get(nil, next)
		if err != nil {
			return err
		}

		if items, err = lldb.DecodeScalars(b); err != nil {
			return err
		}

		var h int64
		switch len(items) {
		case 1:
			// nop
		case 2:
			if h, ok = items[0].(int64); !ok {
				return fmt.Errorf("(file-009) corrupted DB: chunk link")
			}

		default:
			return fmt.Errorf("(file-010) corrupted DB: chunk items %d (%v)", len(items), items)
		}

		s.mu.Lock()
		if err = s.a.Free(next); err != nil {
			s.mu.Unlock()
			return err
		}

		s.mu.Unlock()
		next = h
	}
	return
}

func (s *file) loadChunks(enc []byte) (v interface{}, err error) {
	items, err := lldb.DecodeScalars(enc)
	if err != nil {
		return
	}

	var ok bool
	var next int64
	switch len(items) {
	case 2:
		// nop
	case 3:
		if next, ok = items[1].(int64); !ok || next == 0 {
			return nil, fmt.Errorf("(file-011) corrupted DB: first chunk link")
		}
	default:
		//fmt.Printf("%d: %#v\n", len(items), items)
		return nil, fmt.Errorf("(file-012) corrupted DB: first chunk")
	}

	typ, ok := items[0].(int64)
	if !ok {
		return nil, fmt.Errorf("(file-013) corrupted DB: first chunk tag")
	}

	buf, ok := items[len(items)-1].([]byte)
	if !ok {
		return nil, fmt.Errorf("(file-014) corrupted DB: first chunk data")
	}

	for next != 0 {
		b, err := s.a.Get(nil, next)
		if err != nil {
			return nil, err
		}

		if items, err = lldb.DecodeScalars(b); err != nil {
			return nil, err
		}

		switch len(items) {
		case 1:
			next = 0
		case 2:
			if next, ok = items[0].(int64); !ok {
				return nil, fmt.Errorf("(file-015) corrupted DB: chunk link")
			}

			items = items[1:]
		default:
			return nil, fmt.Errorf("(file-016) corrupted DB: chunk items %d (%v)", len(items), items)
		}

		if b, ok = items[0].([]byte); !ok {
			return nil, fmt.Errorf("(file-017) corrupted DB: chunk data")
		}

		buf = append(buf, b...)
	}
	return s.codec.decode(buf, int(typ))
}

func (s *file) Update(h int64, data ...interface{}) (err error) {
	b, err := lldb.EncodeScalars(data...)
	if err != nil {
		return
	}

	defer s.lock()()
	return s.a.Realloc(h, b)
}

func (s *file) UpdateRow(h int64, blobCols []*col, data ...interface{}) (err error) {
	if len(blobCols) == 0 {
		return s.Update(h, data...)
	}

	if err = expand(data); err != nil {
		return
	}

	data0, err := s.Read(nil, h, blobCols...)
	if err != nil {
		return
	}

	for _, c := range blobCols {
		if c.index+2 >= len(data0) {
			continue
		}

		if x := data0[c.index+2]; x != nil {
			if err = s.freeChunks(x.(chunk).b); err != nil {
				return
			}
		}
	}

	if err = s.flatten(data); err != nil {
		return
	}

	return s.Update(h, data...)
}

// []interface{}{qltype, ...}->[]interface{}{lldb scalar type, ...}
// + long blobs are (pre)written to a chain of chunks.
func (s *file) flatten(data []interface{}) (err error) {
	for i, v := range data {
		tag := 0
		var b []byte
		switch x := v.(type) {
		case []byte:
			tag = qBlob
			b = x
		case *big.Int:
			tag = qBigInt
			b, err = s.codec.encode(x)
		case *big.Rat:
			tag = qBigRat
			b, err = s.codec.encode(x)
		case time.Time:
			tag = qTime
			b, err = s.codec.encode(x)
		case time.Duration:
			tag = qDuration
			b, err = s.codec.encode(x)
		default:
			continue
		}
		if err != nil {
			return
		}

		const chunk = 1 << 16
		chunks := 0
		var next int64
		var buf []byte
		for rem := len(b); rem > shortBlob; {
			n := mathutil.Min(rem, chunk)
			part := b[rem-n:]
			b = b[:rem-n]
			rem -= n
			switch next {
			case 0: // last chunk
				buf, err = lldb.EncodeScalars([]interface{}{part}...)
			default: // middle chunk
				buf, err = lldb.EncodeScalars([]interface{}{next, part}...)
			}
			if err != nil {
				return
			}

			s.mu.Lock()
			h, err := s.a.Alloc(buf)
			s.mu.Unlock()
			if err != nil {
				return err
			}

			next = h
			chunks++
		}

		switch next {
		case 0: // single chunk
			buf, err = lldb.EncodeScalars([]interface{}{tag, b}...)
		default: // multi chunks
			buf, err = lldb.EncodeScalars([]interface{}{tag, next, b}...)
		}
		if err != nil {
			return
		}

		data[i] = buf
	}
	return
}

func lockName(dbname string) string {
	base := filepath.Base(filepath.Clean(dbname)) + "lockfile"
	h := sha1.New()
	io.WriteString(h, base)
	return filepath.Join(filepath.Dir(dbname), fmt.Sprintf(".%x", h.Sum(nil)))
}

func walName(dbname string) (r string) {
	base := filepath.Base(filepath.Clean(dbname))
	h := sha1.New()
	io.WriteString(h, base)
	return filepath.Join(filepath.Dir(dbname), fmt.Sprintf(".%x", h.Sum(nil)))
}

type fileIndex struct {
	f      *file
	h      int64
	t      *lldb.BTree
	unique bool
	codec  *gobCoder
}

func (x *fileIndex) Clear() error {
	return x.t.Clear()
}

var gbZeroInt64 []byte

func init() {
	var err error
	if gbZeroInt64, err = lldb.EncodeScalars(int64(0)); err != nil {
		panic(err)
	}
}

func isIndexNull(data []interface{}) bool {
	for _, v := range data {
		if v != nil {
			return false
		}
	}
	return true
}

// The []byte version of the key in the BTree shares chunks, if any, with
// the value stored in the record.
func (x *fileIndex) Create(indexedValues []interface{}, h int64) error {
	for i, indexedValue := range indexedValues {
		chunk, ok := indexedValue.(chunk)
		if ok {
			indexedValues[i] = chunk.b
		}
	}

	t := x.t
	switch {
	case !x.unique:
		k, err := lldb.EncodeScalars(append(indexedValues, h)...)
		if err != nil {
			return err
		}

		return t.Set(k, gbZeroInt64)
	case isIndexNull(indexedValues): // unique, NULL
		k, err := lldb.EncodeScalars(nil, h)
		if err != nil {
			return err
		}

		return t.Set(k, gbZeroInt64)
	default: // unique, non NULL
		k, err := lldb.EncodeScalars(append(indexedValues, int64(0))...)
		if err != nil {
			return err
		}

		v, err := lldb.EncodeScalars(h)
		if err != nil {
			return err
		}

		_, _, err = t.Put(nil, k, func(key, old []byte) (new []byte, write bool, err error) {
			if old == nil {
				return v, true, nil
			}

			return nil, false, fmt.Errorf("(file-018) cannot insert into unique index: duplicate value(s): %v", indexedValues)
		})
		return err
	}
}

func (x *fileIndex) Delete(indexedValues []interface{}, h int64) error {
	for i, indexedValue := range indexedValues {
		chunk, ok := indexedValue.(chunk)
		if ok {
			indexedValues[i] = chunk.b
		}
	}

	t := x.t
	var k []byte
	var err error
	switch {
	case !x.unique:
		k, err = lldb.EncodeScalars(append(indexedValues, h)...)
	case isIndexNull(indexedValues): // unique, NULL
		k, err = lldb.EncodeScalars(nil, h)
	default: // unique, non NULL
		k, err = lldb.EncodeScalars(append(indexedValues, int64(0))...)
	}
	if err != nil {
		return err
	}

	return t.Delete(k)
}

func (x *fileIndex) Drop() error {
	if err := x.Clear(); err != nil {
		return err
	}

	return x.f.a.Free(x.h)
}

// []interface{}{qltype, ...}->[]interface{}{lldb scalar type, ...}
func (x *fileIndex) flatten(data []interface{}) (err error) {
	for i, v := range data {
		tag := 0
		var b []byte
		switch xx := v.(type) {
		case []byte:
			tag = qBlob
			b = xx
		case *big.Int:
			tag = qBigInt
			b, err = x.codec.encode(xx)
		case *big.Rat:
			tag = qBigRat
			b, err = x.codec.encode(xx)
		case time.Time:
			tag = qTime
			b, err = x.codec.encode(xx)
		case time.Duration:
			tag = qDuration
			b, err = x.codec.encode(xx)
		default:
			continue
		}
		if err != nil {
			return
		}

		var buf []byte
		if buf, err = lldb.EncodeScalars([]interface{}{tag, b}...); err != nil {
			return
		}

		data[i] = buf
	}
	return
}

func (x *fileIndex) Seek(indexedValues []interface{}) (indexIterator, bool, error) {
	data := append(indexedValues, 0)
	if err := x.flatten(data); err != nil {
		return nil, false, err
	}

	k, err := lldb.EncodeScalars(data...)
	if err != nil {
		return nil, false, err
	}

	en, hit, err := x.t.Seek(k)
	if err != nil {
		return nil, false, err
	}

	return &fileIndexIterator{x.f, en, x.unique}, hit, nil
}

func (x *fileIndex) SeekFirst() (iter indexIterator, err error) {
	en, err := x.t.SeekFirst()
	return &fileIndexIterator{x.f, en, x.unique}, err
}

func (x *fileIndex) SeekLast() (iter indexIterator, err error) {
	en, err := x.t.SeekLast()
	return &fileIndexIterator{x.f, en, x.unique}, err
}

type fileIndexIterator struct {
	f      *file
	en     *lldb.BTreeEnumerator
	unique bool
}

func (i *fileIndexIterator) nextPrev(f func() ([]byte, []byte, error)) ([]interface{}, int64, error) { //TODO(indices) blobs: +test
	bk, bv, err := f()
	if err != nil {
		return nil, -1, err
	}

	dk, err := lldb.DecodeScalars(bk)
	if err != nil {
		return nil, -1, err
	}

	b, ok := dk[0].([]byte)
	if ok {
		dk[0] = chunk{i.f, b}
		if expand(dk[:1]); err != nil {
			return nil, -1, err
		}
	}

	var k indexKey
	k.value = dk[:len(dk)-1]
	switch i.unique {
	case true:
		if isIndexNull(k.value) {
			return nil, dk[len(dk)-1].(int64), nil
		}

		dv, err := lldb.DecodeScalars(bv)
		if err != nil {
			return nil, -1, err
		}

		return k.value, dv[0].(int64), nil
	default:
		return k.value, dk[len(dk)-1].(int64), nil
	}
}

func (i *fileIndexIterator) Next() ([]interface{}, int64, error) { //TODO(indices) blobs: +test
	return i.nextPrev(i.en.Next)
}

func (i *fileIndexIterator) Prev() ([]interface{}, int64, error) { //TODO(indices) blobs: +test
	return i.nextPrev(i.en.Prev)
}
