package disk_bloom

import (
	"encoding/binary"
	"fmt"
	"os"
	"strings"
	"sync"
)

const metadataSize = 64

var (
	InvalidPatternErr = fmt.Errorf("invalid pattern")
)

type filterObj struct {
	filename string
	filter   *DiskFilter
	added    uint64
	expected uint64
}

// |added entries(8)|expected max entries(8)|slots(1)|bits(8)|reserved(39)|
type Metadata struct {
	Added    uint64
	Expected uint64
	Slots    uint8
	Bits     uint64
}

func (o *filterObj) control(f *os.File, modified bool) {
	if !modified {
		return
	}
	var b [8]byte
	binary.LittleEndian.PutUint64(b[:], o.added)
	// file: |len of metadata size(2)|added entries(8)|expected max entries(8)|slots(1)|bits(8)|bloom|
	f.WriteAt(b[:], LenOfMetadataSize)
}

func parseMetadata(bMetadata []byte) Metadata {
	return Metadata{
		Added:    binary.LittleEndian.Uint64(bMetadata[:8]),
		Expected: binary.LittleEndian.Uint64(bMetadata[8:16]),
		Slots:    bMetadata[16],
		Bits:     binary.LittleEndian.Uint64(bMetadata[17:]),
	}
}

func (m Metadata) Encode() []byte {
	//|added entries(8)|expected max entries(8)|slots(1)|bits(8)|
	var b [metadataSize]byte
	binary.LittleEndian.PutUint64(b[:], m.Added)
	binary.LittleEndian.PutUint64(b[8:], m.Expected)
	b[16] = m.Slots
	binary.LittleEndian.PutUint64(b[17:], m.Bits)
	return b[:]
}

// FilterGroup manages a group of DiskFilter.
// To make sure each filter meets the best performance, it will add new filters to the group if necessary.
type FilterGroup struct {
	filters      []*filterObj
	nextFilename func() string
	fsync        FsyncMode
	n            uint64
	param        FilterParam
	mu           sync.Mutex
}

// NewGroup returns a FilterGroup, each filter is a file.
// The filenames are generated by taking pattern and adding a index to the end.
// the Pattern should includes a "*", and the index replaces the last "*".
// n is the expected number of entries in single file.
// p is the expected false positive rate.
func NewGroup(pattern string, fsync FsyncMode, n uint64, p float64, hash func([]byte) (uint64, uint64)) (*FilterGroup, error) {
	slots, bits := OptimalParam(n, p)
	g := &FilterGroup{
		fsync: fsync,
		n:     n,
		param: FilterParam{
			Slots: slots,
			Bits:  bits,
			Hash:  hash,
		},
	}
	if err := g.resolvePatternAndSearch(pattern, fsync, hash); err != nil {
		return nil, err
	}

	if len(g.filters) == 0 || g.filters[len(g.filters)-1].added >= g.filters[len(g.filters)-1].expected {
		// last filter is full
		g.mu.Lock()
		defer g.mu.Unlock()
		if err := g.appendNewFilter(); err != nil {
			return nil, err
		}
	}
	return g, nil
}

func (g *FilterGroup) appendNewFilter() error {
	obj := new(filterObj)
	if filter, err := New(
		g.nextFilename(),
		Controller{
			Fsync:        g.fsync,
			MetadataSize: metadataSize,
			Control:      obj.control,
			GetParam: func(metadata []byte) (param FilterParam, updatedMetadata []byte) {
				obj.added = 0
				obj.expected = g.n
				return g.param, Metadata{
					Added:    0,
					Expected: g.n,
					Slots:    g.param.Slots,
					Bits:     g.param.Bits,
				}.Encode()
			},
		},
	); err != nil {
		return err
	} else {
		obj.filter = filter
	}
	g.filters = append(g.filters, obj)
	return nil
}

func (g *FilterGroup) resolvePatternAndSearch(pattern string, fsync FsyncMode, hash func([]byte) (uint64, uint64)) error {
	starIndex := strings.LastIndex(pattern, "*")
	if starIndex == -1 {
		return InvalidPatternErr
	}
	g.nextFilename = func() string {
		return fmt.Sprintf("%v%v%v", pattern[:starIndex], len(g.filters), pattern[starIndex+1:])
	}
	for {
		if _, err := os.Stat(g.nextFilename()); os.IsNotExist(err) {
			return nil
		}
		obj := new(filterObj)
		if filter, err := New(
			g.nextFilename(),
			Controller{
				Fsync:        fsync,
				MetadataSize: metadataSize,
				Control:      obj.control,
				GetParam: func(metadata []byte) (FilterParam, []byte) {
					m := parseMetadata(metadata)
					obj.added = m.Added
					obj.expected = m.Expected
					return FilterParam{
						Slots: m.Slots,
						Bits:  m.Bits,
						Hash:  hash,
					}, nil
				},
			},
		); err != nil {
			return err
		} else {
			obj.filter = filter
		}
		g.filters = append(g.filters, obj)
		// loop until the last filter is not full
		if obj.added < obj.expected {
			return nil
		}
	}
}

// ExistOrAdd returns whether the entry was in the filter, and adds an entry to the filterGroup if it was not in.
// TODO: batch?
func (g *FilterGroup) ExistOrAdd(b []byte) (exist bool) {
	defer func() {
		if exist == false {
			g.mu.Lock()
			defer g.mu.Unlock()
			g.filters[len(g.filters)-1].added++
			if g.filters[len(g.filters)-1].added < g.filters[len(g.filters)-1].expected {
				return
			}
			if err := g.appendNewFilter(); err != nil {
				// TODO:
				// log.Println("[error] ExistOrAdd: appendNewFilter:", err)
			}
		}
	}()
	// defers will be invoked in last-in-first-out order
	g.mu.Lock()
	defer g.mu.Unlock()
	for _, f := range g.filters[:len(g.filters)-1] {
		if f.filter.Exist(b) {
			return true
		}
	}
	return g.filters[len(g.filters)-1].filter.ExistOrAdd(b)
}

// Exist returns if an entry is in the filterGroup
func (g *FilterGroup) Exist(b []byte) (exist bool) {
	g.mu.Lock()
	defer g.mu.Unlock()
	for _, f := range g.filters {
		if f.filter.Exist(b) {
			return true
		}
	}
	return false
}
