// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gitgraph

import (
	"bytes"
	"context"
	"fmt"
	"strings"
	"time"

	asymkey_model "forgejo.org/models/asymkey"
	"forgejo.org/models/db"
	git_model "forgejo.org/models/git"
	repo_model "forgejo.org/models/repo"
	user_model "forgejo.org/models/user"
	"forgejo.org/modules/container"
	"forgejo.org/modules/git"
	"forgejo.org/modules/log"
)

// NewGraph creates a basic graph
func NewGraph() *Graph {
	graph := &Graph{}
	graph.relationCommit = &Commit{
		Row:    -1,
		Column: -1,
	}
	graph.Flows = map[int64]*Flow{}
	graph.continuationAbove = map[[2]int]bool{}
	return graph
}

// Graph represents a collection of flows
type Graph struct {
	Flows             map[int64]*Flow
	Commits           []*Commit
	MinRow            int
	MinColumn         int
	MaxRow            int
	MaxColumn         int
	relationCommit    *Commit
	continuationAbove map[[2]int]bool
}

// Width returns the width of the graph
func (graph *Graph) Width() int {
	return graph.MaxColumn - graph.MinColumn + 1
}

// Height returns the height of the graph
func (graph *Graph) Height() int {
	return graph.MaxRow - graph.MinRow + 1
}

// ComputeGlyphConnectivity sets ConnectsUp/ConnectsDown for commit glyphs based on parent/child relationships
func (graph *Graph) ComputeGlyphConnectivity() {
	revs := make(container.Set[string])
	revByPos := make(map[[2]int]string)
	for _, c := range graph.Commits {
		if c.Rev != "" {
			revs.Add(c.Rev)
			revByPos[[2]int{c.Row, c.Column}] = c.Rev
		}
	}

	connectsDown := make(container.Set[string])
	connectsUp := make(container.Set[string])

	// Commits with parents connect down (even if parent is on another page)
	// Commits with visible children connect up
	for _, c := range graph.Commits {
		if len(c.ParentHashes) > 0 {
			connectsDown.Add(c.Rev)
		}
		for _, parentHash := range c.ParentHashes {
			if revs.Contains(parentHash) {
				connectsUp.Add(parentHash)
			}
		}
	}

	// Commits with a non-commit glyph above also connect up
	for _, flow := range graph.Flows {
		for _, g := range flow.Glyphs {
			if g.Glyph != '*' {
				pos := [2]int{g.Row + 1, g.Column}
				if rev, exists := revByPos[pos]; exists {
					connectsUp.Add(rev)
				}
			}
		}
	}

	// Commits with continuation from previous page connect up
	for pos := range graph.continuationAbove {
		if rev, exists := revByPos[pos]; exists {
			connectsUp.Add(rev)
		}
	}

	for _, flow := range graph.Flows {
		for i := range flow.Glyphs {
			glyph := &flow.Glyphs[i]
			if glyph.Glyph == '*' {
				pos := [2]int{glyph.Row, glyph.Column}
				if rev, exists := revByPos[pos]; exists {
					glyph.ConnectsUp = connectsUp.Contains(rev)
					glyph.ConnectsDown = connectsDown.Contains(rev)
				}
			} else {
				glyph.ConnectsUp = true
				glyph.ConnectsDown = true
			}
		}
	}
}

// AddGlyph adds glyph to flows
func (graph *Graph) AddGlyph(row, column int, flowID int64, color int, glyph byte) {
	flow, ok := graph.Flows[flowID]
	if !ok {
		flow = NewFlow(flowID, color, row, column)
		graph.Flows[flowID] = flow
	}
	flow.AddGlyph(row, column, glyph)

	if row < graph.MinRow {
		graph.MinRow = row
	}
	if row > graph.MaxRow {
		graph.MaxRow = row
	}
	if column < graph.MinColumn {
		graph.MinColumn = column
	}
	if column > graph.MaxColumn {
		graph.MaxColumn = column
	}
}

// AddCommit adds a commit at row, column on flowID with the provided data
func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error {
	commit, err := NewCommit(row, column, data)
	if err != nil {
		return err
	}
	commit.Flow = flowID
	graph.Commits = append(graph.Commits, commit)

	graph.Flows[flowID].Commits = append(graph.Flows[flowID].Commits, commit)
	return nil
}

// LoadAndProcessCommits will load the git.Commits for each commit in the graph,
// the associate the commit with the user author, and check the commit verification
// before finally retrieving the latest status
func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_model.Repository, gitRepo *git.Repository) error {
	var err error
	var ok bool

	emails := map[string]*user_model.User{}
	keyMap := map[string]bool{}

	for _, c := range graph.Commits {
		if len(c.Rev) == 0 {
			continue
		}
		c.Commit, err = gitRepo.GetCommit(c.Rev)
		if err != nil {
			return fmt.Errorf("GetCommit: %s Error: %w", c.Rev, err)
		}

		if c.Commit.Author != nil {
			email := c.Commit.Author.Email
			if c.User, ok = emails[email]; !ok {
				c.User, _ = user_model.GetUserByEmail(ctx, email)
				emails[email] = c.User
			}
		}

		c.Verification = asymkey_model.ParseCommitWithSignature(ctx, c.Commit)

		_ = asymkey_model.CalculateTrustStatus(c.Verification, repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
			return repo_model.IsOwnerMemberCollaborator(ctx, repository, user.ID)
		}, &keyMap)

		statuses, _, err := git_model.GetLatestCommitStatus(ctx, repository.ID, c.Commit.ID.String(), db.ListOptions{})
		if err != nil {
			log.Error("GetLatestCommitStatus: %v", err)
		} else {
			c.Status = git_model.CalcCommitStatus(statuses)
		}
	}
	return nil
}

// NewFlow creates a new flow
func NewFlow(flowID int64, color, row, column int) *Flow {
	return &Flow{
		ID:          flowID,
		ColorNumber: color,
		MinRow:      row,
		MinColumn:   column,
		MaxRow:      row,
		MaxColumn:   column,
	}
}

// Flow represents a series of glyphs
type Flow struct {
	ID          int64
	ColorNumber int
	Glyphs      []Glyph
	Commits     []*Commit
	MinRow      int
	MinColumn   int
	MaxRow      int
	MaxColumn   int
}

// Color16 wraps the color numbers around mod 16
func (flow *Flow) Color16() int {
	return flow.ColorNumber % 16
}

// AddGlyph adds glyph at row and column
func (flow *Flow) AddGlyph(row, column int, glyph byte) {
	if row < flow.MinRow {
		flow.MinRow = row
	}
	if row > flow.MaxRow {
		flow.MaxRow = row
	}
	if column < flow.MinColumn {
		flow.MinColumn = column
	}
	if column > flow.MaxColumn {
		flow.MaxColumn = column
	}

	flow.Glyphs = append(flow.Glyphs, Glyph{
		Row:    row,
		Column: column,
		Glyph:  glyph,
	})
}

// Glyph represents a coordinate and glyph
type Glyph struct {
	Row          int
	Column       int
	Glyph        byte
	ConnectsUp   bool
	ConnectsDown bool
}

// RelationCommit represents an empty relation commit
var RelationCommit = &Commit{
	Row: -1,
}

// NewCommit creates a new commit from a provided line
func NewCommit(row, column int, line []byte) (*Commit, error) {
	data := bytes.SplitN(line, []byte("|"), 6)
	if len(data) < 6 {
		return nil, fmt.Errorf("malformed data section on line %d with commit: %s", row, string(line))
	}
	// Format is a slight modification from RFC1123Z
	t, err := time.Parse("Mon, _2 Jan 2006 15:04:05 -0700", string(data[3]))
	if err != nil {
		return nil, fmt.Errorf("could not parse date of commit: %w", err)
	}

	var parents []string
	for p := range bytes.FieldsSeq(data[0]) {
		parents = append(parents, string(p))
	}

	return &Commit{
		Row:    row,
		Column: column,
		// 0 matches git log --pretty=format:%P => parent hashes
		ParentHashes: parents,
		// 1 matches git log --pretty=format:%d => ref names, like the --decorate option of git-log(1)
		Refs: newRefsFromRefNames(data[1]),
		// 2 matches git log --pretty=format:%H => commit hash
		Rev: string(data[2]),
		// 3 matches git log --pretty=format:%aD => author date, RFC2822 style
		Date: t,
		// 4 matches git log --pretty=format:%h => abbreviated commit hash
		ShortRev: string(data[4]),
		// 5 matches git log --pretty=format:%s => subject
		Subject: string(data[5]),
	}, nil
}

func newRefsFromRefNames(refNames []byte) []git.Reference {
	refBytes := bytes.Split(refNames, []byte{',', ' '})
	refs := make([]git.Reference, 0, len(refBytes))
	for _, refNameBytes := range refBytes {
		if len(refNameBytes) == 0 {
			continue
		}
		refName := string(refNameBytes)
		if after, ok := strings.CutPrefix(refName, "tag: "); ok {
			refName = after
		} else {
			refName = strings.TrimPrefix(refName, "HEAD -> ")
		}
		refs = append(refs, git.Reference{
			Name: refName,
		})
	}
	return refs
}

// Commit represents a commit at coordinate X, Y with the data
type Commit struct {
	Commit       *git.Commit
	User         *user_model.User
	Verification *asymkey_model.ObjectVerification
	Status       *git_model.CommitStatus
	Flow         int64
	Row          int
	Column       int
	Refs         []git.Reference
	Rev          string
	Date         time.Time
	ShortRev     string
	Subject      string
	ParentHashes []string
}

// OnlyRelation returns whether this a relation only commit
func (c *Commit) OnlyRelation() bool {
	return c.Row == -1
}
