// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package components

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
	"gopkg.in/yaml.v3"
)

var hline string

func init() {
	for i := 0; i < 2000; i++ {
		hline += string(tcell.RuneHLine)
	}
}

// Separator hline with a description below.
type Separator struct {
	*tview.TextView
}

// GetHeight implements Multiline interface.
func (s *Separator) GetHeight() int {
	return 2
}

// NewSeparator creates new form special form item which can be used as a sections Separator.
func NewSeparator(description string) *Item {
	return NewItem("", "", func(item *Item) tview.Primitive {
		s := &Separator{
			tview.NewTextView(),
		}
		s.SetText(hline + "\n" + description)
		s.SetWrap(false)

		return s
	})
}

// Item represents a single form item.
type Item struct {
	Name        string
	description string
	dest        interface{}
	options     []interface{}
}

// TableHeaders represents table headers list for item options which are using table representation.
type TableHeaders []interface{}

// NewTableHeaders creates TableHeaders object.
func NewTableHeaders(headers ...interface{}) TableHeaders {
	return TableHeaders(headers)
}

// NewItem creates new form item.
func NewItem(name, description string, dest interface{}, options ...interface{}) *Item {
	return &Item{
		Name:        name,
		dest:        dest,
		description: description,
		options:     options,
	}
}

func (item *Item) assign(value string) error {
	// rely on yaml parser to decode value into the right type
	return yaml.Unmarshal([]byte(value), item.dest)
}

// createFormItems dynamically creates tview.FormItem list based on the wrapped type.
//nolint:gocyclo
func (item *Item) createFormItems() ([]tview.Primitive, error) {
	res := []tview.Primitive{}

	v := reflect.ValueOf(item.dest)
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}

	var formItem tview.Primitive

	label := fmt.Sprintf("[::b]%s[::-]:", item.Name)
	addDescription := true

	//nolint:exhaustive
	switch v.Kind() {
	case reflect.Func:
		if f, ok := item.dest.(func(*Item) tview.Primitive); ok {
			formItem = f(item)
		}
	case reflect.Bool:
		// use checkbox for boolean fields
		checkbox := tview.NewCheckbox()
		checkbox.SetChangedFunc(func(checked bool) {
			v.Set(reflect.ValueOf(checked))
		})
		checkbox.SetChecked(v.Bool())
		checkbox.SetLabel(label)
		formItem = checkbox
	default:
		if len(item.options) > 0 {
			tableHeaders, ok := item.options[0].(TableHeaders)
			if ok {
				table := NewTable()
				table.SetHeader(tableHeaders...)

				addDescription = false

				data := item.options[1:]
				numColumns := len(tableHeaders)

				if len(data)%numColumns != 0 {
					return nil, fmt.Errorf("incorrect amount of data provided for the table")
				}

				selected := -1

				for i := 0; i < len(data); i += numColumns {
					table.AddRow(data[i : i+numColumns]...)

					if v.Interface() == data[i] {
						selected = i / numColumns
					}
				}

				if selected != -1 {
					table.SelectRow(selected)
				}

				formItem = table
				table.SetRowSelectedFunc(func(row int) {
					v.Set(reflect.ValueOf(table.GetValue(row, 0))) // always pick the first column
				})
			} else {
				dropdown := tview.NewDropDown()

				if len(item.options)%2 != 0 {
					return nil, fmt.Errorf("wrong amount of arguments for options: should be even amount of key, value pairs")
				}

				for i := 0; i < len(item.options); i += 2 {
					if optionName, ok := item.options[i].(string); ok {
						selected := -1

						func(index int) {
							dropdown.AddOption(optionName, func() {
								v.Set(reflect.ValueOf(item.options[index]))
							})

							if v.Interface() == item.options[index] {
								selected = i / 2
							}
						}(i + 1)

						if selected != -1 {
							dropdown.SetCurrentOption(selected)
						}
					} else {
						return nil, fmt.Errorf("expected string option name, got %s", item.options[i])
					}
				}

				dropdown.SetLabel(label)

				formItem = dropdown
			}
		} else {
			input := tview.NewInputField()
			formItem = input
			input.SetLabel(label)
			text, err := yaml.Marshal(item.dest)
			if err != nil {
				return nil, err
			}
			input.SetText(string(text))
			input.SetChangedFunc(func(text string) {
				if err := item.assign(text); err != nil {
					// TODO: highlight red
					return
				}
			})
		}
	}

	res = append(res, formItem)

	if item.description != "" && addDescription {
		parts := strings.Split(item.description, "\n")
		for _, part := range parts {
			desc := NewFormLabel(part)
			res = append(res, desc)
		}
	}

	res = append(res, NewFormLabel(""))

	return res, nil
}

// NewForm creates a new form.
func NewForm(app *tview.Application) *Form {
	f := &Form{
		Flex:      tview.NewFlex().SetDirection(tview.FlexRow),
		formItems: []tview.FormItem{},
		form:      tview.NewFlex().SetDirection(tview.FlexRow),
		buttons:   tview.NewFlex(),
		group:     NewGroup(app),
	}

	f.Flex.AddItem(f.form, 0, 1, false)
	f.Flex.AddItem(f.buttons, 0, 0, false)

	f.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
		//nolint:exhaustive
		switch e.Key() {
		case tcell.KeyTAB:
			f.group.NextFocus()
		case tcell.KeyBacktab:
			f.group.PrevFocus()
		}

		return e
	})

	return f
}

// Form is a more flexible form component for tview lib.
type Form struct {
	*tview.Flex
	form          *tview.Flex
	buttons       *tview.Flex
	formItems     []tview.FormItem
	maxLabelLen   int
	hasMenuButton bool
	group         *Group
}

// AddFormItem adds a new item to the form.
func (f *Form) AddFormItem(item tview.Primitive) {
	if formItem, ok := item.(tview.FormItem); ok {
		f.formItems = append(f.formItems, formItem)
		labelLen := tview.TaggedStringWidth(formItem.GetLabel()) + 1

		if labelLen > f.maxLabelLen {
			for _, item := range f.formItems[:len(f.formItems)-1] {
				item.SetFormAttributes(
					labelLen,
					tview.Styles.PrimaryTextColor,
					f.GetBackgroundColor(),
					tview.Styles.PrimaryTextColor,
					tview.Styles.ContrastBackgroundColor,
				)
			}

			f.maxLabelLen = labelLen
		}

		formItem.SetFormAttributes(
			f.maxLabelLen,
			tview.Styles.PrimaryTextColor,
			f.GetBackgroundColor(),
			tview.Styles.PrimaryTextColor,
			tview.Styles.ContrastBackgroundColor,
		)
	} else if box, ok := item.(Box); ok {
		box.SetBackgroundColor(f.GetBackgroundColor())
	}

	height := 1
	multiline, ok := item.(Multiline)

	if ok {
		height = multiline.GetHeight()
	}

	f.form.AddItem(item, height, 1, false)

	switch item.(type) {
	case *FormLabel:
	case *Separator:
	default:
		f.group.AddElement(item)
	}
}

// Focus overrides default focus behavior.
func (f *Form) Focus(delegate func(tview.Primitive)) {
	f.group.FocusFirst()
}

// AddMenuButton adds a button to the menu at the bottom of the form.
func (f *Form) AddMenuButton(label string, alignRight bool) *tview.Button {
	b := tview.NewButton(label)

	if f.hasMenuButton || alignRight {
		f.buttons.AddItem(
			tview.NewBox().SetBackgroundColor(f.GetBackgroundColor()),
			0,
			1,
			false,
		)
	}

	f.ResizeItem(f.buttons, 3, 0)

	f.buttons.AddItem(b, tview.TaggedStringWidth(label)+4, 0, false)
	f.hasMenuButton = true
	f.group.AddElement(b)

	return b
}

// AddFormItems constructs form from data represented as a list of Item objects.
func (f *Form) AddFormItems(items []*Item) error {
	for _, item := range items {
		formItems, e := item.createFormItems()
		if e != nil {
			return e
		}

		for _, formItem := range formItems {
			f.AddFormItem(formItem)
		}
	}

	return nil
}

// Multiline interface represents elements that can occupy more than one line.
type Multiline interface {
	GetHeight() int
}

// Box interface that has just SetBackgroundColor.
type Box interface {
	SetBackgroundColor(tcell.Color) *tview.Box
}
