// 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 (
	"fmt"
	"io/ioutil"
	"math"

	"github.com/go-errors/errors"
	"github.com/jesseduffield/gocui"
	"github.com/jesseduffield/lazygit/pkg/commands"
	"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
	"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
)

func (gui *Gui) handleSelectTop() error {
	return gui.withMergeConflictLock(func() error {
		gui.takeOverMergeConflictScrolling()
		gui.State.Panels.Merging.SelectTopOption()
		return gui.refreshMergePanel()
	})
}

func (gui *Gui) handleSelectBottom() error {
	return gui.withMergeConflictLock(func() error {
		gui.takeOverMergeConflictScrolling()
		gui.State.Panels.Merging.SelectBottomOption()
		return gui.refreshMergePanel()
	})
}

func (gui *Gui) handleSelectNextConflict() error {
	return gui.withMergeConflictLock(func() error {
		gui.takeOverMergeConflictScrolling()
		gui.State.Panels.Merging.SelectNextConflict()
		return gui.refreshMergePanel()
	})
}

func (gui *Gui) handleSelectPrevConflict() error {
	return gui.withMergeConflictLock(func() error {
		gui.takeOverMergeConflictScrolling()
		gui.State.Panels.Merging.SelectPrevConflict()
		return gui.refreshMergePanel()
	})
}

func (gui *Gui) pushFileSnapshot() error {
	content, err := gui.catSelectedFile()
	if err != nil {
		return err
	}
	gui.State.Panels.Merging.PushFileSnapshot(content)
	return nil
}

func (gui *Gui) handlePopFileSnapshot() error {
	prevContent, ok := gui.State.Panels.Merging.PopFileSnapshot()
	if !ok {
		return nil
	}

	gitFile := gui.getSelectedFile()
	if gitFile == nil {
		return nil
	}
	gui.OnRunCommand(oscommands.NewCmdLogEntry("Undoing last conflict resolution", "Undo merge conflict resolution", false))
	if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
		return err
	}

	return gui.refreshMergePanel()
}

func (gui *Gui) handlePickHunk() error {
	return gui.withMergeConflictLock(func() error {
		gui.takeOverMergeConflictScrolling()

		ok, err := gui.resolveConflict(gui.State.Panels.Merging.Selection())
		if err != nil {
			return err
		}

		if !ok {
			return nil
		}

		if gui.State.Panels.Merging.IsFinalConflict() {
			if err := gui.handleCompleteMerge(); err != nil {
				return err
			}
		}
		return gui.refreshMergePanel()
	})
}

func (gui *Gui) handlePickBothHunks() error {
	return gui.withMergeConflictLock(func() error {
		gui.takeOverMergeConflictScrolling()

		ok, err := gui.resolveConflict(mergeconflicts.BOTH)
		if err != nil {
			return err
		}

		if !ok {
			return nil
		}

		return gui.refreshMergePanel()
	})
}

func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error) {
	gitFile := gui.getSelectedFile()
	if gitFile == nil {
		return false, nil
	}

	ok, output, err := gui.State.Panels.Merging.ContentAfterConflictResolve(gitFile.Name, selection)
	if err != nil {
		return false, err
	}

	if !ok {
		return false, nil
	}

	if err := gui.pushFileSnapshot(); err != nil {
		return false, gui.surfaceError(err)
	}

	var logStr string
	switch selection {
	case mergeconflicts.TOP:
		logStr = "Picking top hunk"
	case mergeconflicts.BOTTOM:
		logStr = "Picking bottom hunk"
	case mergeconflicts.BOTH:
		logStr = "Picking both hunks"
	}
	gui.OnRunCommand(oscommands.NewCmdLogEntry(logStr, "Resolve merge conflict", false))
	return true, ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}

func (gui *Gui) refreshMergePanelWithLock() error {
	return gui.withMergeConflictLock(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:  NewRenderStringTask(err.Error()),
			},
		})
	}

	panelState.SetConflictsFromCat(cat)

	if panelState.NoConflicts() {
		return gui.handleCompleteMerge()
	}

	hasFocus := gui.currentViewName() == "main"
	content := mergeconflicts.ColoredConflictFile(cat, panelState.State, hasFocus)

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

	return gui.refreshMainViews(refreshMainOpts{
		main: &viewUpdateOpts{
			title:  gui.Tr.MergeConflictsTitle,
			task:   NewRenderStringWithoutScrollTask(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 panelState.NoConflicts() {
		return nil
	}

	gui.centerYPos(gui.Views.Main, panelState.GetConflictMiddle())

	return nil
}

func (gui *Gui) centerYPos(view *gocui.View, y int) {
	ox, _ := view.Origin()
	_, height := view.Size()
	newOriginY := int(math.Max(0, float64(y-(height/2))))
	gui.g.Update(func(g *gocui.Gui) error {
		return view.SetOrigin(ox, newOriginY)
	})
}

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.takeOverMergeConflictScrolling()

	gui.State.Panels.Merging.Reset()
	if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{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.Views.Main {
		return gui.pushContext(gui.State.Contexts.Files)
	}
	return nil
}

func (gui *Gui) handleCompleteMerge() error {
	if err := gui.stageSelectedFile(); err != nil {
		return err
	}
	if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{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.promptToContinueRebase()
	}
	return gui.handleEscapeMerge()
}

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

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

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

func (gui *Gui) canScrollMergePanel() bool {
	currentView := gui.g.CurrentView()
	if currentView != gui.Views.Main && currentView != gui.Views.Files {
		return false
	}

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

	return file.HasInlineMergeConflicts
}

func (gui *Gui) withMergeConflictLock(f func() error) error {
	gui.State.Panels.Merging.Lock()
	defer gui.State.Panels.Merging.Unlock()

	return f()
}

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