// 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.

package tcell

import (
	"sync"
	"unicode/utf8"

	"golang.org/x/text/transform"
)

// NewSimulationScreen returns a SimulationScreen.  Note that
// SimulationScreen is also a Screen.
func NewSimulationScreen(charset string) SimulationScreen {
	if charset == "" {
		charset = "UTF-8"
	}
	ss := &simscreen{charset: charset}
	ss.Screen = &baseScreen{screenImpl: ss}
	return ss
}

// SimulationScreen represents a screen simulation.  This is intended to
// be a superset of normal Screens, but also adds some important interfaces
// for testing.
type SimulationScreen interface {
	Screen

	// InjectKeyBytes injects a stream of bytes corresponding to
	// the native encoding (see charset).  It turns true if the entire
	// set of bytes were processed and delivered as KeyEvents, false
	// if any bytes were not fully understood.  Any bytes that are not
	// fully converted are discarded.
	InjectKeyBytes(buf []byte) bool

	// InjectKey injects a key event.  The rune is a UTF-8 rune, post
	// any translation.
	InjectKey(key Key, r rune, mod ModMask)

	// InjectMouse injects a mouse event.
	InjectMouse(x, y int, buttons ButtonMask, mod ModMask)

	// GetContents returns screen contents as an array of
	// cells, along with the physical width & height.   Note that the
	// physical contents will be used until the next time SetSize()
	// is called.
	GetContents() (cells []SimCell, width int, height int)

	// GetCursor returns the cursor details.
	GetCursor() (x int, y int, visible bool)

	// GetTitle gets the previously set title.
	GetTitle() string

	// GetClipboardData gets the actual data for the clipboard.
	GetClipboardData() []byte
}

// SimCell represents a simulated screen cell.  The purpose of this
// is to track on screen content.
type SimCell struct {
	// Bytes is the actual character bytes.  Normally this is
	// rune data, but it could be be data in another encoding system.
	Bytes []byte

	// Style is the style used to display the data.
	Style Style

	// Runes is the list of runes, unadulterated, in UTF-8.
	Runes []rune
}

type simscreen struct {
	physw int
	physh int
	fini  bool
	style Style
	evch  chan Event
	quit  chan struct{}

	front     []SimCell
	back      CellBuffer
	clear     bool
	cursorx   int
	cursory   int
	cursorvis bool
	mouse     bool
	paste     bool
	charset   string
	encoder   transform.Transformer
	decoder   transform.Transformer
	fillchar  rune
	fillstyle Style
	fallback  map[rune]string
	title     string
	clipboard []byte

	Screen
	sync.Mutex
}

func (s *simscreen) Init() error {
	s.evch = make(chan Event, 10)
	s.quit = make(chan struct{})
	s.fillchar = 'X'
	s.fillstyle = StyleDefault
	s.mouse = false
	s.physw = 80
	s.physh = 25
	s.cursorx = -1
	s.cursory = -1
	s.style = StyleDefault

	if enc := GetEncoding(s.charset); enc != nil {
		s.encoder = enc.NewEncoder()
		s.decoder = enc.NewDecoder()
	} else {
		return ErrNoCharset
	}

	s.front = make([]SimCell, s.physw*s.physh)
	s.back.Resize(80, 25)

	// default fallbacks
	s.fallback = make(map[rune]string)
	for k, v := range RuneFallbacks {
		s.fallback[k] = v
	}
	return nil
}

func (s *simscreen) Fini() {
	s.Lock()
	s.fini = true
	s.back.Resize(0, 0)
	s.Unlock()
	if s.quit != nil {
		close(s.quit)
	}
	s.physw = 0
	s.physh = 0
	s.front = nil
}

func (s *simscreen) SetStyle(style Style) {
	s.Lock()
	s.style = style
	s.Unlock()
}

func (s *simscreen) drawCell(x, y int) int {

	mainc, combc, style, width := s.back.GetContent(x, y)
	if !s.back.Dirty(x, y) {
		return width
	}
	if x >= s.physw || y >= s.physh || x < 0 || y < 0 {
		return width
	}
	simc := &s.front[(y*s.physw)+x]

	if style == StyleDefault {
		style = s.style
	}
	simc.Style = style
	simc.Runes = append([]rune{mainc}, combc...)

	// now emit runes - taking care to not overrun width with a
	// wide character, and to ensure that we emit exactly one regular
	// character followed up by any residual combing characters

	simc.Bytes = nil

	if x > s.physw-width {
		simc.Runes = []rune{' '}
		simc.Bytes = []byte{' '}
		return width
	}

	lbuf := make([]byte, 12)
	ubuf := make([]byte, 12)
	nout := 0

	for _, r := range simc.Runes {

		l := utf8.EncodeRune(ubuf, r)

		nout, _, _ = s.encoder.Transform(lbuf, ubuf[:l], true)

		if nout == 0 || lbuf[0] == '\x1a' {

			// skip combining

			if subst, ok := s.fallback[r]; ok {
				simc.Bytes = append(simc.Bytes,
					[]byte(subst)...)

			} else if r >= ' ' && r <= '~' {
				simc.Bytes = append(simc.Bytes, byte(r))

			} else if simc.Bytes == nil {
				simc.Bytes = append(simc.Bytes, '?')
			}
		} else {
			simc.Bytes = append(simc.Bytes, lbuf[:nout]...)
		}
	}
	s.back.SetDirty(x, y, false)
	return width
}

func (s *simscreen) ShowCursor(x, y int) {
	s.Lock()
	s.cursorx, s.cursory = x, y
	s.showCursor()
	s.Unlock()
}

func (s *simscreen) HideCursor() {
	s.ShowCursor(-1, -1)
}

func (s *simscreen) showCursor() {

	x, y := s.cursorx, s.cursory
	if x < 0 || y < 0 || x >= s.physw || y >= s.physh {
		s.cursorvis = false
	} else {
		s.cursorvis = true
	}
}

func (s *simscreen) hideCursor() {
	// does not update cursor position
	s.cursorvis = false
}

func (s *simscreen) SetCursor(CursorStyle, Color) {}

func (s *simscreen) Show() {
	s.Lock()
	s.resize()
	s.draw()
	s.Unlock()
}

func (s *simscreen) clearScreen() {
	// We emulate a hardware clear by filling with a specific pattern
	for i := range s.front {
		s.front[i].Style = s.fillstyle
		s.front[i].Runes = []rune{s.fillchar}
		s.front[i].Bytes = []byte{byte(s.fillchar)}
	}
	s.clear = false
}

func (s *simscreen) draw() {
	s.hideCursor()
	if s.clear {
		s.clearScreen()
	}

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

func (s *simscreen) EnableMouse(...MouseFlags) {
	s.mouse = true
}

func (s *simscreen) DisableMouse() {
	s.mouse = false
}

func (s *simscreen) EnablePaste() {
	s.paste = true
}

func (s *simscreen) DisablePaste() {
	s.paste = false
}

func (s *simscreen) EnableFocus() {
}

func (s *simscreen) DisableFocus() {
}

func (s *simscreen) Size() (int, int) {
	s.Lock()
	w, h := s.back.Size()
	s.Unlock()
	return w, h
}

func (s *simscreen) resize() {
	w, h := s.physw, s.physh
	ow, oh := s.back.Size()
	if w != ow || h != oh {
		s.back.Resize(w, h)
		ev := NewEventResize(w, h)
		s.postEvent(ev)
	}
}

func (s *simscreen) Colors() int {
	return 256
}

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

func (s *simscreen) InjectMouse(x, y int, buttons ButtonMask, mod ModMask) {
	ev := NewEventMouse(x, y, buttons, mod)
	s.postEvent(ev)
}

func (s *simscreen) InjectKey(key Key, r rune, mod ModMask) {
	ev := NewEventKey(key, r, mod)
	s.postEvent(ev)
}

func (s *simscreen) InjectKeyBytes(b []byte) bool {
	failed := false

outer:
	for len(b) > 0 {
		if b[0] >= ' ' && b[0] <= 0x7F {
			// printable ASCII easy to deal with -- no encodings
			ev := NewEventKey(KeyRune, rune(b[0]), ModNone)
			s.postEvent(ev)
			b = b[1:]
			continue
		}

		if b[0] < 0x80 {
			// No encodings start with low numbered values
			if b[0] > 0 && b[0] < ' ' { // control keys
				switch Key(b[0]) {
				case KeyESC, KeyEnter, KeyTAB:
					s.postEvent(NewEventKey(Key(b[0]), 0, 0))
					continue;
				default:
					s.postEvent(NewEventKey(Key(b[0]), rune(b[0])+'\x60', ModCtrl))
					continue
				}
			}
			mod := ModNone
			ev := NewEventKey(Key(b[0]), 0, mod)
			s.postEvent(ev)
			b = b[1:]
			continue
		}

		utfb := make([]byte, len(b)*4) // worst case
		for l := 1; l < len(b); l++ {
			s.decoder.Reset()
			nout, nin, _ := s.decoder.Transform(utfb, b[:l], true)

			if nout != 0 {
				r, _ := utf8.DecodeRune(utfb[:nout])
				if r != utf8.RuneError {
					ev := NewEventKey(KeyRune, r, ModNone)
					s.postEvent(ev)
				}
				b = b[nin:]
				continue outer
			}
		}
		failed = true
		b = b[1:]
		continue
	}

	return !failed
}

func (s *simscreen) Sync() {
	s.Lock()
	s.clear = true
	s.resize()
	s.back.Invalidate()
	s.draw()
	s.Unlock()
}

func (s *simscreen) CharacterSet() string {
	return s.charset
}

func (s *simscreen) SetSize(w, h int) {
	s.Lock()
	newc := make([]SimCell, w*h)
	for row := 0; row < h && row < s.physh; row++ {
		for col := 0; col < w && col < s.physw; col++ {
			newc[(row*w)+col] = s.front[(row*s.physw)+col]
		}
	}
	s.cursorx, s.cursory = -1, -1
	s.physw, s.physh = w, h
	s.front = newc
	s.back.Resize(w, h)
	s.Unlock()
}

func (s *simscreen) GetContents() ([]SimCell, int, int) {
	s.Lock()
	cells, w, h := s.front, s.physw, s.physh
	s.Unlock()
	return cells, w, h
}

func (s *simscreen) GetCursor() (int, int, bool) {
	s.Lock()
	x, y, vis := s.cursorx, s.cursory, s.cursorvis
	s.Unlock()
	return x, y, vis
}

func (s *simscreen) RegisterRuneFallback(r rune, subst string) {
	s.Lock()
	s.fallback[r] = subst
	s.Unlock()
}

func (s *simscreen) UnregisterRuneFallback(r rune) {
	s.Lock()
	delete(s.fallback, r)
	s.Unlock()
}

func (s *simscreen) CanDisplay(r rune, checkFallbacks bool) bool {

	if enc := s.encoder; enc != nil {
		nb := make([]byte, 6)
		ob := make([]byte, 6)
		num := utf8.EncodeRune(ob, r)

		enc.Reset()
		dst, _, err := enc.Transform(nb, ob[:num], true)
		if dst != 0 && err == nil && nb[0] != '\x1A' {
			return true
		}
	}
	if !checkFallbacks {
		return false
	}
	if _, ok := s.fallback[r]; ok {
		return true
	}
	return false
}

func (s *simscreen) HasMouse() bool {
	return false
}

func (s *simscreen) Resize(int, int, int, int) {}

func (s *simscreen) HasKey(Key) bool {
	return true
}

func (s *simscreen) Beep() error {
	return nil
}

func (s *simscreen) Suspend() error {
	return nil
}

func (s *simscreen) Resume() error {
	return nil
}

func (s *simscreen) Tty() (Tty, bool) {
	return nil, false
}

func (s *simscreen) GetCells() *CellBuffer {
	return &s.back
}

func (s *simscreen) EventQ() chan Event {
	return s.evch
}

func (s *simscreen) StopQ() <-chan struct{} {
	return s.quit
}

func (s *simscreen) SetTitle(title string) {
	s.title = title
}

func (s *simscreen) GetTitle() string {
	return s.title
}

func (s *simscreen) SetClipboard(data []byte) {
	s.clipboard = data
}

func (s *simscreen) GetClipboard() {
	if s.clipboard != nil {
		ev := NewEventClipboard(s.clipboard)
		s.postEvent(ev)
	}
}

func (s *simscreen) GetClipboardData() []byte {
	return s.clipboard
}
