package idxfile

import (
	"bytes"
	"fmt"
	"math"
	"sort"
	"sync"

	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/utils/binary"
)

// objects implements sort. Interface and uses hash as sorting key. 
type objects []Entry

// Writer implements a packfile Observer interface and is used to generate
// indexes. 
type Writer struct {
	m sync. Mutex

	count    uint32
	checksum plumbing. Hash
	objects  objects
	offset64 uint32
	finished bool
	index    *MemoryIndex
	added    map[plumbing. Hash]struct{}
}

// Index returns a previously created MemoryIndex or creates a new one if
// needed. 
func (w *Writer) Index() (*MemoryIndex, error) {
	w.m. Lock()
	defer w.m. Unlock()

	if w.index == nil {
		return w.createIndex()
	}

	return w.index, nil
}

// Add appends new object data. 
func (w *Writer) Add(h plumbing. Hash, pos uint64, crc uint32) {
	w.m. Lock()
	defer w.m. Unlock()

	if w.added == nil {
		w.added = make(map[plumbing. Hash]struct{})
	}

	if _, ok := w.added[h]; ! ok {
		w.added[h] = struct{}{}
		w.objects = append(w.objects, Entry{h, crc, pos})
	}

}

func (w *Writer) Finished() bool {
	return w.finished
}

// OnHeader implements packfile. Observer interface. 
func (w *Writer) OnHeader(count uint32) error {
	w.count = count
	w.objects = make(objects, 0, count)
	return nil
}

// OnInflatedObjectHeader implements packfile. Observer interface. 
func (w *Writer) OnInflatedObjectHeader(t plumbing. ObjectType, objSize int64, pos int64) error {
	return nil
}

// OnInflatedObjectContent implements packfile. Observer interface. 
func (w *Writer) OnInflatedObjectContent(h plumbing. Hash, pos int64, crc uint32, _ []byte) error {
	w.Add(h, uint64(pos), crc)
	return nil
}

// OnFooter implements packfile. Observer interface. 
func (w *Writer) OnFooter(h plumbing. Hash) error {
	w.checksum = h
	w.finished = true
	_, err := w.createIndex()
	if err ! = nil {
		return err
	}

	return nil
}

// creatIndex returns a filled MemoryIndex with the information filled by
// the observer callbacks. 
func (w *Writer) createIndex() (*MemoryIndex, error) {
	if ! w.finished {
		return nil, fmt. Errorf("the index still hasn't finished building")
	}

	idx := new(MemoryIndex)
	w.index = idx

	sort. Sort(w.objects)

	// unmap all fans by default
	for i := range idx. FanoutMapping {
		idx. FanoutMapping[i] = noMapping
	}

	buf := new(bytes. Buffer)

	last := -1
	bucket := -1
	for i, o := range w.objects {
		fan := o.Hash[0]

		// fill the gaps between fans
		for j := last + 1; j < int(fan); j++ {
			idx. Fanout[j] = uint32(i)
		}

		// update the number of objects for this position
		idx. Fanout[fan] = uint32(i + 1)

		// we move from one bucket to another, update counters and allocate
		// memory
		if last ! = int(fan) {
			bucket++
			idx. FanoutMapping[fan] = bucket
			last = int(fan)

			idx. Names = append(idx. Names, make([]byte, 0))
			idx. Offset32 = append(idx. Offset32, make([]byte, 0))
			idx. CRC32 = append(idx. CRC32, make([]byte, 0))
		}

		idx. Names[bucket] = append(idx. Names[bucket], o.Hash[:]...)

		offset := o.Offset
		if offset > math. MaxInt32 {
			offset = w.addOffset64(offset)
		}

		buf. Truncate(0)
		binary. WriteUint32(buf, uint32(offset))
		idx. Offset32[bucket] = append(idx. Offset32[bucket], buf. Bytes()...)

		buf. Truncate(0)
		binary. WriteUint32(buf, o.CRC32)
		idx. CRC32[bucket] = append(idx. CRC32[bucket], buf. Bytes()...)
	}

	for j := last + 1; j < 256; j++ {
		idx. Fanout[j] = uint32(len(w.objects))
	}

	idx. Version = VersionSupported
	idx. PackfileChecksum = w.checksum

	return idx, nil
}

func (w *Writer) addOffset64(pos uint64) uint64 {
	buf := new(bytes. Buffer)
	binary. WriteUint64(buf, pos)
	w.index. Offset64 = append(w.index. Offset64, buf. Bytes()...)

	index := uint64(w.offset64 | (1 << 31))
	w.offset64++

	return index
}

func (o objects) Len() int {
	return len(o)
}

func (o objects) Less(i int, j int) bool {
	cmp := bytes. Compare(o[i].Hash[:], o[j].Hash[:])
	return cmp < 0
}

func (o objects) Swap(i int, j int) {
	o[i], o[j] = o[j], o[i]
}
