package bolt

import (
	"encoding"
	"strings"
	"sync"

	"git.orcatech.org/sdks/golang/log/v2"
	bolt "github.com/coreos/bbolt"
)

// BinaryUnmarshalerFactory ...
type BinaryUnmarshalerFactory func() encoding.BinaryUnmarshaler

type DB struct {
	options Optioner
	db      *bolt.DB
	mutex   sync.Mutex
}

func Open(opts Optioner) (*DB, error) {
	db := &DB{options: opts}
	return db, db.open()
}

func (d *DB) open() error {
	log.Test("Opening bolt db: %s", d.options.Database())
	db, err := bolt.Open(d.options.Database(), 0600, &bolt.Options{Timeout: d.options.OpenTimeout()})
	log.Test("Opened bolt db")
	d.db = db
	return err
}

func (d *DB) Batch(fn func(*Tx) error) error {
	f := func(tx *bolt.Tx) error {
		return fn(&Tx{tx: tx})
	}
	return d.db.Batch(f)
}

func (d *DB) Update(fn func(*Tx) error) error {
	f := func(tx *bolt.Tx) error {
		return fn(&Tx{tx: tx})
	}
	return d.db.Update(f)
}

func (d *DB) View(fn func(*Tx) error) error {
	f := func(tx *bolt.Tx) error {
		return fn(&Tx{tx: tx})
	}
	return d.db.View(f)
}

func (d *DB) Begin(writeable bool) (*Tx, error) {
	tx, err := d.db.Begin(writeable)
	if err != nil {
		return nil, err
	}
	return &Tx{tx: tx}, nil
}

func (d *DB) Close() error {
	log.Test("Closing bolt db")
	defer log.Test("Closed bolt db")
	return d.compactAndClose()
}

func (d *DB) GoString() string {
	return d.db.GoString()
}

func (d *DB) Info() *bolt.Info {
	return d.db.Info()
}

func (d *DB) IsReadOnly() bool {
	return d.db.IsReadOnly()
}

func (d *DB) Path() string {
	return d.db.Path()
}

func (d *DB) Stats() bolt.Stats {
	return d.db.Stats()
}

func (d *DB) String() string {
	return d.db.String()
}

func (d *DB) Sync() error {
	return d.db.Sync()
}

// Put ...
func (d *DB) Put(bucketKey string, key string, value encoding.BinaryMarshaler) error {
	if d == nil {
		return ErrorUnableToAccessCache
	}

	tx, err := d.Begin(true)
	if err != nil {
		return err
	}

	bucket, err := tx.CreateBucketIfNotExists([]byte(bucketKey))
	if err != nil {
		tx.Commit()
		return err
	}

	b, err := value.MarshalBinary()
	if err != nil {
		tx.Commit()
		return err
	}

	if err := bucket.Put([]byte(strings.ToUpper(key)), b); err != nil {
		tx.Commit()
		return err
	}

	return tx.Commit()
}

// Get ...
func (d *DB) Get(bucketKey string, key string, value encoding.BinaryUnmarshaler) error {
	if d == nil {
		return ErrorUnableToAccessCache
	}

	tx, err := d.Begin(true)
	if err != nil {
		return err
	}
	defer tx.Commit()

	bucket, err := tx.CreateBucketIfNotExists([]byte(bucketKey))
	if err != nil {
		return err
	}

	v, err := bucket.Get([]byte(strings.ToUpper(key)))
	if err != nil {
		return err
	}

	if v == nil {
		return ErrorValueDoesNotExist
	}

	if err = value.UnmarshalBinary(v); err != nil {
		return err
	}

	return nil
}

// Remove ...
func (d *DB) Remove(bucketKey string, key string) error {
	if d == nil {
		return ErrorUnableToAccessCache
	}

	tx, err := d.Begin(true)
	if err != nil {
		return err
	}
	defer tx.Commit()

	bucket, err := tx.CreateBucketIfNotExists([]byte(bucketKey))
	if err != nil {
		return err
	}

	if err := bucket.Delete([]byte(strings.ToUpper(key))); err != nil {
		return err
	}

	return tx.Commit()
}

// GetAll ...
func (d *DB) GetAll(bucketKey string, factory BinaryUnmarshalerFactory) (map[string]encoding.BinaryUnmarshaler, error) {
	if d == nil {
		return nil, ErrorUnableToAccessCache
	}

	tx, err := d.Begin(true)
	if err != nil {
		return nil, err
	}
	defer tx.Commit()

	bucket, err := tx.CreateBucketIfNotExists([]byte(bucketKey))
	if err != nil {
		return nil, err
	}

	cursor := bucket.Cursor()
	m := make(map[string]encoding.BinaryUnmarshaler, 0)

	k, b, err := cursor.First()
	for {
		if err != nil {
			return m, err
		}
		if k == nil {
			return m, nil
		}

		// Unmarshal binary array
		value := factory()
		if err = value.UnmarshalBinary(b); err != nil {
			log.Error(err.Error())
			k, b, err = cursor.Next()
			continue
		}

		m[string(k)] = value

		k, b, err = cursor.Next()
	}
}

// BucketStats ...
func (d *DB) BucketStats(bucketKey string) (*BucketStats, error) {
	if d == nil {
		return nil, ErrorUnableToAccessCache
	}

	tx, err := d.Begin(true)
	if err != nil {
		return nil, err
	}
	defer tx.Commit()

	bucket, err := tx.CreateBucketIfNotExists([]byte(bucketKey))
	if err != nil {
		return nil, err
	}

	stats := bucket.Stats()
	return &stats, nil
}
