package index

import (
	"bytes"

	"github.com/asdine/storm/internal"
	"github.com/boltdb/bolt"
)

// NewListIndex loads a ListIndex
func NewListIndex(parent *bolt.Bucket, indexName []byte) (*ListIndex, error) {
	var err error
	b := parent.Bucket(indexName)
	if b == nil {
		if !parent.Writable() {
			return nil, ErrNotFound
		}
		b, err = parent.CreateBucket(indexName)
		if err != nil {
			return nil, err
		}
	}

	ids, err := NewUniqueIndex(b, []byte("storm__ids"))
	if err != nil {
		return nil, err
	}

	return &ListIndex{
		IndexBucket: b,
		Parent:      parent,
		IDs:         ids,
	}, nil
}

// ListIndex is an index that references values and the corresponding IDs.
type ListIndex struct {
	Parent      *bolt.Bucket
	IndexBucket *bolt.Bucket
	IDs         *UniqueIndex
}

// Add a value to the list index
func (idx *ListIndex) Add(newValue []byte, targetID []byte) error {
	if newValue == nil || len(newValue) == 0 {
		return ErrNilParam
	}
	if targetID == nil || len(targetID) == 0 {
		return ErrNilParam
	}

	key := idx.IDs.Get(targetID)
	if key != nil {
		err := idx.IndexBucket.Delete(key)
		if err != nil {
			return err
		}

		err = idx.IDs.Remove(targetID)
		if err != nil {
			return err
		}

		key = key[:0]
	}

	key = append(key, newValue...)
	key = append(key, '_')
	key = append(key, '_')
	key = append(key, targetID...)

	err := idx.IDs.Add(targetID, key)
	if err != nil {
		return err
	}

	return idx.IndexBucket.Put(key, targetID)
}

// Remove a value from the unique index
func (idx *ListIndex) Remove(value []byte) error {
	var err error
	var keys [][]byte

	c := idx.IndexBucket.Cursor()
	prefix := generatePrefix(value)

	for k, _ := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, _ = c.Next() {
		keys = append(keys, k)
	}

	for _, k := range keys {
		err = idx.IndexBucket.Delete(k)
		if err != nil {
			return err
		}
	}

	return idx.IDs.RemoveID(value)
}

// RemoveID removes an ID from the list index
func (idx *ListIndex) RemoveID(targetID []byte) error {
	value := idx.IDs.Get(targetID)
	if value == nil {
		return nil
	}

	err := idx.IndexBucket.Delete(value)
	if err != nil {
		return err
	}

	return idx.IDs.Remove(targetID)
}

// Get the first ID corresponding to the given value
func (idx *ListIndex) Get(value []byte) []byte {
	c := idx.IndexBucket.Cursor()
	prefix := generatePrefix(value)

	for k, id := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, id = c.Next() {
		return id
	}

	return nil
}

// All the IDs corresponding to the given value
func (idx *ListIndex) All(value []byte, opts *Options) ([][]byte, error) {
	var list [][]byte
	c := idx.IndexBucket.Cursor()
	cur := internal.Cursor{C: c, Reverse: opts != nil && opts.Reverse}

	prefix := generatePrefix(value)

	k, id := c.Seek(prefix)
	if cur.Reverse {
		var count int
		for ; bytes.HasPrefix(k, prefix) && k != nil; k, _ = c.Next() {
			count++
		}
		k, id = c.Prev()
		list = make([][]byte, 0, count)
	}

	for ; bytes.HasPrefix(k, prefix); k, id = cur.Next() {
		if opts != nil && opts.Skip > 0 {
			opts.Skip--
			continue
		}

		if opts != nil && opts.Limit == 0 {
			break
		}

		if opts != nil && opts.Limit > 0 {
			opts.Limit--
		}

		list = append(list, id)
	}

	return list, nil
}

// AllRecords returns all the IDs of this index
func (idx *ListIndex) AllRecords(opts *Options) ([][]byte, error) {
	var list [][]byte

	c := internal.Cursor{C: idx.IndexBucket.Cursor(), Reverse: opts != nil && opts.Reverse}

	for k, id := c.First(); k != nil; k, id = c.Next() {
		if id == nil || bytes.Equal(k, []byte("storm__ids")) {
			continue
		}

		if opts != nil && opts.Skip > 0 {
			opts.Skip--
			continue
		}

		if opts != nil && opts.Limit == 0 {
			break
		}

		if opts != nil && opts.Limit > 0 {
			opts.Limit--
		}

		list = append(list, id)
	}

	return list, nil
}

// Range returns the ids corresponding to the given range of values
func (idx *ListIndex) Range(min []byte, max []byte, opts *Options) ([][]byte, error) {
	var list [][]byte

	c := internal.RangeCursor{
		C:       idx.IndexBucket.Cursor(),
		Reverse: opts != nil && opts.Reverse,
		Min:     min,
		Max:     max,
		CompareFn: func(val, limit []byte) int {
			pos := bytes.LastIndex(val, []byte("__"))
			return bytes.Compare(val[:pos], limit)
		},
	}

	for k, id := c.First(); c.Continue(k); k, id = c.Next() {
		if id == nil || bytes.Equal(k, []byte("storm__ids")) {
			continue
		}

		if opts != nil && opts.Skip > 0 {
			opts.Skip--
			continue
		}

		if opts != nil && opts.Limit == 0 {
			break
		}

		if opts != nil && opts.Limit > 0 {
			opts.Limit--
		}

		list = append(list, id)
	}

	return list, nil
}

func generatePrefix(value []byte) []byte {
	prefix := make([]byte, len(value)+2)
	var i int
	for i = range value {
		prefix[i] = value[i]
	}
	prefix[i+1] = '_'
	prefix[i+2] = '_'
	return prefix
}
