// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future

package gui

import (
	"bufio"
	"bytes"
	"fmt"
	"io/ioutil"
	"math"
	"os"
	"strings"

	"github.com/fatih/color"
	"github.com/go-errors/errors"
	"github.com/golang-collections/collections/stack"
	"github.com/jesseduffield/gocui"
	"github.com/jesseduffield/lazygit/pkg/commands"
	"github.com/jesseduffield/lazygit/pkg/theme"
	"github.com/jesseduffield/lazygit/pkg/utils"
)

func (gui *Gui) findConflicts(content string) []commands.Conflict {
	conflicts := make([]commands.Conflict, 0)

	if content == "" {
		return conflicts
	}

	var newConflict commands.Conflict
	for i, line := range utils.SplitLines(content) {
		trimmedLine := strings.TrimPrefix(line, "++")
		if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" || trimmedLine == "<<<<<<< ours" {
			newConflict = commands.Conflict{Start: i}
		} else if trimmedLine == "=======" {
			newConflict.Middle = i
		} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
			newConflict.End = i
			conflicts = append(conflicts, newConflict)
		}
	}
	return conflicts
}

func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
	return conflicts[0], conflicts[1:]
}

func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
	return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}

func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
	if len(conflicts) == 0 {
		return content
	}
	conflict, remainingConflicts := gui.shiftConflict(conflicts)
	var outputBuffer bytes.Buffer
	for i, line := range utils.SplitLines(content) {
		colourAttr := theme.DefaultTextColor
		if i == conflict.Start || i == conflict.Middle || i == conflict.End {
			colourAttr = color.FgRed
		}
		colour := color.New(colourAttr)
		if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
			colour.Add(color.Bold)
			colour.Add(theme.SelectedRangeBgColor)
		}
		if i == conflict.End && len(remainingConflicts) > 0 {
			conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
		}
		outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
	}
	return outputBuffer.String()
}

func (gui *Gui) takeOverScrolling() {
	gui.State.Panels.Merging.UserScrolling = false
}

func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
	gui.takeOverScrolling()
	gui.State.Panels.Merging.ConflictTop = true
	return gui.refreshMergePanel()
}

func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
	gui.takeOverScrolling()
	gui.State.Panels.Merging.ConflictTop = false
	return gui.refreshMergePanel()
}

func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
	gui.takeOverScrolling()
	if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
		return nil
	}
	gui.State.Panels.Merging.ConflictIndex++
	return gui.refreshMergePanel()
}

func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
	gui.takeOverScrolling()
	if gui.State.Panels.Merging.ConflictIndex <= 0 {
		return nil
	}
	gui.State.Panels.Merging.ConflictIndex--
	return gui.refreshMergePanel()
}

func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
	return i == conflict.Middle ||
		i == conflict.Start ||
		i == conflict.End ||
		pick != "both" &&
			(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
		(pick == "top" && i > conflict.Middle && i < conflict.End)
}

func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
	gitFile := gui.getSelectedFile()
	if gitFile == nil {
		return nil
	}
	file, err := os.Open(gitFile.Name)
	if err != nil {
		return err
	}
	defer file.Close()

	reader := bufio.NewReader(file)
	output := ""
	for i := 0; true; i++ {
		line, err := reader.ReadString('\n')
		if err != nil {
			break
		}
		if !gui.isIndexToDelete(i, conflict, pick) {
			output += line
		}
	}
	return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}

func (gui *Gui) pushFileSnapshot() error {
	gitFile := gui.getSelectedFile()
	if gitFile == nil {
		return nil
	}
	content, err := gui.GitCommand.CatFile(gitFile.Name)
	if err != nil {
		return err
	}
	gui.State.Panels.Merging.EditHistory.Push(content)
	return nil
}

func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
	if gui.State.Panels.Merging.EditHistory.Len() == 0 {
		return nil
	}
	prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
	gitFile := gui.getSelectedFile()
	if gitFile == nil {
		return nil
	}
	if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
		return err
	}

	return gui.refreshMergePanel()
}

func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
	gui.takeOverScrolling()

	conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
	if err := gui.pushFileSnapshot(); err != nil {
		return err
	}

	pick := "bottom"
	if gui.State.Panels.Merging.ConflictTop {
		pick = "top"
	}
	err := gui.resolveConflict(conflict, pick)
	if err != nil {
		panic(err)
	}

	// if that was the last conflict, finish the merge for this file
	if len(gui.State.Panels.Merging.Conflicts) == 1 {
		if err := gui.handleCompleteMerge(); err != nil {
			return err
		}
	}
	return gui.refreshMergePanel()
}

func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
	gui.takeOverScrolling()

	conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
	if err := gui.pushFileSnapshot(); err != nil {
		return err
	}
	err := gui.resolveConflict(conflict, "both")
	if err != nil {
		panic(err)
	}
	return gui.refreshMergePanel()
}

func (gui *Gui) refreshMergePanel() error {
	panelState := gui.State.Panels.Merging
	cat, err := gui.catSelectedFile()
	if err != nil {
		return gui.refreshMainViews(refreshMainOpts{
			main: &viewUpdateOpts{
				title: "",
				task:  gui.createRenderStringTask(err.Error()),
			},
		})
	}

	panelState.Conflicts = gui.findConflicts(cat)

	// handle potential fixes that the user made in their editor since we last refreshed
	if len(panelState.Conflicts) == 0 {
		return gui.handleCompleteMerge()
	} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
		panelState.ConflictIndex = len(panelState.Conflicts) - 1
	}

	hasFocus := gui.currentViewName() == "main"
	content := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)

	if err := gui.scrollToConflict(); err != nil {
		return err
	}

	return gui.refreshMainViews(refreshMainOpts{
		main: &viewUpdateOpts{
			title:  gui.Tr.MergeConflictsTitle,
			task:   gui.createRenderStringWithoutScrollTask(content),
			noWrap: true,
		},
	})
}

func (gui *Gui) catSelectedFile() (string, error) {
	item := gui.getSelectedFile()
	if item == nil {
		return "", errors.New(gui.Tr.NoFilesDisplay)
	}

	if item.Type != "file" {
		return "", errors.New(gui.Tr.NotAFile)
	}

	cat, err := gui.GitCommand.CatFile(item.Name)
	if err != nil {
		gui.Log.Error(err)
		return "", err
	}
	return cat, nil
}

func (gui *Gui) scrollToConflict() error {
	if gui.State.Panels.Merging.UserScrolling {
		return nil
	}

	panelState := gui.State.Panels.Merging
	if len(panelState.Conflicts) == 0 {
		return nil
	}
	mergingView := gui.getMainView()
	conflict := panelState.Conflicts[panelState.ConflictIndex]
	ox, _ := mergingView.Origin()
	_, height := mergingView.Size()
	conflictMiddle := (conflict.End + conflict.Start) / 2
	newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
	gui.g.Update(func(g *gocui.Gui) error {
		return mergingView.SetOrigin(ox, newOriginY)
	})
	return nil
}

func (gui *Gui) getMergingOptions() map[string]string {
	keybindingConfig := gui.Config.GetUserConfig().Keybinding

	return map[string]string{
		fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)):   gui.Tr.LcSelectHunk,
		fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock)): gui.Tr.LcNavigateConflicts,
		gui.getKeyDisplay(keybindingConfig.Universal.Select):   gui.Tr.LcPickHunk,
		gui.getKeyDisplay(keybindingConfig.Main.PickBothHunks): gui.Tr.LcPickBothHunks,
		gui.getKeyDisplay(keybindingConfig.Universal.Undo):     gui.Tr.LcUndo,
	}
}

func (gui *Gui) handleEscapeMerge() error {
	gui.takeOverScrolling()

	gui.State.Panels.Merging.EditHistory = stack.New()
	if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
		return err
	}
	// it's possible this method won't be called from the merging view so we need to
	// ensure we only 'return' focus if we already have it
	if gui.g.CurrentView() == gui.getMainView() {
		return gui.pushContext(gui.Contexts.Files.Context)
	}
	return nil
}

func (gui *Gui) handleCompleteMerge() error {
	if err := gui.stageSelectedFile(); err != nil {
		return err
	}
	if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
		return err
	}
	// if we got conflicts after unstashing, we don't want to call any git
	// commands to continue rebasing/merging here
	if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_NORMAL {
		return gui.handleEscapeMerge()
	}
	// if there are no more files with merge conflicts, we should ask whether the user wants to continue
	if !gui.anyFilesWithMergeConflicts() {
		return gui.promptToContinue()
	}
	return gui.handleEscapeMerge()
}

// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinue() error {
	gui.takeOverScrolling()

	return gui.ask(askOpts{
		title:               "continue",
		prompt:              gui.Tr.ConflictsResolved,
		handlersManageFocus: true,
		handleConfirm: func() error {
			if err := gui.pushContext(gui.Contexts.Files.Context); err != nil {
				return err
			}

			return gui.genericMergeCommand("continue")
		},
		handleClose: func() error {
			return gui.pushContext(gui.Contexts.Files.Context)
		},
	})
}

func (gui *Gui) canScrollMergePanel() bool {
	currentViewName := gui.currentViewName()
	if currentViewName != "main" {
		return false
	}

	file := gui.getSelectedFile()
	if file == nil {
		return false
	}

	return file.HasInlineMergeConflicts
}
