// Copyright © 2016-present Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.

package gitmap

import (
	"bytes"
	"errors"
	"fmt"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

var (
	// will be modified during tests
	gitExec string

	GitNotFound = errors.New("Git executable not found in $PATH")
)

type GitRepo struct {
	// TopLevelAbsPath contains the absolute path of the top-level directory.
	// This is similar to the answer from "git rev-parse --show-toplevel"
	// except symbolic link is not followed on non-Windows platforms.
	// Note that this follows Git's way of handling paths, so expect to get forward slashes,
	// even on Windows.
	TopLevelAbsPath string

	// The files in this Git repository.
	Files GitMap
}

// GitMap maps filenames to Git revision information.
type GitMap map[string]*GitInfo

// GitInfo holds information about a Git commit.
type GitInfo struct {
	Hash            string    `json:"hash"`            // Commit hash
	AbbreviatedHash string    `json:"abbreviatedHash"` // Abbreviated commit hash
	Subject         string    `json:"subject"`         // The commit message's subject/title line
	AuthorName      string    `json:"authorName"`      // The author name, respecting .mailmap
	AuthorEmail     string    `json:"authorEmail"`     // The author email address, respecting .mailmap
	AuthorDate      time.Time `json:"authorDate"`      // The author date
	CommitDate      time.Time `json:"commitDate"`      // The commit date
}

// Map creates a GitRepo with a file map from the given repository path and revision.
// Use blank or HEAD as revision for the currently active revision.
func Map(repository, revision string) (*GitRepo, error) {
	m := make(GitMap)

	// First get the top level repo path
	absRepoPath, err := filepath.Abs(repository)

	if err != nil {
		return nil, err
	}

	out, err := git("-C", repository, "rev-parse", "--show-cdup")

	if err != nil {
		return nil, err
	}

	cdUp := strings.TrimSpace(string(out))
	topLevelPath := filepath.ToSlash(filepath.Join(absRepoPath, cdUp))

	gitLogArgs := strings.Fields(fmt.Sprintf(
		`--name-only --no-merges --format=format:%%x1e%%H%%x1f%%h%%x1f%%s%%x1f%%aN%%x1f%%aE%%x1f%%ai%%x1f%%ci %s`,
		revision,
	))

	gitLogArgs = append([]string{"-c", "diff.renames=0", "-c", "log.showSignature=0", "-C", repository, "log"}, gitLogArgs...)
	out, err = git(gitLogArgs...)

	if err != nil {
		return nil, err
	}

	entriesStr := string(out)
	entriesStr = strings.Trim(entriesStr, "\n\x1e'")
	entries := strings.Split(entriesStr, "\x1e")

	for _, e := range entries {
		lines := strings.Split(e, "\n")
		gitInfo, err := toGitInfo(lines[0])

		if err != nil {
			return nil, err
		}

		for _, filename := range lines[1:] {
			filename := strings.TrimSpace(filename)
			if filename == "" {
				continue
			}
			if _, ok := m[filename]; !ok {
				m[filename] = gitInfo
			}
		}
	}

	return &GitRepo{Files: m, TopLevelAbsPath: topLevelPath}, nil
}

func git(args ...string) ([]byte, error) {
	out, err := exec.Command(gitExec, args...).CombinedOutput()

	if err != nil {
		if ee, ok := err.(*exec.Error); ok {
			if ee.Err == exec.ErrNotFound {
				return nil, GitNotFound
			}
		}

		return nil, errors.New(string(bytes.TrimSpace(out)))
	}

	return out, nil
}

func toGitInfo(entry string) (*GitInfo, error) {
	items := strings.Split(entry, "\x1f")
	authorDate, err := time.Parse("2006-01-02 15:04:05 -0700", items[5])
	if err != nil {
		return nil, err
	}
	commitDate, err := time.Parse("2006-01-02 15:04:05 -0700", items[6])
	if err != nil {
		return nil, err
	}

	return &GitInfo{
		Hash:            items[0],
		AbbreviatedHash: items[1],
		Subject:         items[2],
		AuthorName:      items[3],
		AuthorEmail:     items[4],
		AuthorDate:      authorDate,
		CommitDate:      commitDate,
	}, nil
}

func init() {
	initDefaults()
}

func initDefaults() {
	gitExec = "git"
}
