package modes

import (
	"os"
	"sort"
	"strings"
	"sync"
	"unicode"

	"src.elv.sh/pkg/cli"
	"src.elv.sh/pkg/cli/term"
	"src.elv.sh/pkg/cli/tk"
	"src.elv.sh/pkg/ui"
)

type Navigation interface {
	tk.Widget
	// SelectedName returns the currently selected name. It returns an empty
	// string if there is no selected name, which can happen if the current
	// directory is empty.
	SelectedName() string
	// Select changes the selection.
	Select(f func(tk.ListBoxState) int)
	// ScrollPreview scrolls the preview.
	ScrollPreview(delta int)
	// Ascend ascends to the parent directory.
	Ascend()
	// Descend descends into the currently selected child directory.
	Descend()
	// MutateFiltering changes the filtering status.
	MutateFiltering(f func(bool) bool)
	// MutateShowHidden changes whether hidden files - files whose names start
	// with ".", should be shown.
	MutateShowHidden(f func(bool) bool)
}

// NavigationSpec specifieis the configuration for the navigation mode.
type NavigationSpec struct {
	// Key bindings.
	Bindings tk.Bindings
	// Underlying filesystem.
	Cursor NavigationCursor
	// A function that returns the relative weights of the widths of the 3
	// columns. If unspecified, the ratio is 1:3:4.
	WidthRatio func() [3]int
	// Configuration for the filter.
	Filter FilterSpec
	// RPrompt of the code area (first row of the widget).
	CodeAreaRPrompt func() ui.Text
}

type navigationState struct {
	Filtering  bool
	ShowHidden bool
}

type navigation struct {
	NavigationSpec
	app        cli.App
	attachedTo tk.CodeArea
	codeArea   tk.CodeArea
	colView    tk.ColView
	lastFilter string
	stateMutex sync.RWMutex
	state      navigationState
}

func (w *navigation) MutateState(f func(*navigationState)) {
	w.stateMutex.Lock()
	defer w.stateMutex.Unlock()
	f(&w.state)
}

func (w *navigation) CopyState() navigationState {
	w.stateMutex.RLock()
	defer w.stateMutex.RUnlock()
	return w.state
}

func (w *navigation) Handle(event term.Event) bool {
	if w.colView.Handle(event) {
		return true
	}
	if w.CopyState().Filtering {
		if w.codeArea.Handle(event) {
			filter := w.codeArea.CopyState().Buffer.Content
			if filter != w.lastFilter {
				w.lastFilter = filter
				updateState(w, "")
			}
			return true
		}
		return false
	}
	return w.attachedTo.Handle(event)
}

func (w *navigation) Render(width, height int) *term.Buffer {
	buf := w.codeArea.Render(width, height)
	bufColView := w.colView.Render(width, height-len(buf.Lines))
	buf.Extend(bufColView, false)
	return buf
}

func (w *navigation) MaxHeight(width, height int) int {
	return w.codeArea.MaxHeight(width, height) + w.colView.MaxHeight(width, height)
}

func (w *navigation) Focus() bool {
	return w.CopyState().Filtering
}

func (w *navigation) ascend() {
	// Remember the name of the current directory before ascending.
	currentName := ""
	current, err := w.Cursor.Current()
	if err == nil {
		currentName = current.Name()
	}

	err = w.Cursor.Ascend()
	if err != nil {
		w.app.Notify(ErrorText(err))
	} else {
		w.codeArea.MutateState(func(s *tk.CodeAreaState) {
			s.Buffer = tk.CodeBuffer{}
		})
		w.lastFilter = ""
		updateState(w, currentName)
	}
}

func (w *navigation) descend() {
	currentCol, ok := w.colView.CopyState().Columns[1].(tk.ListBox)
	if !ok {
		return
	}
	state := currentCol.CopyState()
	if state.Items.Len() == 0 {
		return
	}
	selected := state.Items.(fileItems)[state.Selected]
	if !selected.IsDirDeep() {
		return
	}
	err := w.Cursor.Descend(selected.Name())
	if err != nil {
		w.app.Notify(ErrorText(err))
	} else {
		w.codeArea.MutateState(func(s *tk.CodeAreaState) {
			s.Buffer = tk.CodeBuffer{}
		})
		w.lastFilter = ""
		updateState(w, "")
	}
}

// NewNavigation creates a new navigation mode.
func NewNavigation(app cli.App, spec NavigationSpec) (Navigation, error) {
	codeArea, err := FocusedCodeArea(app)
	if err != nil {
		return nil, err
	}
	if spec.Cursor == nil {
		spec.Cursor = NewOSNavigationCursor(os.Chdir)
	}
	if spec.WidthRatio == nil {
		spec.WidthRatio = func() [3]int { return [3]int{1, 3, 4} }
	}

	var w *navigation
	w = &navigation{
		NavigationSpec: spec,
		app:            app,
		attachedTo:     codeArea,
		codeArea: tk.NewCodeArea(tk.CodeAreaSpec{
			Prompt: func() ui.Text {
				if w.CopyState().ShowHidden {
					return modeLine(" NAVIGATING (show hidden) ", true)
				}
				return modeLine(" NAVIGATING ", true)
			},
			RPrompt:     spec.CodeAreaRPrompt,
			Highlighter: spec.Filter.Highlighter,
		}),
		colView: tk.NewColView(tk.ColViewSpec{
			Bindings: spec.Bindings,
			Weights: func(int) []int {
				a := spec.WidthRatio()
				return a[:]
			},
			OnLeft:  func(tk.ColView) { w.ascend() },
			OnRight: func(tk.ColView) { w.descend() },
		}),
	}
	updateState(w, "")
	return w, nil
}

func (w *navigation) SelectedName() string {
	col, ok := w.colView.CopyState().Columns[1].(tk.ListBox)
	if !ok {
		return ""
	}
	state := col.CopyState()
	if 0 <= state.Selected && state.Selected < state.Items.Len() {
		return state.Items.(fileItems)[state.Selected].Name()
	}
	return ""
}

func updateState(w *navigation, selectName string) {
	colView := w.colView
	cursor := w.Cursor
	filter := w.lastFilter
	showHidden := w.CopyState().ShowHidden

	var parentCol, currentCol tk.Widget

	colView.MutateState(func(s *tk.ColViewState) {
		*s = tk.ColViewState{
			Columns: []tk.Widget{
				tk.Empty{}, tk.Empty{}, tk.Empty{}},
			FocusColumn: 1,
		}
	})

	parent, err := cursor.Parent()
	if err == nil {
		parentCol = makeCol(parent, showHidden)
	} else {
		parentCol = makeErrCol(err)
	}

	current, err := cursor.Current()
	if err == nil {
		currentCol = makeColInner(
			current,
			w.Filter.makePredicate(filter),
			showHidden,
			func(it tk.Items, i int) {
				previewCol := makeCol(it.(fileItems)[i], showHidden)
				colView.MutateState(func(s *tk.ColViewState) {
					s.Columns[2] = previewCol
				})
			})
		tryToSelectName(parentCol, current.Name())
		if selectName != "" {
			tryToSelectName(currentCol, selectName)
		}
	} else {
		currentCol = makeErrCol(err)
		tryToSelectNothing(parentCol)
	}

	colView.MutateState(func(s *tk.ColViewState) {
		s.Columns[0] = parentCol
		s.Columns[1] = currentCol
	})
}

// Selects nothing if the widget is a listbox.
func tryToSelectNothing(w tk.Widget) {
	list, ok := w.(tk.ListBox)
	if !ok {
		return
	}
	list.Select(func(tk.ListBoxState) int { return -1 })
}

// Selects the item with the given name, if the widget is a listbox with
// fileItems and has such an item.
func tryToSelectName(w tk.Widget, name string) {
	list, ok := w.(tk.ListBox)
	if !ok {
		// Do nothing
		return
	}
	list.Select(func(state tk.ListBoxState) int {
		items, ok := state.Items.(fileItems)
		if !ok {
			return 0
		}
		for i, file := range items {
			if file.Name() == name {
				return i
			}
		}
		return 0
	})
}

func makeCol(f NavigationFile, showHidden bool) tk.Widget {
	return makeColInner(f, func(string) bool { return true }, showHidden, nil)
}

func makeColInner(f NavigationFile, filter func(string) bool, showHidden bool, onSelect func(tk.Items, int)) tk.Widget {
	files, content, err := f.Read()
	if err != nil {
		return makeErrCol(err)
	}

	if files != nil {
		var filtered []NavigationFile
		for _, file := range files {
			name := file.Name()
			hidden := len(name) > 0 && name[0] == '.'
			if filter(name) && (showHidden || !hidden) {
				filtered = append(filtered, file)
			}
		}
		files = filtered
		sort.Slice(files, func(i, j int) bool {
			return files[i].Name() < files[j].Name()
		})
		return tk.NewListBox(tk.ListBoxSpec{
			Padding: 1, ExtendStyle: true, OnSelect: onSelect,
			State: tk.ListBoxState{Items: fileItems(files)},
		})
	}

	lines := strings.Split(sanitize(string(content)), "\n")
	return tk.NewTextView(tk.TextViewSpec{
		State:      tk.TextViewState{Lines: lines},
		Scrollable: true,
	})
}

func makeErrCol(err error) tk.Widget {
	return tk.Label{Content: ui.T(err.Error(), ui.FgRed)}
}

type fileItems []NavigationFile

func (it fileItems) Show(i int) ui.Text {
	return it[i].ShowName()
}

func (it fileItems) Len() int { return len(it) }

func sanitize(content string) string {
	// Remove unprintable characters, and replace tabs with 4 spaces.
	var sb strings.Builder
	for _, r := range content {
		if r == '\t' {
			sb.WriteString("    ")
		} else if r == '\n' || unicode.IsGraphic(r) {
			sb.WriteRune(r)
		}
	}
	return sb.String()
}

func (w *navigation) Select(f func(tk.ListBoxState) int) {
	if listBox, ok := w.colView.CopyState().Columns[1].(tk.ListBox); ok {
		listBox.Select(f)
	}
}

func (w *navigation) ScrollPreview(delta int) {
	if textView, ok := w.colView.CopyState().Columns[2].(tk.TextView); ok {
		textView.ScrollBy(delta)
	}
}

func (w *navigation) Ascend() {
	w.colView.Left()
}

func (w *navigation) Descend() {
	w.colView.Right()
}

func (w *navigation) MutateFiltering(f func(bool) bool) {
	w.MutateState(func(s *navigationState) { s.Filtering = f(s.Filtering) })
}

func (w *navigation) MutateShowHidden(f func(bool) bool) {
	w.MutateState(func(s *navigationState) { s.ShowHidden = f(s.ShowHidden) })
	updateState(w, w.SelectedName())
}
