package ui

import (
	"fmt"
	"strings"
	"time"
	"unicode"

	"opslag.de/schobers/geom"
)

type TextSelection struct {
	Caret int
	Start int
	End   int
}

func (s *TextSelection) SetSelectionToCaret() {
	s.Start = s.Caret
	s.End = s.Caret
}

func (s *TextSelection) MoveCaret(delta int) {
	s.Caret = s.Caret + delta
}

func (s TextSelection) HasSelection() bool {
	return s.Start != s.End
}

type TextBox struct {
	ControlBase

	box   BufferControl
	blink time.Time

	Focus     bool
	Text      string
	Selection TextSelection

	textChanged Events
}

func BuildTextBox(fn func(*TextBox)) *TextBox {
	var b = &TextBox{}
	if fn != nil {
		fn(b)
	}
	return b
}

func (b *TextBox) pad(ctx Context) float32 {
	return ctx.Style().Dimensions.TextPadding
}

func (b *TextBox) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
	b.ControlBase.Arrange(ctx, bounds, offset, parent)
	b.box.Arrange(ctx, bounds.Inset(b.pad(ctx)), offset, b)
}

func (b *TextBox) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
	font := b.Font_(ctx)
	var width = font.WidthOf(b.Text)
	var height = font.Height()
	var pad = b.pad(ctx)
	return geom.PtF32(width+pad*2, height+pad*2)
}

func (b *TextBox) TextChanged() *Events { return &b.textChanged }

func (b *TextBox) mousePosToCaretPos(ctx Context, e MouseEvent) int {
	p := b.ToControlPosition(e.Pos())
	offset := p.X - b.box.bounds.Min.X
	f := b.Font_(ctx)
	var carets = [3]int{0, 0, len(b.Text)}
	var offsets = [3]float32{0, 0, f.WidthOf(b.Text)}
	var updateCenter = func() {
		var c = (carets[0] + carets[2]) / 2
		carets[1] = c
		offsets[1] = f.WidthOf(b.Text[:c])
	}
	updateCenter()
	for (carets[2] - carets[0]) > 1 {
		if offset > offsets[1] {
			carets[0] = carets[1]
			offsets[0] = offsets[1]
			updateCenter()
		} else {
			carets[2] = carets[1]
			offsets[2] = offsets[1]
			updateCenter()
		}
	}
	if geom.Abs32(offsets[0]-offset) < geom.Abs32(offsets[2]-offset) {
		return carets[0]
	}
	return carets[2]
}

func (b *TextBox) cut(ctx Context) string {
	start, end := b.selectionRange()
	if end == 0 {
		return ""
	}
	cut := b.Text[start:end]
	b.updateText(ctx, b.Text[:start]+b.Text[end:])
	b.Selection.Caret = start
	b.Selection.SetSelectionToCaret()
	return cut
}

func (b *TextBox) selection() string {
	start, end := b.selectionRange()
	if end == 0 {
		return ""
	}
	return b.Text[start:end]
}

func (b *TextBox) selectionRange() (int, int) {
	start, end := b.Selection.Start, b.Selection.End
	if start == end {
		return 0, 0
	}
	if start > end {
		start, end = end, start
	}
	return start, end
}

func (b *TextBox) Handle(ctx Context, e Event) bool {
	if b.ControlBase.Handle(ctx, e) {
		return true
	}
	b.box.Handle(ctx, e)

	if b.over {
		if b.Disabled {
			return true
		}
		ctx.Renderer().SetMouseCursor(MouseCursorText)
	}
	if b.Focus {
		ctx.Animate()
	}

	switch e := e.(type) {
	case *MouseButtonDownEvent:
		if b.over {
			b.Focus = true
			b.Selection.Caret = b.mousePosToCaretPos(ctx, e.MouseEvent)
			b.Selection.SetSelectionToCaret()
			b.blink = time.Now()
			return true
		}
		b.Focus = false
	case *MouseMoveEvent:
		if b.Focus && b.pressed && b.over {
			b.Selection.Caret = b.mousePosToCaretPos(ctx, e.MouseEvent)
			b.Selection.End = b.Selection.Caret
			return true
		}
	case *KeyDownEvent:
		if !b.Focus {
			break
		}
		selectAfterMove := func() {
			if e.Modifiers&KeyModifierShift == KeyModifierShift {
				b.Selection.End = b.Selection.Caret
			} else {
				b.Selection.SetSelectionToCaret()
			}
		}
		switch {
		case e.Key == KeyDelete:
			if b.Selection.HasSelection() {
				b.cut(ctx)
			} else {
				caret := b.Selection.Caret
				if caret < len(b.Text) {
					b.updateText(ctx, b.Text[:caret]+b.Text[caret+1:])
				}
			}
		case e.Key == KeyBackspace:
			if b.Selection.HasSelection() {
				b.cut(ctx)
			} else {
				caret := b.Selection.Caret
				if caret > 0 {
					b.updateText(ctx, b.Text[:caret-1]+b.Text[caret:])
					b.Selection.Caret = caret - 1
					b.Selection.SetSelectionToCaret()
				}
			}
		case e.Key == KeyEnd:
			b.Selection.Caret = len(b.Text)
			selectAfterMove()
		case e.Key == KeyHome:
			b.Selection.Caret = 0
			selectAfterMove()
		case e.Key == KeyLeft:
			if b.Selection.Caret > 0 {
				b.Selection.MoveCaret(-1)
				selectAfterMove()
			}
		case e.Key == KeyRight:
			if b.Selection.Caret < len(b.Text) {
				b.Selection.MoveCaret(1)
				selectAfterMove()
			}
		case e.Modifiers&KeyModifierControl == KeyModifierControl:
			switch e.Key {
			case KeyA:
				b.Selection.Start = 0
				b.Selection.End = len(b.Text)
				b.Selection.Caret = b.Selection.End
			case KeyC:
				DefaultClipboard.WriteText(b.selection())
			case KeyV:
				text, err := DefaultClipboard.ReadText()
				text = strings.Map(func(r rune) rune {
					if unicode.IsControl(r) {
						return -1
					}
					return r
				}, text)
				if err == nil {
					b.cut(ctx)
					caret := b.Selection.Caret
					b.updateText(ctx, b.Text[:caret]+text+b.Text[caret:])
					b.Selection.Caret = caret + len(text)
				}
			case KeyX:
				DefaultClipboard.WriteText(b.cut(ctx))
			}
		}
		return true
	case *TextInputEvent:
		if b.Selection.HasSelection() {
			b.cut(ctx)
		}
		caret := b.Selection.Caret
		b.updateText(ctx, fmt.Sprintf("%s%c%s", b.Text[:caret], e.Character, b.Text[caret:]))
		b.Selection.Caret = caret + 1
		b.Selection.SetSelectionToCaret()
		return true
	}
	return false
}

func (b *TextBox) Render(ctx Context) {
	b.RenderBackground(ctx)
	b.RenderOutline(ctx)

	c := b.TextColor(ctx)
	style := ctx.Style()
	var caretWidth float32 = 1
	b.box.RenderFn(ctx, func(_ Context, size geom.PointF32) {
		var renderer = ctx.Renderer()
		back := b.Background
		if back == nil {
			back = ctx.Style().Palette.Background
		}
		renderer.Clear(back)
		font := b.Font_(ctx)
		if b.Selection.Start != b.Selection.End {
			left, right := font.WidthOf(b.Text[:b.Selection.Start]), font.WidthOf(b.Text[:b.Selection.End])
			renderer.FillRectangle(geom.RectF32(left, 0, right, size.Y), style.Palette.PrimaryHighlight)
		}
		renderer.Text(font, geom.ZeroPtF32, c, b.Text)
		const interval = 500 * time.Millisecond
		if b.Focus && (time.Since(b.blink)/interval)%2 == 0 {
			var w = font.WidthOf(b.Text[:b.Selection.Caret])
			var caret = w + .5*caretWidth
			renderer.Rectangle(geom.RectF32(caret, 0, caret, size.Y), c, caretWidth)
		}
	})
}

func (b *TextBox) updateText(ctx Context, text string) {
	if b.Text == text {
		return
	}
	b.Text = text
	b.textChanged.Notify(ctx, text)
}