// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0

package main

import (
	"fmt"

	"github.com/attic-labs/noms/go/datas"
	"github.com/attic-labs/noms/go/types"
)

type CommitIterator struct {
	db       datas.Database
	branches branchList
}

// NewCommitIterator initializes a new CommitIterator with the first commit to be printed.
func NewCommitIterator(db datas.Database, commit types.Struct) *CommitIterator {
	cr := types.NewRef(commit)
	return &CommitIterator{db: db, branches: branchList{branch{cr: cr, commit: commit}}}
}

// Next returns information about the next commit to be printed. LogNode contains enough contextual
// info that the commit and associated graph can be correctly printed.
// This works by traversing the "commit" di-graph in a breadth-first manner. Each time it is called,
// the commit in the branchlist with the greatest height is returned. If that commit has multiple
// parents, new branches are added to the branchlist so that they can be traversed in order. When
// more than one branch contains the same node, that indicates that the branches are converging and so
// the branchlist will have branches removed to reflect that.
func (iter *CommitIterator) Next() (LogNode, bool) {
	if iter.branches.IsEmpty() {
		return LogNode{}, false
	}

	// Number of branches present when printing this commit
	startingColCount := len(iter.branches)

	branchIndexes := iter.branches.HighestBranchIndexes()
	col := branchIndexes[0]
	br := iter.branches[col]

	// Any additional indexes, represent other branches with the same ancestor. So they are merging
	// into a common ancestor and are no longer graphed.
	iter.branches = iter.branches.RemoveBranches(branchIndexes[1:])

	// If this commit has parents, then a branch is splitting. Create a branch for each of the parents
	// and splice that into the iterators list of branches.
	branches := branchList{}
	parents := commitRefsFromSet(br.commit.Get(datas.ParentsField).(types.Set))
	for _, p := range parents {
		b := branch{cr: p, commit: iter.db.ReadValue(p.TargetHash()).(types.Struct)}
		branches = append(branches, b)
	}
	iter.branches = iter.branches.Splice(col, 1, branches...)

	// Collect the indexes for any newly created branches.
	newCols := []int{}
	for cnt := 1; cnt < len(parents); cnt++ {
		newCols = append(newCols, col+cnt)
	}

	// Now that the branchlist has been adusted, check to see if there are branches with common
	// ancestors that will be folded together on this commit's graph.
	foldedCols := iter.branches.HighestBranchIndexes()
	node := LogNode{
		cr:               br.cr,
		commit:           br.commit,
		startingColCount: startingColCount,
		endingColCount:   len(iter.branches),
		col:              col,
		newCols:          newCols,
		foldedCols:       foldedCols,
		lastCommit:       iter.branches.IsEmpty(),
	}
	return node, true
}

type LogNode struct {
	cr               types.Ref    // typed ref of commit to be printed
	commit           types.Struct // commit that needs to be printed
	startingColCount int          // how many branches are being tracked when this commit is printed
	endingColCount   int          // home many branches will be tracked when next commit is printed
	col              int          // col to put the '*' character in graph
	newCols          []int        // col to start using '\' in graph
	foldedCols       []int        // cols with common ancestors, that will get folded together
	lastCommit       bool         // this is the last commit that will be returned by iterator
}

func (n LogNode) String() string {
	return fmt.Sprintf("cr: %s(%d), startingColCount: %d, endingColCount: %d, col: %d, newCols: %v, foldedCols: %v, expanding: %t, shrunk: %t, shrinking: %t", n.cr.TargetHash().String()[0:9], n.cr.Height(), n.startingColCount, n.endingColCount, n.col, n.newCols, n.foldedCols, n.Expanding(), n.Shrunk(), n.Shrinking())
}

// Expanding reports whether this commit's graph will expand to show an additional branch
func (n LogNode) Expanding() bool {
	return n.startingColCount < n.endingColCount
}

// Shrinking reports whether this commit's graph will show a branch being folded into another branch
func (n LogNode) Shrinking() bool {
	return len(n.foldedCols) > 1
}

// Shrunk reports whether the previous commit showed a branch being folded into another branch.
func (n LogNode) Shrunk() bool {
	return n.startingColCount > n.endingColCount
}

type branch struct {
	cr     types.Ref
	commit types.Struct
}

func (b branch) String() string {
	return fmt.Sprintf("%s(%d)", b.cr.TargetHash().String()[0:9], b.cr.Height())
}

type branchList []branch

func (bl branchList) IsEmpty() bool {
	return len(bl) == 0
}

// look through this list of branches and return the one(s) with the max height.
// If there are multiple nodes with max height, the result will contain a list of all nodes with
// maxHeight that are duplicates of the first one found.
// This indicates that two or more branches or converging.
func (bl branchList) HighestBranchIndexes() []int {
	maxHeight := uint64(0)
	var cr types.Ref
	cols := []int{}
	for i, b := range bl {
		if b.cr.Height() > maxHeight {
			maxHeight = b.cr.Height()
			cr = b.cr
			cols = []int{i}
		} else if b.cr.Height() == maxHeight && b.cr.Equals(cr) {
			cols = append(cols, i)
		}
	}
	return cols
}

func (bl branchList) Splice(start int, deleteCount int, branches ...branch) branchList {
	res := append(branchList{}, bl[:start]...)
	res = append(res, branches...)
	return append(res, bl[start+deleteCount:]...)
}

func (bl branchList) RemoveBranches(indexes []int) branchList {
	for i := len(indexes) - 1; i >= 0; i-- {
		bl = bl.Splice(indexes[i], 1)
	}
	return bl
}

func commitRefsFromSet(set types.Set) []types.Ref {
	res := []types.Ref{}
	set.IterAll(func(v types.Value) {
		res = append(res, v.(types.Ref))
	})
	return res
}
