package cview

import (
	"bytes"
	"fmt"
	"sync"

	"github.com/gdamore/tcell/v2"
)

// TabbedPanels is a tabbed container for other primitives. The tab switcher
// may be positioned vertically or horizontally, before or after the content.
type TabbedPanels struct {
	*Flex
	Switcher *TextView
	panels   *Panels

	tabLabels  map[string]string
	currentTab string

	tabTextColor              tcell.Color
	tabTextColorFocused       tcell.Color
	tabBackgroundColor        tcell.Color
	tabBackgroundColorFocused tcell.Color

	dividerStart string
	dividerMid   string
	dividerEnd   string

	switcherVertical     bool
	switcherAfterContent bool

	width, lastWidth int

	setFocus func(Primitive)

	sync.RWMutex
}

// NewTabbedPanels returns a new TabbedPanels object.
func NewTabbedPanels() *TabbedPanels {
	t := &TabbedPanels{
		Flex:                      NewFlex(),
		Switcher:                  NewTextView(),
		panels:                    NewPanels(),
		tabTextColor:              Styles.PrimaryTextColor,
		tabTextColorFocused:       Styles.InverseTextColor,
		tabBackgroundColor:        ColorUnset,
		tabBackgroundColorFocused: Styles.PrimaryTextColor,
		dividerMid:                string(BoxDrawingsDoubleVertical),
		dividerEnd:                string(BoxDrawingsLightVertical),
		tabLabels:                 make(map[string]string),
	}

	s := t.Switcher
	s.SetDynamicColors(true)
	s.SetRegions(true)
	s.SetWrap(true)
	s.SetWordWrap(true)
	s.SetHighlightedFunc(func(added, removed, remaining []string) {
		if len(added) == 0 {
			return
		}

		t.SetCurrentTab(added[0])
		if t.setFocus != nil {
			t.setFocus(t.panels)
		}
		s.Highlight()
	})

	t.rebuild()

	return t
}

// AddTab adds a new tab. Tab names should consist only of letters, numbers
// and spaces.
func (t *TabbedPanels) AddTab(name, label string, item Primitive) {
	t.Lock()
	t.tabLabels[name] = label
	t.Unlock()

	t.panels.AddPanel(name, item, true, false)

	t.updateAll()
}

// RemoveTab removes a tab.
func (t *TabbedPanels) RemoveTab(name string) {
	t.panels.RemovePanel(name)

	t.updateAll()
}

// HasTab returns true if a tab with the given name exists in this object.
func (t *TabbedPanels) HasTab(name string) bool {
	t.RLock()
	defer t.RUnlock()

	for _, panel := range t.panels.panels {
		if panel.Name == name {
			return true
		}
	}
	return false
}

// SetCurrentTab sets the currently visible tab.
func (t *TabbedPanels) SetCurrentTab(name string) {
	t.Lock()

	if t.currentTab == name {
		t.Unlock()
		return
	}

	t.currentTab = name

	t.updateAll()

	t.Unlock()

	t.Switcher.Highlight(t.currentTab)
}

// GetCurrentTab returns the currently visible tab.
func (t *TabbedPanels) GetCurrentTab() string {
	t.RLock()
	defer t.RUnlock()
	return t.currentTab
}

// SetTabLabel sets the label of a tab.
func (t *TabbedPanels) SetTabLabel(name, label string) {
	t.Lock()
	defer t.Unlock()

	if t.tabLabels[name] == label {
		return
	}

	t.tabLabels[name] = label
	t.updateTabLabels()
}

// SetTabTextColor sets the color of the tab text.
func (t *TabbedPanels) SetTabTextColor(color tcell.Color) {
	t.Lock()
	defer t.Unlock()
	t.tabTextColor = color
}

// SetTabTextColorFocused sets the color of the tab text when the tab is in focus.
func (t *TabbedPanels) SetTabTextColorFocused(color tcell.Color) {
	t.Lock()
	defer t.Unlock()
	t.tabTextColorFocused = color
}

// SetTabBackgroundColor sets the background color of the tab.
func (t *TabbedPanels) SetTabBackgroundColor(color tcell.Color) {
	t.Lock()
	defer t.Unlock()
	t.tabBackgroundColor = color
}

// SetTabBackgroundColorFocused sets the background color of the tab when the
// tab is in focus.
func (t *TabbedPanels) SetTabBackgroundColorFocused(color tcell.Color) {
	t.Lock()
	defer t.Unlock()
	t.tabBackgroundColorFocused = color
}

// SetTabSwitcherDivider sets the tab switcher divider text. Color tags are supported.
func (t *TabbedPanels) SetTabSwitcherDivider(start, mid, end string) {
	t.Lock()
	defer t.Unlock()
	t.dividerStart, t.dividerMid, t.dividerEnd = start, mid, end
}

// SetTabSwitcherVertical sets the orientation of the tab switcher.
func (t *TabbedPanels) SetTabSwitcherVertical(vertical bool) {
	t.Lock()
	defer t.Unlock()

	if t.switcherVertical == vertical {
		return
	}

	t.switcherVertical = vertical
	t.rebuild()
}

// SetTabSwitcherAfterContent sets whether the tab switcher is positioned after content.
func (t *TabbedPanels) SetTabSwitcherAfterContent(after bool) {
	t.Lock()
	defer t.Unlock()

	if t.switcherAfterContent == after {
		return
	}

	t.switcherAfterContent = after
	t.rebuild()
}

func (t *TabbedPanels) rebuild() {
	f := t.Flex
	if t.switcherVertical {
		f.SetDirection(FlexColumn)
	} else {
		f.SetDirection(FlexRow)
	}
	f.RemoveItem(t.panels)
	f.RemoveItem(t.Switcher)
	if t.switcherAfterContent {
		f.AddItem(t.panels, 0, 1, true)
		f.AddItem(t.Switcher, 1, 1, false)
	} else {
		f.AddItem(t.Switcher, 1, 1, false)
		f.AddItem(t.panels, 0, 1, true)
	}

	t.updateTabLabels()
}

func (t *TabbedPanels) updateTabLabels() {
	if len(t.panels.panels) == 0 {
		t.Switcher.SetText("")
		t.Flex.ResizeItem(t.Switcher, 0, 1)
		return
	}

	maxWidth := 0
	for _, panel := range t.panels.panels {
		label := t.tabLabels[panel.Name]
		if len(label) > maxWidth {
			maxWidth = len(label)
		}
	}

	var b bytes.Buffer
	if !t.switcherVertical {
		b.WriteString(t.dividerStart)
	}
	l := len(t.panels.panels)
	spacer := []byte(" ")
	for i, panel := range t.panels.panels {
		if i > 0 && t.switcherVertical {
			b.WriteRune('\n')
		}

		if t.switcherVertical && t.switcherAfterContent {
			b.WriteString(t.dividerMid)
			b.WriteRune(' ')
		}

		textColor := t.tabTextColor
		backgroundColor := t.tabBackgroundColor
		if panel.Name == t.currentTab {
			textColor = t.tabTextColorFocused
			backgroundColor = t.tabBackgroundColorFocused
		}

		label := t.tabLabels[panel.Name]
		if !t.switcherVertical {
			label = " " + label
		}

		if t.switcherVertical {
			spacer = bytes.Repeat([]byte(" "), maxWidth-len(label)+1)
		}

		b.WriteString(fmt.Sprintf(`["%s"][%s:%s]%s%s[-:-][""]`, panel.Name, ColorHex(textColor), ColorHex(backgroundColor), label, spacer))

		if i == l-1 && !t.switcherVertical {
			b.WriteString(t.dividerEnd)
		} else if !t.switcherAfterContent {
			b.WriteString(t.dividerMid)
		}
	}
	t.Switcher.SetText(b.String())

	var reqLines int
	if t.switcherVertical {
		reqLines = maxWidth + 2
	} else {
		reqLines = len(WordWrap(t.Switcher.GetText(true), t.width))
		if reqLines < 1 {
			reqLines = 1
		}
	}
	t.Flex.ResizeItem(t.Switcher, reqLines, 1)
}

func (t *TabbedPanels) updateVisibleTabs() {
	allPanels := t.panels.panels

	var newTab string

	var foundCurrent bool
	for _, panel := range allPanels {
		if panel.Name == t.currentTab {
			newTab = panel.Name
			foundCurrent = true
			break
		}
	}
	if !foundCurrent {
		for _, panel := range allPanels {
			if panel.Name != "" {
				newTab = panel.Name
				break
			}
		}
	}

	if t.currentTab != newTab {
		t.SetCurrentTab(newTab)
		return
	}

	for _, panel := range allPanels {
		if panel.Name == t.currentTab {
			t.panels.ShowPanel(panel.Name)
		} else {
			t.panels.HidePanel(panel.Name)
		}
	}
}

func (t *TabbedPanels) updateAll() {
	t.updateTabLabels()
	t.updateVisibleTabs()
}

// Draw draws this primitive onto the screen.
func (t *TabbedPanels) Draw(screen tcell.Screen) {
	if !t.GetVisible() {
		return
	}

	t.Box.Draw(screen)

	_, _, t.width, _ = t.GetInnerRect()
	if t.width != t.lastWidth {
		t.updateTabLabels()
	}
	t.lastWidth = t.width

	t.Flex.Draw(screen)
}

// InputHandler returns the handler for this primitive.
func (t *TabbedPanels) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
	return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
		if t.setFocus == nil {
			t.setFocus = setFocus
		}
		t.Flex.InputHandler()(event, setFocus)
	})
}

// MouseHandler returns the mouse handler for this primitive.
func (t *TabbedPanels) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
	return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
		if t.setFocus == nil {
			t.setFocus = setFocus
		}

		x, y := event.Position()
		if !t.InRect(x, y) {
			return false, nil
		}

		if t.Switcher.InRect(x, y) {
			if t.setFocus != nil {
				defer t.setFocus(t.panels)
			}
			defer t.Switcher.MouseHandler()(action, event, setFocus)
			return true, nil
		}

		return t.Flex.MouseHandler()(action, event, setFocus)
	})
}
