package object

import (
	"container/list"
	"errors"
	"io"

	"github.com/go-git/go-git/v6/plumbing"
	"github.com/go-git/go-git/v6/plumbing/storer"
	"github.com/go-git/go-git/v6/storage"
)

type commitPreIterator struct {
	seenExternal map[plumbing.Hash]bool
	seen         map[plumbing.Hash]bool
	stack        []CommitIter
	start        *Commit
}

func forEachCommit(next func() (*Commit, error), cb func(*Commit) error) error {
	for {
		c, err := next()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return err
		}

		err = cb(c)
		if err == storer.ErrStop {
			break
		}
		if err != nil {
			return err
		}
	}
	return nil
}

// NewCommitPreorderIter returns a CommitIter that walks the commit history,
// starting at the given commit and visiting its parents in pre-order.
// The given callback will be called for each visited commit. Each commit will
// be visited only once. If the callback returns an error, walking will stop
// and will return the error. Other errors might be returned if the history
// cannot be traversed (e.g. missing objects). Ignore allows to skip some
// commits from being iterated.
func NewCommitPreorderIter(
	c *Commit,
	seenExternal map[plumbing.Hash]bool,
	ignore []plumbing.Hash,
) CommitIter {
	seen := make(map[plumbing.Hash]bool)
	for _, h := range ignore {
		seen[h] = true
	}

	return &commitPreIterator{
		seenExternal: seenExternal,
		seen:         seen,
		stack:        make([]CommitIter, 0),
		start:        c,
	}
}

func (w *commitPreIterator) Next() (*Commit, error) {
	var c *Commit
	for {
		if w.start != nil {
			c = w.start
			w.start = nil
		} else {
			current := len(w.stack) - 1
			if current < 0 {
				return nil, io.EOF
			}

			var err error
			c, err = w.stack[current].Next()
			if err == io.EOF {
				w.stack = w.stack[:current]
				continue
			}

			if err != nil {
				return nil, err
			}
		}

		if w.seen[c.Hash] || w.seenExternal[c.Hash] {
			continue
		}

		w.seen[c.Hash] = true

		if c.NumParents() > 0 {
			w.stack = append(w.stack, filteredParentIter(c, w.seen))
		}

		return c, nil
	}
}

func filteredParentIter(c *Commit, seen map[plumbing.Hash]bool) CommitIter {
	var hashes []plumbing.Hash
	for _, h := range c.ParentHashes {
		if !seen[h] {
			hashes = append(hashes, h)
		}
	}

	return NewCommitIter(c.s,
		storer.NewEncodedObjectLookupIter(c.s, plumbing.CommitObject, hashes),
	)
}

func (w *commitPreIterator) ForEach(cb func(*Commit) error) error {
	return forEachCommit(w.Next, cb)
}

func (w *commitPreIterator) Close() {}

type commitPostIterator struct {
	stack []*Commit
	seen  map[plumbing.Hash]bool
}

// NewCommitPostorderIter returns a CommitIter that walks the commit
// history like WalkCommitHistory but in post-order. This means that after
// walking a merge commit, the merged commit will be walked before the base
// it was merged on. This can be useful if you wish to see the history in
// chronological order. Ignore allows to skip some commits from being iterated.
func NewCommitPostorderIter(c *Commit, ignore []plumbing.Hash) CommitIter {
	seen := make(map[plumbing.Hash]bool)
	for _, h := range ignore {
		seen[h] = true
	}

	return &commitPostIterator{
		stack: []*Commit{c},
		seen:  seen,
	}
}

func (w *commitPostIterator) Next() (*Commit, error) {
	for {
		if len(w.stack) == 0 {
			return nil, io.EOF
		}

		c := w.stack[len(w.stack)-1]
		w.stack = w.stack[:len(w.stack)-1]

		if w.seen[c.Hash] {
			continue
		}

		w.seen[c.Hash] = true

		return c, c.Parents().ForEach(func(p *Commit) error {
			w.stack = append(w.stack, p)
			return nil
		})
	}
}

func (w *commitPostIterator) ForEach(cb func(*Commit) error) error {
	return forEachCommit(w.Next, cb)
}

func (w *commitPostIterator) Close() {}

type commitPostIteratorFirstParent struct {
	stack []*Commit
	seen  map[plumbing.Hash]bool
}

// NewCommitPostorderIterFirstParent returns a CommitIter that walks the commit
// history like WalkCommitHistory but in post-order.
//
// This option acts like the git log --first-parent flag, skipping intermediate
// commits that were brought in via a merge commit.
// Ignore allows to skip some commits from being iterated.
func NewCommitPostorderIterFirstParent(c *Commit, ignore []plumbing.Hash) CommitIter {
	seen := make(map[plumbing.Hash]bool)
	for _, h := range ignore {
		seen[h] = true
	}

	return &commitPostIteratorFirstParent{
		stack: []*Commit{c},
		seen:  seen,
	}
}

func (w *commitPostIteratorFirstParent) Next() (*Commit, error) {
	for {
		if len(w.stack) == 0 {
			return nil, io.EOF
		}

		c := w.stack[len(w.stack)-1]
		w.stack = w.stack[:len(w.stack)-1]

		if w.seen[c.Hash] {
			continue
		}

		w.seen[c.Hash] = true

		return c, c.Parents().ForEach(func(p *Commit) error {
			if len(c.ParentHashes) > 0 && p.Hash == c.ParentHashes[0] {
				w.stack = append(w.stack, p)
			}
			return nil
		})
	}
}

func (w *commitPostIteratorFirstParent) ForEach(cb func(*Commit) error) error {
	return forEachCommit(w.Next, cb)
}

func (w *commitPostIteratorFirstParent) Close() {}

// commitAllIterator stands for commit iterator for all refs.
type commitAllIterator struct {
	// currCommit points to the current commit.
	currCommit *list.Element
}

// NewCommitAllIter returns a new commit iterator for all refs.
// repoStorer is a repo Storer used to get commits and references.
// commitIterFunc is a commit iterator function, used to iterate through ref commits in chosen order
func NewCommitAllIter(repoStorer storage.Storer, commitIterFunc func(*Commit) CommitIter) (CommitIter, error) {
	commitsPath := list.New()
	commitsLookup := make(map[plumbing.Hash]*list.Element)
	head, err := storer.ResolveReference(repoStorer, plumbing.HEAD)
	if err == nil {
		err = addReference(repoStorer, commitIterFunc, head, commitsPath, commitsLookup)
	}

	if err != nil && err != plumbing.ErrReferenceNotFound {
		return nil, err
	}

	// add all references along with the HEAD
	refIter, err := repoStorer.IterReferences()
	if err != nil {
		return nil, err
	}
	defer refIter.Close()

	for {
		ref, err := refIter.Next()
		if err == io.EOF {
			break
		}

		if err == plumbing.ErrReferenceNotFound {
			continue
		}

		if err != nil {
			return nil, err
		}

		if err = addReference(repoStorer, commitIterFunc, ref, commitsPath, commitsLookup); err != nil {
			return nil, err
		}
	}

	return &commitAllIterator{commitsPath.Front()}, nil
}

func addReference(
	repoStorer storage.Storer,
	commitIterFunc func(*Commit) CommitIter,
	ref *plumbing.Reference,
	commitsPath *list.List,
	commitsLookup map[plumbing.Hash]*list.Element,
) error {
	_, exists := commitsLookup[ref.Hash()]
	if exists {
		// we already have it - skip the reference.
		return nil
	}

	refCommit, _ := GetCommit(repoStorer, ref.Hash())
	if refCommit == nil {
		// if it's not a commit - skip it.
		return nil
	}

	var (
		refCommits []*Commit
		parent     *list.Element
	)
	// collect all ref commits to add
	commitIter := commitIterFunc(refCommit)
	for c, e := commitIter.Next(); e == nil; {
		parent, exists = commitsLookup[c.Hash]
		if exists {
			break
		}
		refCommits = append(refCommits, c)
		c, e = commitIter.Next()
	}
	commitIter.Close()

	if parent == nil {
		// common parent - not found
		// add all commits to the path from this ref (maybe it's a HEAD and we don't have anything, yet)
		for _, c := range refCommits {
			parent = commitsPath.PushBack(c)
			commitsLookup[c.Hash] = parent
		}
	} else {
		// add ref's commits to the path in reverse order (from the latest)
		for i := len(refCommits) - 1; i >= 0; i-- {
			c := refCommits[i]
			// insert before found common parent
			parent = commitsPath.InsertBefore(c, parent)
			commitsLookup[c.Hash] = parent
		}
	}

	return nil
}

func (it *commitAllIterator) Next() (*Commit, error) {
	if it.currCommit == nil {
		return nil, io.EOF
	}

	c := it.currCommit.Value.(*Commit)
	it.currCommit = it.currCommit.Next()

	return c, nil
}

func (it *commitAllIterator) ForEach(cb func(*Commit) error) error {
	return forEachCommit(it.Next, cb)
}

func (it *commitAllIterator) Close() {
	it.currCommit = nil
}
