// Copyright 2024 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build js && wasm
// +build js,wasm

package tcell

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"syscall/js"
	"unicode/utf8"

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

func NewTerminfoScreen() (Screen, error) {
	t := &wScreen{}
	t.fallback = make(map[rune]string)

	return &baseScreen{screenImpl: t}, nil
}

type wScreen struct {
	w, h  int
	style Style
	cells CellBuffer

	running      bool
	clear        bool
	flagsPresent bool
	pasteEnabled bool
	mouseFlags   MouseFlags

	cursorStyle CursorStyle

	quit     chan struct{}
	evch     chan Event
	fallback map[rune]string
	finiOnce sync.Once

	sync.Mutex
}

func (t *wScreen) Init() error {
	t.w, t.h = 80, 24 // default for html as of now
	t.evch = make(chan Event, 10)
	t.quit = make(chan struct{})

	t.Lock()
	t.running = true
	t.style = StyleDefault
	t.cells.Resize(t.w, t.h)
	t.Unlock()

	js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent))
	js.Global().Set("onMouseClick", js.FuncOf(t.unset))
	js.Global().Set("onMouseMove", js.FuncOf(t.unset))
	js.Global().Set("onFocus", js.FuncOf(t.unset))

	return nil
}

func (t *wScreen) Fini() {
	t.finiOnce.Do(func() {
		close(t.quit)
	})
}

func (t *wScreen) SetStyle(style Style) {
	t.Lock()
	t.style = style
	t.Unlock()
}

// paletteColor gives a more natural palette color actually matching
// typical XTerm.  We might in the future want to permit styling these
// via CSS.

var palette = map[Color]int32{
	ColorBlack:   0x000000,
	ColorMaroon:  0xcd0000,
	ColorGreen:   0x00cd00,
	ColorOlive:   0xcdcd00,
	ColorNavy:    0x0000ee,
	ColorPurple:  0xcd00cd,
	ColorTeal:    0x00cdcd,
	ColorSilver:  0xe5e5e5,
	ColorGray:    0x7f7f7f,
	ColorRed:     0xff0000,
	ColorLime:    0x00ff00,
	ColorYellow:  0xffff00,
	ColorBlue:    0x5c5cff,
	ColorFuchsia: 0xff00ff,
	ColorAqua:    0x00ffff,
	ColorWhite:   0xffffff,
}

func paletteColor(c Color) int32 {
	if c.IsRGB() {
		return int32(c & 0xffffff)
	}
	if c >= ColorBlack && c <= ColorWhite {
		return palette[c]
	}
	return c.Hex()
}

func (t *wScreen) drawCell(x, y int) int {
	mainc, combc, style, width := t.cells.GetContent(x, y)

	if !t.cells.Dirty(x, y) {
		return width
	}

	if style == StyleDefault {
		style = t.style
	}

	fg, bg := paletteColor(style.fg), paletteColor(style.bg)
	if fg == -1 {
		fg = 0xe5e5e5
	}
	if bg == -1 {
		bg = 0x000000
	}
	us, uc := style.ulStyle, paletteColor(style.ulColor)
	if uc == -1 {
		uc = 0x000000
	}

	s := ""
	if len(combc) > 0 {
		b := make([]rune, 0, 1 + len(combc))
		b = append(b, mainc)
		b = append(b, combc...)
		s = string(b)
	} else {
		s = string(mainc)
	}

	t.cells.SetDirty(x, y, false)
	js.Global().Call("drawCell", x, y, s, fg, bg, int(style.attrs), int(us), int(uc))

	return width
}

func (t *wScreen) ShowCursor(x, y int) {
	t.Lock()
	js.Global().Call("showCursor", x, y)
	t.Unlock()
}

func (t *wScreen) SetCursor(cs CursorStyle, cc Color) {
	if !cc.Valid() {
		cc = ColorLightGray
	}
	t.Lock()
	js.Global().Call("setCursorStyle", curStyleClasses[cs], fmt.Sprintf("#%06x", cc.Hex()))
	t.Unlock()
}

func (t *wScreen) HideCursor() {
	t.ShowCursor(-1, -1)
}

func (t *wScreen) Show() {
	t.Lock()
	t.resize()
	t.draw()
	t.Unlock()
}

func (t *wScreen) clearScreen() {
	js.Global().Call("clearScreen", t.style.fg.Hex(), t.style.bg.Hex())
	t.clear = false
}

func (t *wScreen) draw() {
	if t.clear {
		t.clearScreen()
	}

	for y := 0; y < t.h; y++ {
		for x := 0; x < t.w; x++ {
			width := t.drawCell(x, y)
			x += width - 1
		}
	}

	js.Global().Call("show")
}

func (t *wScreen) EnableMouse(flags ...MouseFlags) {
	var f MouseFlags
	flagsPresent := false
	for _, flag := range flags {
		f |= flag
		flagsPresent = true
	}
	if !flagsPresent {
		f = MouseMotionEvents | MouseDragEvents | MouseButtonEvents
	}

	t.Lock()
	t.mouseFlags = f
	t.enableMouse(f)
	t.Unlock()
}

func (t *wScreen) enableMouse(f MouseFlags) {
	if f&MouseButtonEvents != 0 {
		js.Global().Set("onMouseClick", js.FuncOf(t.onMouseEvent))
	} else {
		js.Global().Set("onMouseClick", js.FuncOf(t.unset))
	}

	if f&MouseDragEvents != 0 || f&MouseMotionEvents != 0 {
		js.Global().Set("onMouseMove", js.FuncOf(t.onMouseEvent))
	} else {
		js.Global().Set("onMouseMove", js.FuncOf(t.unset))
	}
}

func (t *wScreen) DisableMouse() {
	t.Lock()
	t.mouseFlags = 0
	t.enableMouse(0)
	t.Unlock()
}

func (t *wScreen) EnablePaste() {
	t.Lock()
	t.pasteEnabled = true
	t.enablePasting(true)
	t.Unlock()
}

func (t *wScreen) DisablePaste() {
	t.Lock()
	t.pasteEnabled = false
	t.enablePasting(false)
	t.Unlock()
}

func (t *wScreen) enablePasting(on bool) {
	if on {
		js.Global().Set("onPaste", js.FuncOf(t.onPaste))
	} else {
		js.Global().Set("onPaste", js.FuncOf(t.unset))
	}
}

func (t *wScreen) EnableFocus() {
	t.Lock()
	js.Global().Set("onFocus", js.FuncOf(t.onFocus))
	t.Unlock()
}

func (t *wScreen) DisableFocus() {
	t.Lock()
	js.Global().Set("onFocus", js.FuncOf(t.unset))
	t.Unlock()
}

func (t *wScreen) Size() (int, int) {
	t.Lock()
	w, h := t.w, t.h
	t.Unlock()
	return w, h
}

// resize does nothing, as asking the web window to resize
// without a specified width or height will cause no change.
func (t *wScreen) resize() {}

func (t *wScreen) Colors() int {
	return 16777216 // 256 ^ 3
}

func (t *wScreen) clip(x, y int) (int, int) {
	w, h := t.cells.Size()
	if x < 0 {
		x = 0
	}
	if y < 0 {
		y = 0
	}
	if x > w-1 {
		x = w - 1
	}
	if y > h-1 {
		y = h - 1
	}
	return x, y
}

func (t *wScreen) postEvent(ev Event) {
	select {
	case t.evch <- ev:
	case <-t.quit:
	}
}

func (t *wScreen) onMouseEvent(this js.Value, args []js.Value) interface{} {
	mod := ModNone
	button := ButtonNone

	switch args[2].Int() {
	case 0:
		if t.mouseFlags&MouseMotionEvents == 0 {
			// don't want this event! is a mouse motion event, but user has asked not.
			return nil
		}
		button = ButtonNone
	case 1:
		button = Button1
	case 2:
		button = Button3 // Note we prefer to treat right as button 2
	case 3:
		button = Button2 // And the middle button as button 3
	}

	if args[3].Bool() { // mod shift
		mod |= ModShift
	}

	if args[4].Bool() { // mod alt
		mod |= ModAlt
	}

	if args[5].Bool() { // mod ctrl
		mod |= ModCtrl
	}

	t.postEvent(NewEventMouse(args[0].Int(), args[1].Int(), button, mod))
	return nil
}

func (t *wScreen) onKeyEvent(this js.Value, args []js.Value) interface{} {
	key := args[0].String()

	// don't accept any modifier keys as their own
	if key == "Control" || key == "Alt" || key == "Meta" || key == "Shift" {
		return nil
	}

	mod := ModNone
	if args[1].Bool() { // mod shift
		mod |= ModShift
	}

	if args[2].Bool() { // mod alt
		mod |= ModAlt
	}

	if args[3].Bool() { // mod ctrl
		mod |= ModCtrl
	}

	if args[4].Bool() { // mod meta
		mod |= ModMeta
	}

	// check for special case of Ctrl + key
	if mod == ModCtrl {
		if k, ok := WebKeyNames["Ctrl-"+strings.ToLower(key)]; ok {
			t.postEvent(NewEventKey(k, 0, mod))
			return nil
		}
	}

	// next try function keys
	if k, ok := WebKeyNames[key]; ok {
		t.postEvent(NewEventKey(k, 0, mod))
		return nil
	}

	// finally try normal, printable chars
	r, _ := utf8.DecodeRuneInString(key)
	t.postEvent(NewEventKey(KeyRune, r, mod))
	return nil
}

func (t *wScreen) onPaste(this js.Value, args []js.Value) interface{} {
	t.postEvent(NewEventPaste(args[0].Bool()))
	return nil
}

func (t *wScreen) onFocus(this js.Value, args []js.Value) interface{} {
	t.postEvent(NewEventFocus(args[0].Bool()))
	return nil
}

// unset is a dummy function for js when we want nothing to
// happen when javascript calls a function (for example, when
// mouse input is disabled, when onMouseEvent() is called from
// js, it redirects here and does nothing).
func (t *wScreen) unset(this js.Value, args []js.Value) interface{} {
	return nil
}

func (t *wScreen) Sync() {
	t.Lock()
	t.resize()
	t.clear = true
	t.cells.Invalidate()
	t.draw()
	t.Unlock()
}

func (t *wScreen) CharacterSet() string {
	return "UTF-8"
}

func (t *wScreen) RegisterRuneFallback(orig rune, fallback string) {
	t.Lock()
	t.fallback[orig] = fallback
	t.Unlock()
}

func (t *wScreen) UnregisterRuneFallback(orig rune) {
	t.Lock()
	delete(t.fallback, orig)
	t.Unlock()
}

func (t *wScreen) CanDisplay(r rune, checkFallbacks bool) bool {
	if utf8.ValidRune(r) {
		return true
	}
	if !checkFallbacks {
		return false
	}
	if _, ok := t.fallback[r]; ok {
		return true
	}
	return false
}

func (t *wScreen) HasMouse() bool {
	return true
}

func (t *wScreen) HasKey(k Key) bool {
	return true
}

func (t *wScreen) SetSize(w, h int) {
	if w == t.w && h == t.h {
		return
	}

	t.cells.Invalidate()
	t.cells.Resize(w, h)
	js.Global().Call("resize", w, h)
	t.w, t.h = w, h
	t.postEvent(NewEventResize(w, h))
}

func (t *wScreen) Resize(int, int, int, int) {}

// Suspend simply pauses all input and output, and clears the screen.
// There isn't a "default terminal" to go back to.
func (t *wScreen) Suspend() error {
	t.Lock()
	if !t.running {
		t.Unlock()
		return nil
	}
	t.running = false
	t.clearScreen()
	t.enableMouse(0)
	t.enablePasting(false)
	js.Global().Set("onKeyEvent", js.FuncOf(t.unset)) // stop keypresses
	return nil
}

func (t *wScreen) Resume() error {
	t.Lock()

	if t.running {
		return errors.New("already engaged")
	}
	t.running = true

	t.enableMouse(t.mouseFlags)
	t.enablePasting(t.pasteEnabled)

	js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent))

	t.Unlock()
	return nil
}

func (t *wScreen) Beep() error {
	js.Global().Call("beep")
	return nil
}

func (t *wScreen) Tty() (Tty, bool) {
	return nil, false
}

func (t *wScreen) GetCells() *CellBuffer {
	return &t.cells
}

func (t *wScreen) EventQ() chan Event {
	return t.evch
}

func (t *wScreen) StopQ() <-chan struct{} {
	return t.quit
}

func (t *wScreen) SetTitle(title string) {
	js.Global().Call("setTitle", title)
}

// WebKeyNames maps string names reported from HTML
// (KeyboardEvent.key) to tcell accepted keys.
var WebKeyNames = map[string]Key{
	"Enter":      KeyEnter,
	"Backspace":  KeyBackspace,
	"Tab":        KeyTab,
	"Backtab":    KeyBacktab,
	"Escape":     KeyEsc,
	"Backspace2": KeyBackspace2,
	"Delete":     KeyDelete,
	"Insert":     KeyInsert,
	"ArrowUp":    KeyUp,
	"ArrowDown":  KeyDown,
	"ArrowLeft":  KeyLeft,
	"ArrowRight": KeyRight,
	"Home":       KeyHome,
	"End":        KeyEnd,
	"UpLeft":     KeyUpLeft,    // not supported by HTML
	"UpRight":    KeyUpRight,   // not supported by HTML
	"DownLeft":   KeyDownLeft,  // not supported by HTML
	"DownRight":  KeyDownRight, // not supported by HTML
	"Center":     KeyCenter,
	"PgDn":       KeyPgDn,
	"PgUp":       KeyPgUp,
	"Clear":      KeyClear,
	"Exit":       KeyExit,
	"Cancel":     KeyCancel,
	"Pause":      KeyPause,
	"Print":      KeyPrint,
	"F1":         KeyF1,
	"F2":         KeyF2,
	"F3":         KeyF3,
	"F4":         KeyF4,
	"F5":         KeyF5,
	"F6":         KeyF6,
	"F7":         KeyF7,
	"F8":         KeyF8,
	"F9":         KeyF9,
	"F10":        KeyF10,
	"F11":        KeyF11,
	"F12":        KeyF12,
	"F13":        KeyF13,
	"F14":        KeyF14,
	"F15":        KeyF15,
	"F16":        KeyF16,
	"F17":        KeyF17,
	"F18":        KeyF18,
	"F19":        KeyF19,
	"F20":        KeyF20,
	"F21":        KeyF21,
	"F22":        KeyF22,
	"F23":        KeyF23,
	"F24":        KeyF24,
	"F25":        KeyF25,
	"F26":        KeyF26,
	"F27":        KeyF27,
	"F28":        KeyF28,
	"F29":        KeyF29,
	"F30":        KeyF30,
	"F31":        KeyF31,
	"F32":        KeyF32,
	"F33":        KeyF33,
	"F34":        KeyF34,
	"F35":        KeyF35,
	"F36":        KeyF36,
	"F37":        KeyF37,
	"F38":        KeyF38,
	"F39":        KeyF39,
	"F40":        KeyF40,
	"F41":        KeyF41,
	"F42":        KeyF42,
	"F43":        KeyF43,
	"F44":        KeyF44,
	"F45":        KeyF45,
	"F46":        KeyF46,
	"F47":        KeyF47,
	"F48":        KeyF48,
	"F49":        KeyF49,
	"F50":        KeyF50,
	"F51":        KeyF51,
	"F52":        KeyF52,
	"F53":        KeyF53,
	"F54":        KeyF54,
	"F55":        KeyF55,
	"F56":        KeyF56,
	"F57":        KeyF57,
	"F58":        KeyF58,
	"F59":        KeyF59,
	"F60":        KeyF60,
	"F61":        KeyF61,
	"F62":        KeyF62,
	"F63":        KeyF63,
	"F64":        KeyF64,
	"Ctrl-a":     KeyCtrlA,          // not reported by HTML- need to do special check
	"Ctrl-b":     KeyCtrlB,          // not reported by HTML- need to do special check
	"Ctrl-c":     KeyCtrlC,          // not reported by HTML- need to do special check
	"Ctrl-d":     KeyCtrlD,          // not reported by HTML- need to do special check
	"Ctrl-e":     KeyCtrlE,          // not reported by HTML- need to do special check
	"Ctrl-f":     KeyCtrlF,          // not reported by HTML- need to do special check
	"Ctrl-g":     KeyCtrlG,          // not reported by HTML- need to do special check
	"Ctrl-j":     KeyCtrlJ,          // not reported by HTML- need to do special check
	"Ctrl-k":     KeyCtrlK,          // not reported by HTML- need to do special check
	"Ctrl-l":     KeyCtrlL,          // not reported by HTML- need to do special check
	"Ctrl-n":     KeyCtrlN,          // not reported by HTML- need to do special check
	"Ctrl-o":     KeyCtrlO,          // not reported by HTML- need to do special check
	"Ctrl-p":     KeyCtrlP,          // not reported by HTML- need to do special check
	"Ctrl-q":     KeyCtrlQ,          // not reported by HTML- need to do special check
	"Ctrl-r":     KeyCtrlR,          // not reported by HTML- need to do special check
	"Ctrl-s":     KeyCtrlS,          // not reported by HTML- need to do special check
	"Ctrl-t":     KeyCtrlT,          // not reported by HTML- need to do special check
	"Ctrl-u":     KeyCtrlU,          // not reported by HTML- need to do special check
	"Ctrl-v":     KeyCtrlV,          // not reported by HTML- need to do special check
	"Ctrl-w":     KeyCtrlW,          // not reported by HTML- need to do special check
	"Ctrl-x":     KeyCtrlX,          // not reported by HTML- need to do special check
	"Ctrl-y":     KeyCtrlY,          // not reported by HTML- need to do special check
	"Ctrl-z":     KeyCtrlZ,          // not reported by HTML- need to do special check
	"Ctrl- ":     KeyCtrlSpace,      // not reported by HTML- need to do special check
	"Ctrl-_":     KeyCtrlUnderscore, // not reported by HTML- need to do special check
	"Ctrl-]":     KeyCtrlRightSq,    // not reported by HTML- need to do special check
	"Ctrl-\\":    KeyCtrlBackslash,  // not reported by HTML- need to do special check
	"Ctrl-^":     KeyCtrlCarat,      // not reported by HTML- need to do special check
}

var curStyleClasses = map[CursorStyle]string{
	CursorStyleDefault:           "cursor-blinking-block",
	CursorStyleBlinkingBlock:     "cursor-blinking-block",
	CursorStyleSteadyBlock:       "cursor-steady-block",
	CursorStyleBlinkingUnderline: "cursor-blinking-underline",
	CursorStyleSteadyUnderline:   "cursor-steady-underline",
	CursorStyleBlinkingBar:       "cursor-blinking-bar",
	CursorStyleSteadyBar:         "cursor-steady-bar",
}

func LookupTerminfo(name string) (ti *terminfo.Terminfo, e error) {
	return nil, errors.New("LookupTermInfo not supported")
}
