// 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/jesseduffield/gocui"
	"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
	"github.com/jesseduffield/lazygit/pkg/gui/types"
)

func (gui *Gui) handleSelectPrevConflictHunk() error {
	return gui.withMergeConflictLock(func() error {
		gui.takeOverMergeConflictScrolling()
		gui.State.Panels.Merging.SelectPrevConflictHunk()
		return gui.renderConflictsWithFocus()
	})
}

func (gui *Gui) handleSelectNextConflictHunk() error {
	return gui.withMergeConflictLock(func() error {
		gui.takeOverMergeConflictScrolling()
		gui.State.Panels.Merging.SelectNextConflictHunk()
		return gui.renderConflictsWithFocus()
	})
}

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

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

func (gui *Gui) handleMergeConflictUndo() error {
	state := gui.State.Panels.Merging

	ok := state.Undo()
	if !ok {
		return nil
	}

	gui.c.LogAction("Restoring file to previous state")
	gui.LogCommand("Undoing last conflict resolution", false)
	if err := ioutil.WriteFile(state.GetPath(), []byte(state.GetContent()), 0644); err != nil {
		return err
	}

	return gui.renderConflictsWithFocus()
}

func (gui *Gui) handlePickHunk() error {
	return gui.withMergeConflictLock(func() error {
		ok, err := gui.resolveConflict(gui.State.Panels.Merging.Selection())
		if err != nil {
			return err
		}

		if !ok {
			return nil
		}

		if gui.State.Panels.Merging.AllConflictsResolved() {
			return gui.onLastConflictResolved()
		}

		return gui.renderConflictsWithFocus()
	})
}

func (gui *Gui) handlePickAllHunks() error {
	return gui.withMergeConflictLock(func() error {
		ok, err := gui.resolveConflict(mergeconflicts.ALL)
		if err != nil {
			return err
		}

		if !ok {
			return nil
		}

		if gui.State.Panels.Merging.AllConflictsResolved() {
			return gui.onLastConflictResolved()
		}

		return gui.renderConflictsWithFocus()
	})
}

func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error) {
	gui.takeOverMergeConflictScrolling()

	state := gui.State.Panels.Merging

	ok, content, err := state.ContentAfterConflictResolve(selection)
	if err != nil {
		return false, err
	}

	if !ok {
		return false, nil
	}

	var logStr string
	switch selection {
	case mergeconflicts.TOP:
		logStr = "Picking top hunk"
	case mergeconflicts.MIDDLE:
		logStr = "Picking middle hunk"
	case mergeconflicts.BOTTOM:
		logStr = "Picking bottom hunk"
	case mergeconflicts.ALL:
		logStr = "Picking all hunks"
	}
	gui.c.LogAction("Resolve merge conflict")
	gui.LogCommand(logStr, false)
	state.PushContent(content)
	return true, ioutil.WriteFile(state.GetPath(), []byte(content), 0644)
}

// precondition: we actually have conflicts to render
func (gui *Gui) renderConflicts(hasFocus bool) error {
	state := gui.State.Panels.Merging.State
	content := mergeconflicts.ColoredConflictFile(state, hasFocus)

	if !gui.State.Panels.Merging.UserVerticalScrolling {
		// TODO: find a way to not have to do this OnUIThread thing. Why doesn't it work
		// without it given that we're calling the 'no scroll' variant below?
		gui.OnUIThread(func() error {
			gui.State.Panels.Merging.Lock()
			defer gui.State.Panels.Merging.Unlock()

			if !state.Active() {
				return nil
			}

			gui.centerYPos(gui.Views.Main, state.GetConflictMiddle())
			return nil
		})
	}

	return gui.refreshMainViews(refreshMainOpts{
		main: &viewUpdateOpts{
			title:   gui.c.Tr.MergeConflictsTitle,
			task:    NewRenderStringWithoutScrollTask(content),
			context: gui.State.Contexts.Merging,
			noWrap:  true,
		},
	})
}

func (gui *Gui) renderConflictsWithFocus() error {
	return gui.renderConflicts(true)
}

func (gui *Gui) renderConflictsWithLock(hasFocus bool) error {
	return gui.withMergeConflictLock(func() error {
		return gui.renderConflicts(hasFocus)
	})
}

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

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

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

func (gui *Gui) handleEscapeMerge() error {
	if err := gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}); err != nil {
		return err
	}

	return gui.escapeMerge()
}

func (gui *Gui) onLastConflictResolved() error {
	// as part of refreshing files, we handle the situation where a file has had
	// its merge conflicts resolved.
	return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
}

func (gui *Gui) resetMergeState() {
	gui.takeOverMergeConflictScrolling()
	gui.State.Panels.Merging.Reset()
}

func (gui *Gui) setMergeState(path string) (bool, error) {
	content, err := gui.git.File.Cat(path)
	if err != nil {
		return false, err
	}

	gui.State.Panels.Merging.SetContent(content, path)

	return !gui.State.Panels.Merging.NoConflicts(), nil
}

func (gui *Gui) setMergeStateWithLock(path string) (bool, error) {
	gui.State.Panels.Merging.Lock()
	defer gui.State.Panels.Merging.Unlock()

	return gui.setMergeState(path)
}

func (gui *Gui) resetMergeStateWithLock() {
	gui.State.Panels.Merging.Lock()
	defer gui.State.Panels.Merging.Unlock()

	gui.resetMergeState()
}

func (gui *Gui) escapeMerge() error {
	gui.resetMergeState()

	// doing this in separate UI thread so that we're not still holding the lock by the time refresh the file
	gui.OnUIThread(func() error {
		return gui.pushContext(gui.State.Contexts.Files)
	})
	return nil
}

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

	return gui.State.Panels.Merging.Active()
}

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.UserVerticalScrolling = false
}

func (gui *Gui) setConflictsAndRender(path string, hasFocus bool) (bool, error) {
	hasConflicts, err := gui.setMergeState(path)
	if err != nil {
		return false, err
	}

	if hasConflicts {
		return true, gui.renderConflicts(hasFocus)
	}

	return false, nil
}

func (gui *Gui) setConflictsAndRenderWithLock(path string, hasFocus bool) (bool, error) {
	gui.State.Panels.Merging.Lock()
	defer gui.State.Panels.Merging.Unlock()

	return gui.setConflictsAndRender(path, hasFocus)
}

func (gui *Gui) switchToMerge(path string) error {
	gui.takeOverMergeConflictScrolling()

	if gui.State.Panels.Merging.GetPath() != path {
		hasConflicts, err := gui.setMergeStateWithLock(path)
		if err != nil {
			return err
		}
		if !hasConflicts {
			return nil
		}
	}

	return gui.c.PushContext(gui.State.Contexts.Merging)
}