// Copyright 2015 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package git

import (
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
)

// EntryMode the type of the object in the git tree
type EntryMode int

// There are only a few file modes in Git. They look like unix file modes, but they can only be
// one of these.
const (
	// EntryModeBlob
	EntryModeBlob EntryMode = 0100644
	// EntryModeExec
	EntryModeExec EntryMode = 0100755
	// EntryModeSymlink
	EntryModeSymlink EntryMode = 0120000
	// EntryModeCommit
	EntryModeCommit EntryMode = 0160000
	// EntryModeTree
	EntryModeTree EntryMode = 0040000
)

// TreeEntry the leaf in the git tree
type TreeEntry struct {
	ID   SHA1
	Type ObjectType

	mode EntryMode
	name string

	ptree *Tree

	commited bool

	size  int64
	sized bool
}

// Name returns the name of the entry
func (te *TreeEntry) Name() string {
	return te.name
}

// Size returns the size of the entry
func (te *TreeEntry) Size() int64 {
	if te.IsDir() {
		return 0
	} else if te.sized {
		return te.size
	}

	stdout, err := NewCommand("cat-file", "-s", te.ID.String()).RunInDir(te.ptree.repo.Path)
	if err != nil {
		return 0
	}

	te.sized = true
	te.size, _ = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
	return te.size
}

// IsSubModule if the entry is a sub module
func (te *TreeEntry) IsSubModule() bool {
	return te.mode == EntryModeCommit
}

// IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool {
	return te.mode == EntryModeTree
}

// IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool {
	return te.mode == EntryModeSymlink
}

// Blob retrun the blob object the entry
func (te *TreeEntry) Blob() *Blob {
	return &Blob{
		repo:      te.ptree.repo,
		TreeEntry: te,
	}
}

// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
func (te *TreeEntry) GetSubJumpablePathName() string {
	if te.IsSubModule() || !te.IsDir() {
		return ""
	}
	tree, err := te.ptree.SubTree(te.name)
	if err != nil {
		return te.name
	}
	entries, _ := tree.ListEntries()
	if len(entries) == 1 && entries[0].IsDir() {
		name := entries[0].GetSubJumpablePathName()
		if name != "" {
			return te.name + "/" + name
		}
	}
	return te.name
}

// Entries a list of entry
type Entries []*TreeEntry

var sorter = []func(t1, t2 *TreeEntry) bool{
	func(t1, t2 *TreeEntry) bool {
		return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
	},
	func(t1, t2 *TreeEntry) bool {
		return t1.name < t2.name
	},
}

func (tes Entries) Len() int      { return len(tes) }
func (tes Entries) Swap(i, j int) { tes[i], tes[j] = tes[j], tes[i] }
func (tes Entries) Less(i, j int) bool {
	t1, t2 := tes[i], tes[j]
	var k int
	for k = 0; k < len(sorter)-1; k++ {
		s := sorter[k]
		switch {
		case s(t1, t2):
			return true
		case s(t2, t1):
			return false
		}
	}
	return sorter[k](t1, t2)
}

// Sort sort the list of entry
func (tes Entries) Sort() {
	sort.Sort(tes)
}

// getCommitInfoState transient state for getting commit info for entries
type getCommitInfoState struct {
	entries        map[string]*TreeEntry // map from filepath to entry
	commits        map[string]*Commit    // map from entry name to commit
	lastCommitHash string
	lastCommit     *Commit
	treePath       string
	headCommit     *Commit
	nextSearchSize int // next number of commits to search for
}

func initGetCommitInfoState(entries Entries, headCommit *Commit, treePath string) *getCommitInfoState {
	entriesByPath := make(map[string]*TreeEntry, len(entries))
	for _, entry := range entries {
		entriesByPath[filepath.Join(treePath, entry.Name())] = entry
	}
	return &getCommitInfoState{
		entries:        entriesByPath,
		commits:        make(map[string]*Commit, len(entriesByPath)),
		treePath:       treePath,
		headCommit:     headCommit,
		nextSearchSize: 16,
	}
}

// GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(commit *Commit, treePath string) ([][]interface{}, error) {
	state := initGetCommitInfoState(tes, commit, treePath)
	if err := getCommitsInfo(state); err != nil {
		return nil, err
	}

	commitsInfo := make([][]interface{}, len(tes))
	for i, entry := range tes {
		commit = state.commits[filepath.Join(treePath, entry.Name())]
		switch entry.Type {
		case ObjectCommit:
			subModuleURL := ""
			if subModule, err := state.headCommit.GetSubModule(entry.Name()); err != nil {
				return nil, err
			} else if subModule != nil {
				subModuleURL = subModule.URL
			}
			subModuleFile := NewSubModuleFile(commit, subModuleURL, entry.ID.String())
			commitsInfo[i] = []interface{}{entry, subModuleFile}
		default:
			commitsInfo[i] = []interface{}{entry, commit}
		}
	}
	return commitsInfo, nil
}

func (state *getCommitInfoState) nextCommit(hash string) {
	state.lastCommitHash = hash
	state.lastCommit = nil
}

func (state *getCommitInfoState) commit() (*Commit, error) {
	var err error
	if state.lastCommit == nil {
		state.lastCommit, err = state.headCommit.repo.GetCommit(state.lastCommitHash)
	}
	return state.lastCommit, err
}

func (state *getCommitInfoState) update(path string) error {
	relPath, err := filepath.Rel(state.treePath, path)
	if err != nil {
		return nil
	}
	var entryPath string
	if index := strings.IndexRune(relPath, os.PathSeparator); index >= 0 {
		entryPath = filepath.Join(state.treePath, relPath[:index])
	} else {
		entryPath = path
	}
	if _, ok := state.entries[entryPath]; !ok {
		return nil
	} else if _, ok := state.commits[entryPath]; ok {
		return nil
	}
	state.commits[entryPath], err = state.commit()
	return err
}

func getCommitsInfo(state *getCommitInfoState) error {
	for len(state.entries) > len(state.commits) {
		if err := getNextCommitInfos(state); err != nil {
			return err
		}
	}
	return nil
}

func getNextCommitInfos(state *getCommitInfoState) error {
	logOutput, err := logCommand(state.lastCommitHash, state).RunInDir(state.headCommit.repo.Path)
	if err != nil {
		return err
	}
	lines := strings.Split(logOutput, "\n")
	i := 0
	for i < len(lines) {
		state.nextCommit(lines[i])
		i++
		for ; i < len(lines); i++ {
			path := lines[i]
			if path == "" {
				break
			}
			state.update(path)
		}
		i++ // skip blank line
		if len(state.entries) == len(state.commits) {
			break
		}
	}
	return nil
}

func logCommand(exclusiveStartHash string, state *getCommitInfoState) *Command {
	var commitHash string
	if len(exclusiveStartHash) == 0 {
		commitHash = state.headCommit.ID.String()
	} else {
		commitHash = exclusiveStartHash + "^"
	}
	var command *Command
	numRemainingEntries := len(state.entries) - len(state.commits)
	if numRemainingEntries < 32 {
		searchSize := (numRemainingEntries + 1) / 2
		command = NewCommand("log", prettyLogFormat, "--name-only",
			"-"+strconv.Itoa(searchSize), commitHash, "--")
		for path, entry := range state.entries {
			if _, ok := state.commits[entry.Name()]; !ok {
				command.AddArguments(path)
			}
		}
	} else {
		command = NewCommand("log", prettyLogFormat, "--name-only",
			"-"+strconv.Itoa(state.nextSearchSize), commitHash, "--", state.treePath)
	}
	state.nextSearchSize += state.nextSearchSize
	return command
}
