// Copyright 2020 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package gocui

import (
	"github.com/gdamore/tcell/v2"
	"github.com/mattn/go-runewidth"
)

// We probably don't want this being a global variable for YOLO for now
var Screen tcell.Screen

// oldStyle is a representation of how a cell would be styled when we were using termbox
type oldStyle struct {
	fg         Attribute
	bg         Attribute
	outputMode OutputMode
}

var runeReplacements = map[rune]string{
	'┌': "+",
	'┐': "+",
	'└': "+",
	'┘': "+",
	'╭': "+",
	'╮': "+",
	'╰': "+",
	'╯': "+",
	'─': "-",
	'═': "-",
	'║': "|",
	'╔': "+",
	'╗': "+",
	'╚': "+",
	'╝': "+",

	// using a hyphen here actually looks weird.
	// We see these characters when in portrait mode
	'╶': " ",
	'╴': " ",

	'┴': "+",
	'┬': "+",
	'╷': "|",
	'├': "+",
	'│': "|",
	'▼': "v",
	'►': ">",
	'▲': "^",
	'◄': "<",
}

// tcellInit initializes tcell screen for use.
func (g *Gui) tcellInit(runeReplacements map[rune]string) error {
	runewidth.DefaultCondition.EastAsianWidth = false
	tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)

	if s, e := tcell.NewScreen(); e != nil {
		return e
	} else if e = s.Init(); e != nil {
		return e
	} else {
		registerRuneFallbacks(s, runeReplacements)

		g.screen = s
		Screen = s
		return nil
	}
}

func registerRuneFallbacks(s tcell.Screen, additional map[rune]string) {
	for before, after := range runeReplacements {
		s.RegisterRuneFallback(before, after)
	}

	for before, after := range additional {
		s.RegisterRuneFallback(before, after)
	}
}

// tcellInitSimulation initializes tcell screen for use.
func (g *Gui) tcellInitSimulation(width int, height int) error {
	s := tcell.NewSimulationScreen("")
	if e := s.Init(); e != nil {
		return e
	} else {
		g.screen = s
		Screen = s
		// setting to a larger value than the typical terminal size
		// so that during a test we're more likely to see an item to select in a view.
		s.SetSize(width, height)
		s.Sync()
		return nil
	}
}

// tcellSetCell sets the character cell at a given location to the given
// content (rune) and attributes using provided OutputMode
func tcellSetCell(x, y int, ch rune, fg, bg Attribute, outputMode OutputMode) {
	st := getTcellStyle(oldStyle{fg: fg, bg: bg, outputMode: outputMode})
	Screen.SetContent(x, y, ch, nil, st)
}

// getTcellStyle creates tcell.Style from Attributes
func getTcellStyle(input oldStyle) tcell.Style {
	st := tcell.StyleDefault

	// extract colors and attributes
	if input.fg != ColorDefault {
		st = st.Foreground(getTcellColor(input.fg, input.outputMode))
		st = setTcellFontEffectStyle(st, input.fg)
	}
	if input.bg != ColorDefault {
		st = st.Background(getTcellColor(input.bg, input.outputMode))
		st = setTcellFontEffectStyle(st, input.bg)
	}

	return st
}

// setTcellFontEffectStyle add additional attributes to tcell.Style
func setTcellFontEffectStyle(st tcell.Style, attr Attribute) tcell.Style {
	if attr&AttrBold != 0 {
		st = st.Bold(true)
	}
	if attr&AttrUnderline != 0 {
		st = st.Underline(true)
	}
	if attr&AttrReverse != 0 {
		st = st.Reverse(true)
	}
	if attr&AttrBlink != 0 {
		st = st.Blink(true)
	}
	if attr&AttrDim != 0 {
		st = st.Dim(true)
	}
	if attr&AttrItalic != 0 {
		st = st.Italic(true)
	}
	if attr&AttrStrikeThrough != 0 {
		st = st.StrikeThrough(true)
	}
	return st
}

// gocuiEventType represents the type of event.
type gocuiEventType uint8

// GocuiEvent represents events like a keys, mouse actions, or window resize.
//
//	The 'Mod', 'Key' and 'Ch' fields are valid if 'Type' is 'eventKey'.
//	The 'MouseX' and 'MouseY' fields are valid if 'Type' is 'eventMouse'.
//	The 'Width' and 'Height' fields are valid if 'Type' is 'eventResize'.
//	The 'Focused' field is valid if 'Type' is 'eventFocus'.
//	The 'Err' field is valid if 'Type' is 'eventError'.
type GocuiEvent struct {
	Type    gocuiEventType
	Mod     Modifier
	Key     Key
	Ch      rune
	Width   int
	Height  int
	Err     error
	MouseX  int
	MouseY  int
	Focused bool
	N       int
}

// Event types.
const (
	eventNone gocuiEventType = iota
	eventKey
	eventResize
	eventMouse
	eventMouseMove // only used when no button is down, otherwise it's eventMouse
	eventFocus
	eventInterrupt
	eventError
	eventRaw
)

const (
	NOT_DRAGGING int = iota
	MAYBE_DRAGGING
	DRAGGING
)

var (
	lastMouseKey tcell.ButtonMask = tcell.ButtonNone
	lastMouseMod tcell.ModMask    = tcell.ModNone
	dragState    int              = NOT_DRAGGING
	lastX        int              = 0
	lastY        int              = 0
)

// this wrapper struct has public keys so we can easily serialize/deserialize to JSON
type TcellKeyEventWrapper struct {
	Timestamp int64
	Mod       tcell.ModMask
	Key       tcell.Key
	Ch        rune
}

func NewTcellKeyEventWrapper(event *tcell.EventKey, timestamp int64) *TcellKeyEventWrapper {
	return &TcellKeyEventWrapper{
		Timestamp: timestamp,
		Mod:       event.Modifiers(),
		Key:       event.Key(),
		Ch:        event.Rune(),
	}
}

func (wrapper TcellKeyEventWrapper) toTcellEvent() tcell.Event {
	return tcell.NewEventKey(wrapper.Key, wrapper.Ch, wrapper.Mod)
}

type TcellMouseEventWrapper struct {
	Timestamp  int64
	X          int
	Y          int
	ButtonMask tcell.ButtonMask
	ModMask    tcell.ModMask
}

func NewTcellMouseEventWrapper(event *tcell.EventMouse, timestamp int64) *TcellMouseEventWrapper {
	x, y := event.Position()
	return &TcellMouseEventWrapper{
		Timestamp:  timestamp,
		X:          x,
		Y:          y,
		ButtonMask: event.Buttons(),
		ModMask:    event.Modifiers(),
	}
}

func (wrapper TcellMouseEventWrapper) toTcellEvent() tcell.Event {
	return tcell.NewEventMouse(wrapper.X, wrapper.Y, wrapper.ButtonMask, wrapper.ModMask)
}

type TcellResizeEventWrapper struct {
	Timestamp int64
	Width     int
	Height    int
}

func NewTcellResizeEventWrapper(event *tcell.EventResize, timestamp int64) *TcellResizeEventWrapper {
	w, h := event.Size()

	return &TcellResizeEventWrapper{
		Timestamp: timestamp,
		Width:     w,
		Height:    h,
	}
}

func (wrapper TcellResizeEventWrapper) toTcellEvent() tcell.Event {
	return tcell.NewEventResize(wrapper.Width, wrapper.Height)
}

// pollEvent get tcell.Event and transform it into gocuiEvent
func (g *Gui) pollEvent() GocuiEvent {
	var tev tcell.Event
	if g.playRecording {
		select {
		case ev := <-g.ReplayedEvents.Keys:
			tev = (ev).toTcellEvent()
		case ev := <-g.ReplayedEvents.Resizes:
			tev = (ev).toTcellEvent()
		case ev := <-g.ReplayedEvents.MouseEvents:
			tev = (ev).toTcellEvent()
		}
	} else {
		tev = Screen.PollEvent()
	}

	switch tev := tev.(type) {
	case *tcell.EventInterrupt:
		return GocuiEvent{Type: eventInterrupt}
	case *tcell.EventResize:
		w, h := tev.Size()
		return GocuiEvent{Type: eventResize, Width: w, Height: h}
	case *tcell.EventKey:
		k := tev.Key()
		ch := rune(0)
		if k == tcell.KeyRune {
			k = 0 // if rune remove key (so it can match rune instead of key)
			ch = tev.Rune()
			if ch == ' ' {
				// special handling for spacebar
				k = 32 // tcell keys ends at 31 or starts at 256
				ch = rune(0)
			}
		}
		mod := tev.Modifiers()
		// remove control modifier and setup special handling of ctrl+spacebar, etc.
		if mod == tcell.ModCtrl && k == 32 {
			mod = 0
			ch = rune(0)
			k = tcell.KeyCtrlSpace
		} else if mod == tcell.ModShift && k == tcell.KeyUp {
			mod = 0
			ch = rune(0)
			k = tcell.KeyF62
		} else if mod == tcell.ModShift && k == tcell.KeyDown {
			mod = 0
			ch = rune(0)
			k = tcell.KeyF63
		} else if mod == tcell.ModCtrl || mod == tcell.ModShift {
			// remove Ctrl or Shift if specified
			// - shift - will be translated to the final code of rune
			// - ctrl  - is translated in the key
			mod = 0
		} else if mod == tcell.ModAlt && k == tcell.KeyEnter {
			// for the sake of convenience I'm having a KeyAltEnter key. I will likely
			// regret this laziness in the future. We're arbitrarily mapping that to tcell's
			// KeyF64.
			mod = 0
			k = tcell.KeyF64
		}

		return GocuiEvent{
			Type: eventKey,
			Key:  Key(k),
			Ch:   ch,
			Mod:  Modifier(mod),
		}
	case *tcell.EventMouse:
		x, y := tev.Position()
		button := tev.Buttons()
		mouseKey := MouseRelease
		mouseMod := ModNone
		// process mouse wheel
		if button&tcell.WheelUp != 0 {
			mouseKey = MouseWheelUp
		}
		if button&tcell.WheelDown != 0 {
			mouseKey = MouseWheelDown
		}
		if button&tcell.WheelLeft != 0 {
			mouseKey = MouseWheelLeft
		}
		if button&tcell.WheelRight != 0 {
			mouseKey = MouseWheelRight
		}

		wheeling := mouseKey == MouseWheelUp || mouseKey == MouseWheelDown || mouseKey == MouseWheelLeft || mouseKey == MouseWheelRight

		// process button events (not wheel events)
		button &= tcell.ButtonMask(0xff)
		if button != tcell.ButtonNone && lastMouseKey == tcell.ButtonNone {
			lastMouseKey = button
			lastMouseMod = tev.Modifiers()
			switch button {
			case tcell.ButtonPrimary:
				mouseKey = MouseLeft
				dragState = MAYBE_DRAGGING
				lastX = x
				lastY = y
			case tcell.ButtonSecondary:
				mouseKey = MouseRight
			case tcell.ButtonMiddle:
				mouseKey = MouseMiddle
			default:
			}
		}

		switch tev.Buttons() {
		case tcell.ButtonNone:
			if lastMouseKey != tcell.ButtonNone {
				switch lastMouseKey {
				case tcell.ButtonPrimary:
					dragState = NOT_DRAGGING
				case tcell.ButtonSecondary:
				case tcell.ButtonMiddle:
				default:
				}
				mouseMod = Modifier(lastMouseMod)
				lastMouseMod = tcell.ModNone
				lastMouseKey = tcell.ButtonNone
			}
		default:
		}

		if !wheeling {
			switch dragState {
			case NOT_DRAGGING:
				return GocuiEvent{
					Type:   eventMouseMove,
					MouseX: x,
					MouseY: y,
				}
			// if we haven't released the left mouse button and we've moved the cursor then we're dragging
			case MAYBE_DRAGGING:
				if x != lastX || y != lastY {
					dragState = DRAGGING
				}
			case DRAGGING:
				mouseMod = ModMotion
				mouseKey = MouseLeft
			}
		}

		return GocuiEvent{
			Type:   eventMouse,
			MouseX: x,
			MouseY: y,
			Key:    mouseKey,
			Ch:     0,
			Mod:    mouseMod,
		}
	case *tcell.EventFocus:
		return GocuiEvent{
			Type:    eventFocus,
			Focused: tev.Focused,
		}
	default:
		return GocuiEvent{Type: eventNone}
	}
}
