package main

import (
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/jesseduffield/gocui"
)

var cyclableViews = []string{"files", "branches", "commits", "stash"}

func refreshSidePanels(g *gocui.Gui) error {
	refreshBranches(g)
	refreshFiles(g)
	refreshCommits(g)
	return nil
}

func nextView(g *gocui.Gui, v *gocui.View) error {
	var focusedViewName string
	if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
		focusedViewName = cyclableViews[0]
	} else {
		for i := range cyclableViews {
			if v.Name() == cyclableViews[i] {
				focusedViewName = cyclableViews[i+1]
				break
			}
			if i == len(cyclableViews)-1 {
				devLog(v.Name() + " is not in the list of views")
				return nil
			}
		}
	}
	focusedView, err := g.View(focusedViewName)
	if err != nil {
		panic(err)
	}
	return switchFocus(g, v, focusedView)
}

func previousView(g *gocui.Gui, v *gocui.View) error {
	var focusedViewName string
	if v == nil || v.Name() == cyclableViews[0] {
		focusedViewName = cyclableViews[len(cyclableViews)-1]
	} else {
		for i := range cyclableViews {
			if v.Name() == cyclableViews[i] {
				focusedViewName = cyclableViews[i-1] // TODO: make this work properly
				break
			}
			if i == len(cyclableViews)-1 {
				devLog(v.Name() + " is not in the list of views")
				return nil
			}
		}
	}
	focusedView, err := g.View(focusedViewName)
	if err != nil {
		panic(err)
	}
	return switchFocus(g, v, focusedView)
}

func newLineFocused(g *gocui.Gui, v *gocui.View) error {
	mainView, _ := g.View("main")
	mainView.SetOrigin(0, 0)

	switch v.Name() {
	case "files":
		return handleFileSelect(g, v)
	case "branches":
		return handleBranchSelect(g, v)
	case "confirmation":
		return nil
	case "main":
		// TODO: pull this out into a 'view focused' function
		refreshMergePanel(g)
		v.Highlight = false
		return nil
	case "commits":
		return handleCommitSelect(g, v)
	case "stash":
		return handleStashEntrySelect(g, v)
	default:
		panic("No view matching newLineFocused switch statement")
	}
}

func returnFocus(g *gocui.Gui, v *gocui.View) error {
	previousView, err := g.View(state.PreviousView)
	if err != nil {
		panic(err)
	}
	return switchFocus(g, v, previousView)
}

// pass in oldView = nil if you don't want to be able to return to your old view
func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
	// we assume we'll never want to return focus to a confirmation panel i.e.
	// we should never stack confirmation panels
	if oldView != nil && oldView.Name() != "confirmation" {
		oldView.Highlight = false
		devLog("setting previous view to:", oldView.Name())
		state.PreviousView = oldView.Name()
	}
	newView.Highlight = true
	devLog(newView.Name())
	if _, err := g.SetCurrentView(newView.Name()); err != nil {
		return err
	}
	g.Cursor = newView.Editable
	return newLineFocused(g, newView)
}

func getItemPosition(v *gocui.View) int {
	_, cy := v.Cursor()
	_, oy := v.Origin()
	return oy + cy
}

func trimmedContent(v *gocui.View) string {
	return strings.TrimSpace(v.Buffer())
}

func cursorUp(g *gocui.Gui, v *gocui.View) error {
	// swallowing cursor movements in main
	// TODO: pull this out
	if v == nil || v.Name() == "main" {
		return nil
	}

	ox, oy := v.Origin()
	cx, cy := v.Cursor()
	if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
		if err := v.SetOrigin(ox, oy-1); err != nil {
			return err
		}
	}

	newLineFocused(g, v)
	return nil
}

func cursorDown(g *gocui.Gui, v *gocui.View) error {
	// swallowing cursor movements in main
	// TODO: pull this out
	if v == nil || v.Name() == "main" {
		return nil
	}
	cx, cy := v.Cursor()
	ox, oy := v.Origin()
	if cy+oy >= len(v.BufferLines())-2 {
		return nil
	}
	if err := v.SetCursor(cx, cy+1); err != nil {
		if err := v.SetOrigin(ox, oy+1); err != nil {
			return err
		}
	}

	newLineFocused(g, v)
	return nil
}

func resetOrigin(v *gocui.View) error {
	if err := v.SetCursor(0, 0); err != nil {
		return err
	}
	return v.SetOrigin(0, 0)
}

// if the cursor down past the last item, move it up one
func correctCursor(v *gocui.View) error {
	cx, cy := v.Cursor()
	_, oy := v.Origin()
	lineCount := len(v.BufferLines()) - 2
	if cy >= lineCount-oy {
		return v.SetCursor(cx, lineCount-oy)
	}
	return nil
}

func renderString(g *gocui.Gui, viewName, s string) error {
	g.Update(func(*gocui.Gui) error {
		v, err := g.View(viewName)
		// just in case the view disappeared as this function was called, we'll
		// silently return if it's not found
		if err != nil {
			return nil
		}
		v.Clear()
		fmt.Fprint(v, s)
		v.Wrap = true
		return nil
	})
	return nil
}

func splitLines(multilineString string) []string {
	multilineString = strings.Replace(multilineString, "\r", "", -1)
	if multilineString == "" || multilineString == "\n" {
		return make([]string, 0)
	}
	lines := strings.Split(multilineString, "\n")
	if lines[len(lines)-1] == "" {
		return lines[:len(lines)-1]
	}
	return lines
}

func optionsMapToString(optionsMap map[string]string) string {
	optionsArray := make([]string, 0)
	for key, description := range optionsMap {
		optionsArray = append(optionsArray, key+": "+description)
	}
	sort.Strings(optionsArray)
	return strings.Join(optionsArray, ", ")
}

func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
	return renderString(g, "options", optionsMapToString(optionsMap))
}

func loader() string {
	characters := "|/-\\"
	now := time.Now()
	nanos := now.UnixNano()
	index := nanos / 50000000 % int64(len(characters))
	return characters[index : index+1]
}
