package helpers

import (
	"strings"

	"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
	"github.com/jesseduffield/lazygit/pkg/commands/models"
	"github.com/jesseduffield/lazygit/pkg/commands/patch"
	"github.com/jesseduffield/lazygit/pkg/gui/context"
	"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
	"github.com/jesseduffield/lazygit/pkg/gui/style"
	"github.com/jesseduffield/lazygit/pkg/gui/types"
	"github.com/samber/lo"
)

type DiffHelper struct {
	c *HelperCommon
}

func NewDiffHelper(c *HelperCommon) *DiffHelper {
	return &DiffHelper{
		c: c,
	}
}

func (self *DiffHelper) DiffArgs() []string {
	output := []string{"--stat", "-p", self.c.Modes().Diffing.Ref}

	right := self.currentDiffTerminal()
	if right != "" {
		output = append(output, right)
	}

	if self.c.Modes().Diffing.Reverse {
		output = append(output, "-R")
	}

	output = append(output, "--")

	file := self.currentlySelectedFilename()
	if file != "" {
		output = append(output, file)
	} else if self.c.Modes().Filtering.Active() {
		output = append(output, self.c.Modes().Filtering.GetPath())
	}

	return output
}

// Returns an update task that can be passed to RenderToMainViews to render a
// diff for the selected commit(s). We need to pass both the selected commit
// and the refRange for a range selection. If the refRange is nil (meaning that
// either there's no range, or it can't be diffed for some reason), then we want
// to fall back to rendering the diff for the single commit.
func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Commit, refRange *types.RefRange) types.UpdateTask {
	if refRange != nil {
		from, to := refRange.From, refRange.To
		args := []string{from.ParentRefName(), to.RefName(), "--stat", "-p"}
		args = append(args, "--")
		if filterPath := self.c.Modes().Filtering.GetPath(); filterPath != "" {
			// If both refs are commits, filter by the union of their paths. This is useful for
			// example when diffing a range of commits in filter-by-path mode across a rename.
			fromCommit, ok1 := from.(*models.Commit)
			toCommit, ok2 := to.(*models.Commit)
			if ok1 && ok2 {
				paths := append(self.FilterPathsForCommit(fromCommit), self.FilterPathsForCommit(toCommit)...)
				args = append(args, lo.Uniq(paths)...)
			} else {
				// If either ref is not a commit (which is possible in sticky diff mode, when
				// diffing against a branch or tag), we just filter by the filter path; that's the
				// best we can do in this case.
				args = append(args, filterPath)
			}
		}
		cmdObj := self.c.Git().Diff.DiffCmdObj(args)
		prefix := style.FgYellow.Sprintf("%s %s-%s\n\n", self.c.Tr.ShowingDiffForRange, from.ShortRefName(), to.ShortRefName())
		return types.NewRunPtyTaskWithPrefix(cmdObj.GetCmd(), prefix)
	}

	cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.FilterPathsForCommit(commit))
	return types.NewRunPtyTask(cmdObj.GetCmd())
}

func (self *DiffHelper) FilterPathsForCommit(commit *models.Commit) []string {
	filterPath := self.c.Modes().Filtering.GetPath()
	if filterPath != "" {
		if len(commit.FilterPaths) > 0 {
			return commit.FilterPaths
		}
		return []string{filterPath}
	}
	return nil
}

func (self *DiffHelper) ExitDiffMode() error {
	self.c.Modes().Diffing = diffing.New()
	self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
	return nil
}

func (self *DiffHelper) RenderDiff() {
	args := self.DiffArgs()
	cmdObj := self.c.Git().Diff.DiffCmdObj(args)
	prefix := style.FgMagenta.Sprintf(
		"%s %s\n\n",
		self.c.Tr.ShowingGitDiff,
		"git diff "+strings.Join(args, " "),
	)
	task := types.NewRunPtyTaskWithPrefix(cmdObj.GetCmd(), prefix)

	self.c.RenderToMainViews(types.RefreshMainOpts{
		Pair: self.c.MainViewPairs().Normal,
		Main: &types.ViewUpdateOpts{
			Title:    "Diff",
			SubTitle: self.IgnoringWhitespaceSubTitle(),
			Task:     task,
		},
	})
}

// CurrentDiffTerminals returns the current diff terminals of the currently selected item.
// in the case of a branch it returns both the branch and it's upstream name,
// which becomes an option when you bring up the diff menu, but when you're just
// flicking through branches it will be using the local branch name.
func (self *DiffHelper) CurrentDiffTerminals() []string {
	c := self.c.Context().CurrentSide()

	if c.GetKey() == "" {
		return nil
	}

	switch v := c.(type) {
	case types.DiffableContext:
		return v.GetDiffTerminals()
	}

	return nil
}

func (self *DiffHelper) currentDiffTerminal() string {
	names := self.CurrentDiffTerminals()
	if len(names) == 0 {
		return ""
	}
	return names[0]
}

func (self *DiffHelper) currentlySelectedFilename() string {
	currentContext := self.c.Context().Current()

	switch currentContext := currentContext.(type) {
	case types.IListContext:
		if lo.Contains([]types.ContextKey{context.FILES_CONTEXT_KEY, context.COMMIT_FILES_CONTEXT_KEY}, currentContext.GetKey()) {
			return currentContext.GetSelectedItemId()
		}
	}

	return ""
}

func (self *DiffHelper) WithDiffModeCheck(f func()) {
	if self.c.Modes().Diffing.Active() {
		self.RenderDiff()
	} else {
		f()
	}
}

func (self *DiffHelper) IgnoringWhitespaceSubTitle() string {
	if self.c.UserConfig().Git.IgnoreWhitespaceInDiffView {
		return self.c.Tr.IgnoreWhitespaceDiffViewSubTitle
	}

	return ""
}

func (self *DiffHelper) OpenDiffToolForRef(selectedRef models.Ref) error {
	to := selectedRef.RefName()
	from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff("")
	_, err := self.c.RunSubprocess(self.c.Git().Diff.OpenDiffToolCmdObj(
		git_commands.DiffToolCmdOptions{
			Filepath:    ".",
			FromCommit:  from,
			ToCommit:    to,
			Reverse:     reverse,
			IsDirectory: true,
			Staged:      false,
		}))
	return err
}

// AdjustLineNumber is used to adjust a line number in the diff that's currently
// being viewed, so that it corresponds to the line number in the actual working
// copy state of the file. It is used when clicking on a delta hyperlink in a
// diff, or when pressing `e` in the staging or patch building panels. It works
// by getting a diff of what's being viewed in the main view against the working
// copy, and then using that diff to adjust the line number.
// path is the file path of the file being viewed
// linenumber is the line number to adjust (one-based)
// viewname is the name of the view that shows the diff. We need to pass it
// because the diff adjustment is slightly different depending on which view is
// showing the diff.
func (self *DiffHelper) AdjustLineNumber(path string, linenumber int, viewname string) int {
	switch viewname {

	case "main", "patchBuilding":
		if diffableContext, ok := self.c.Context().CurrentSide().(types.DiffableContext); ok {
			ref := diffableContext.RefForAdjustingLineNumberInDiff()
			if len(ref) != 0 {
				return self.adjustLineNumber(linenumber, ref, "--", path)
			}
		}
		// if the type cast to DiffableContext returns false, we are in the
		// unstaged changes view of the Files panel; no need to adjust line
		// numbers in this case

	case "secondary", "stagingSecondary":
		return self.adjustLineNumber(linenumber, "--", path)
	}

	return linenumber
}

func (self *DiffHelper) adjustLineNumber(linenumber int, diffArgs ...string) int {
	args := append([]string{"--unified=0"}, diffArgs...)
	diff, err := self.c.Git().Diff.GetDiff(false, args...)
	if err != nil {
		return linenumber
	}
	patch := patch.Parse(diff)
	return patch.AdjustLineNumber(linenumber)
}
