package storer

import (
	"errors"
	"io"
	"time"

	"github.com/go-git/go-git/v5/plumbing"
)

var (
	//ErrStop is used to stop a ForEach function in an Iter
	ErrStop = errors.New("stop iter")
)

// EncodedObjectStorer generic storage of objects
type EncodedObjectStorer interface {
	// NewEncodedObject returns a new plumbing.EncodedObject, the real type
	// of the object can be a custom implementation or the default one,
	// plumbing.MemoryObject.
	NewEncodedObject() plumbing.EncodedObject
	// SetEncodedObject saves an object into the storage, the object should
	// be create with the NewEncodedObject, method, and file if the type is
	// not supported.
	SetEncodedObject(plumbing.EncodedObject) (plumbing.Hash, error)
	// EncodedObject gets an object by hash with the given
	// plumbing.ObjectType. Implementors should return
	// (nil, plumbing.ErrObjectNotFound) if an object doesn't exist with
	// both the given hash and object type.
	//
	// Valid plumbing.ObjectType values are CommitObject, BlobObject, TagObject,
	// TreeObject and AnyObject. If plumbing.AnyObject is given, the object must
	// be looked up regardless of its type.
	EncodedObject(plumbing.ObjectType, plumbing.Hash) (plumbing.EncodedObject, error)
	// IterObjects returns a custom EncodedObjectStorer over all the object
	// on the storage.
	//
	// Valid plumbing.ObjectType values are CommitObject, BlobObject, TagObject,
	IterEncodedObjects(plumbing.ObjectType) (EncodedObjectIter, error)
	// HasEncodedObject returns ErrObjNotFound if the object doesn't
	// exist.  If the object does exist, it returns nil.
	HasEncodedObject(plumbing.Hash) error
	// EncodedObjectSize returns the plaintext size of the encoded object.
	EncodedObjectSize(plumbing.Hash) (int64, error)
	AddAlternate(remote string) error
}

// DeltaObjectStorer is an EncodedObjectStorer that can return delta
// objects.
type DeltaObjectStorer interface {
	// DeltaObject is the same as EncodedObject but without resolving deltas.
	// Deltas will be returned as plumbing.DeltaObject instances.
	DeltaObject(plumbing.ObjectType, plumbing.Hash) (plumbing.EncodedObject, error)
}

// Transactioner is a optional method for ObjectStorer, it enables transactional read and write
// operations.
type Transactioner interface {
	// Begin starts a transaction.
	Begin() Transaction
}

// LooseObjectStorer is an optional interface for managing "loose"
// objects, i.e. those not in packfiles.
type LooseObjectStorer interface {
	// ForEachObjectHash iterates over all the (loose) object hashes
	// in the repository without necessarily having to read those objects.
	// Objects only inside pack files may be omitted.
	// If ErrStop is sent the iteration is stop but no error is returned.
	ForEachObjectHash(func(plumbing.Hash) error) error
	// LooseObjectTime looks up the (m)time associated with the
	// loose object (that is not in a pack file). Some
	// implementations (e.g. without loose objects)
	// always return an error.
	LooseObjectTime(plumbing.Hash) (time.Time, error)
	// DeleteLooseObject deletes a loose object if it exists.
	DeleteLooseObject(plumbing.Hash) error
}

// PackedObjectStorer is an optional interface for managing objects in
// packfiles.
type PackedObjectStorer interface {
	// ObjectPacks returns hashes of object packs if the underlying
	// implementation has pack files.
	ObjectPacks() ([]plumbing.Hash, error)
	// DeleteOldObjectPackAndIndex deletes an object pack and the corresponding index file if they exist.
	// Deletion is only performed if the pack is older than the supplied time (or the time is zero).
	DeleteOldObjectPackAndIndex(plumbing.Hash, time.Time) error
}

// PackfileWriter is an optional method for ObjectStorer, it enables directly writing
// a packfile to storage.
type PackfileWriter interface {
	// PackfileWriter returns a writer for writing a packfile to the storage
	//
	// If the Storer not implements PackfileWriter the objects should be written
	// using the Set method.
	PackfileWriter() (io.WriteCloser, error)
}

// EncodedObjectIter is a generic closable interface for iterating over objects.
type EncodedObjectIter interface {
	Next() (plumbing.EncodedObject, error)
	ForEach(func(plumbing.EncodedObject) error) error
	Close()
}

// Transaction is an in-progress storage transaction. A transaction must end
// with a call to Commit or Rollback.
type Transaction interface {
	SetEncodedObject(plumbing.EncodedObject) (plumbing.Hash, error)
	EncodedObject(plumbing.ObjectType, plumbing.Hash) (plumbing.EncodedObject, error)
	Commit() error
	Rollback() error
}

// EncodedObjectLookupIter implements EncodedObjectIter. It iterates over a
// series of object hashes and yields their associated objects by retrieving
// each one from object storage. The retrievals are lazy and only occur when the
// iterator moves forward with a call to Next().
//
// The EncodedObjectLookupIter must be closed with a call to Close() when it is
// no longer needed.
type EncodedObjectLookupIter struct {
	storage EncodedObjectStorer
	series  []plumbing.Hash
	t       plumbing.ObjectType
	pos     int
}

// NewEncodedObjectLookupIter returns an object iterator given an object storage
// and a slice of object hashes.
func NewEncodedObjectLookupIter(
	storage EncodedObjectStorer, t plumbing.ObjectType, series []plumbing.Hash) *EncodedObjectLookupIter {
	return &EncodedObjectLookupIter{
		storage: storage,
		series:  series,
		t:       t,
	}
}

// Next returns the next object from the iterator. If the iterator has reached
// the end it will return io.EOF as an error. If the object can't be found in
// the object storage, it will return plumbing.ErrObjectNotFound as an error.
// If the object is retrieved successfully error will be nil.
func (iter *EncodedObjectLookupIter) Next() (plumbing.EncodedObject, error) {
	if iter.pos >= len(iter.series) {
		return nil, io.EOF
	}

	hash := iter.series[iter.pos]
	obj, err := iter.storage.EncodedObject(iter.t, hash)
	if err == nil {
		iter.pos++
	}

	return obj, err
}

// ForEach call the cb function for each object contained on this iter until
// an error happens or the end of the iter is reached. If ErrStop is sent
// the iteration is stop but no error is returned. The iterator is closed.
func (iter *EncodedObjectLookupIter) ForEach(cb func(plumbing.EncodedObject) error) error {
	return ForEachIterator(iter, cb)
}

// Close releases any resources used by the iterator.
func (iter *EncodedObjectLookupIter) Close() {
	iter.pos = len(iter.series)
}

// EncodedObjectSliceIter implements EncodedObjectIter. It iterates over a
// series of objects stored in a slice and yields each one in turn when Next()
// is called.
//
// The EncodedObjectSliceIter must be closed with a call to Close() when it is
// no longer needed.
type EncodedObjectSliceIter struct {
	series []plumbing.EncodedObject
}

// NewEncodedObjectSliceIter returns an object iterator for the given slice of
// objects.
func NewEncodedObjectSliceIter(series []plumbing.EncodedObject) *EncodedObjectSliceIter {
	return &EncodedObjectSliceIter{
		series: series,
	}
}

// Next returns the next object from the iterator. If the iterator has reached
// the end it will return io.EOF as an error. If the object is retrieved
// successfully error will be nil.
func (iter *EncodedObjectSliceIter) Next() (plumbing.EncodedObject, error) {
	if len(iter.series) == 0 {
		return nil, io.EOF
	}

	obj := iter.series[0]
	iter.series = iter.series[1:]

	return obj, nil
}

// ForEach call the cb function for each object contained on this iter until
// an error happens or the end of the iter is reached. If ErrStop is sent
// the iteration is stop but no error is returned. The iterator is closed.
func (iter *EncodedObjectSliceIter) ForEach(cb func(plumbing.EncodedObject) error) error {
	return ForEachIterator(iter, cb)
}

// Close releases any resources used by the iterator.
func (iter *EncodedObjectSliceIter) Close() {
	iter.series = []plumbing.EncodedObject{}
}

// MultiEncodedObjectIter implements EncodedObjectIter. It iterates over several
// EncodedObjectIter,
//
// The MultiObjectIter must be closed with a call to Close() when it is no
// longer needed.
type MultiEncodedObjectIter struct {
	iters []EncodedObjectIter
}

// NewMultiEncodedObjectIter returns an object iterator for the given slice of
// EncodedObjectIters.
func NewMultiEncodedObjectIter(iters []EncodedObjectIter) EncodedObjectIter {
	return &MultiEncodedObjectIter{iters: iters}
}

// Next returns the next object from the iterator, if one iterator reach io.EOF
// is removed and the next one is used.
func (iter *MultiEncodedObjectIter) Next() (plumbing.EncodedObject, error) {
	if len(iter.iters) == 0 {
		return nil, io.EOF
	}

	obj, err := iter.iters[0].Next()
	if err == io.EOF {
		iter.iters[0].Close()
		iter.iters = iter.iters[1:]
		return iter.Next()
	}

	return obj, err
}

// ForEach call the cb function for each object contained on this iter until
// an error happens or the end of the iter is reached. If ErrStop is sent
// the iteration is stop but no error is returned. The iterator is closed.
func (iter *MultiEncodedObjectIter) ForEach(cb func(plumbing.EncodedObject) error) error {
	return ForEachIterator(iter, cb)
}

// Close releases any resources used by the iterator.
func (iter *MultiEncodedObjectIter) Close() {
	for _, i := range iter.iters {
		i.Close()
	}
}

type bareIterator interface {
	Next() (plumbing.EncodedObject, error)
	Close()
}

// ForEachIterator is a helper function to build iterators without need to
// rewrite the same ForEach function each time.
func ForEachIterator(iter bareIterator, cb func(plumbing.EncodedObject) error) error {
	defer iter.Close()
	for {
		obj, err := iter.Next()
		if err != nil {
			if err == io.EOF {
				return nil
			}

			return err
		}

		if err := cb(obj); err != nil {
			if err == ErrStop {
				return nil
			}

			return err
		}
	}
}
