package gui

import (
	"fmt"
	"time"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/liamg/darktile/internal/app/darktile/hinters"
	"github.com/liamg/darktile/internal/app/darktile/termutil"
)

// time allowed between mouse clicks to chain them into e.g. double-click
const clickChainWindowMS = 500

// max duration of a click before it is counted as a drag
const clickMaxDuration = 100

func (g *GUI) handleMouse() error {

	_, scrollY := ebiten.Wheel()
	if scrollY < 0 {
		g.terminal.GetActiveBuffer().ScrollDown(5)
	} else if scrollY > 0 {
		g.terminal.GetActiveBuffer().ScrollUp(5)
	}

	x, y := ebiten.CursorPosition()
	col := x / g.fontManager.CharSize().X
	line := y / g.fontManager.CharSize().Y
	var moved bool

	if col != int(g.mousePos.Col) || line != int(g.mousePos.Line) {
		if col >= 0 && col < int(g.terminal.GetActiveBuffer().ViewWidth()) && line >= 0 && line < int(g.terminal.GetActiveBuffer().ViewHeight()) {
			// mouse moved!
			moved = true
			g.mousePos = termutil.Position{
				Col:  uint16(col),
				Line: uint64(line),
			}
			if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
				if err := g.handleMouseMove(g.mousePos); err != nil {
					return err
				}
			}
		} else if err := g.clearHinters(); err != nil {
			return err
		}
	}

	pressedLeft := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && g.mouseStateLeft != MouseStatePressed
	pressedMiddle := ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) && g.mouseStateMiddle != MouseStatePressed
	pressedRight := ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) && g.mouseStateRight != MouseStatePressed
	released := (!ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && g.mouseStateLeft == MouseStatePressed) ||
		(!ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) && g.mouseStateMiddle == MouseStatePressed) ||
		(!ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) && g.mouseStateRight == MouseStatePressed)

	defer func() {
		if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
			g.mouseStateLeft = MouseStatePressed
		} else {
			g.mouseStateLeft = MouseStateNone
		}

		if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) {
			g.mouseStateMiddle = MouseStatePressed
		} else {
			g.mouseStateMiddle = MouseStateNone
		}

		if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
			g.mouseStateRight = MouseStatePressed
		} else {
			g.mouseStateRight = MouseStateNone
		}
	}()

	if pressedLeft || pressedMiddle || pressedRight || released {
		if g.handleMouseRemotely(x, y, pressedLeft, pressedMiddle, pressedRight, released, moved) {
			return nil
		}
	}

	if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
		if g.mouseStateLeft == MouseStatePressed {

			if g.mouseDrag {

				// update selection end
				g.terminal.GetActiveBuffer().SetSelectionEnd(termutil.Position{
					Line: uint64(line),
					Col:  uint16(col),
				})
			} else if time.Since(g.lastClick) > time.Millisecond*clickMaxDuration && !ebiten.IsKeyPressed(ebiten.KeyControl) {
				g.mouseDrag = true
			}
		} else {

			if g.clickCount == 0 || time.Since(g.lastClick) < time.Millisecond*clickChainWindowMS {
				g.clickCount++
			} else {
				g.clickCount = 1
			}

			g.lastClick = time.Now()

			handled, err := g.handleClick(g.clickCount, x, y)
			if err != nil {
				return err
			}
			if handled {
				g.mouseDrag = false
				return nil
			}

			//set selection start
			col := x / g.fontManager.CharSize().X
			line := y / g.fontManager.CharSize().Y

			g.terminal.GetActiveBuffer().SetSelectionStart(termutil.Position{
				Line: uint64(line),
				Col:  uint16(col),
			})
		}

		ebiten.ScheduleFrame()

	} else {
		g.mouseDrag = false
	}

	return nil
}

func (g *GUI) clearHinters() error {
	if g.activeHinter > -1 {
		if err := g.hinters[g.activeHinter].Deactivate(g); err != nil {
			return err
		}
		g.activeHinter = -1
	}
	return nil
}

// mouse moved to cell (not during click + drag)
func (g *GUI) handleMouseMove(pos termutil.Position) error {

	// start uses raw coords
	start, _, text, index, ok := g.terminal.GetActiveBuffer().GetBoundedTextAtPosition(pos)
	if !ok {
		g.clearHinters()
		return nil
	}

	activeHinter := -1

	for i, hinter := range g.hinters {
		if ok, offset, length := hinter.Match(text, index); ok {
			match := text[offset : offset+length]

			newStartX := int(start.Col) + offset
			newStartY := start.Line
			for newStartX >= int(g.terminal.GetActiveBuffer().ViewWidth()) {
				newStartX -= int(g.terminal.GetActiveBuffer().ViewWidth())
				newStartY++
			}

			newEndX := newStartX + length - 1
			newEndY := newStartY
			for newEndX > int(g.terminal.GetActiveBuffer().ViewWidth()) {
				newEndX -= int(g.terminal.GetActiveBuffer().ViewWidth())
				newEndY++
			}

			matchStart := termutil.Position{
				Col:  uint16(newStartX),
				Line: newStartY,
			}
			matchEnd := termutil.Position{
				Col:  uint16(newEndX),
				Line: newEndY,
			}

			if err := hinter.Activate(g, match, matchStart, matchEnd); err != nil {
				return err
			}

			activeHinter = i
			break
		}
	}

	// hinter was just deactivated
	if g.activeHinter > -1 && activeHinter == -1 {
		if err := g.clearHinters(); err != nil {
			return err
		}
	}

	g.activeHinter = activeHinter

	return nil
}

func WithHinter(h hinters.Hinter) func(g *GUI) error {
	return func(g *GUI) error {
		g.hinters = append(g.hinters, h)
		return nil
	}
}

func (g *GUI) handleClick(clickCount, x, y int) (bool, error) {

	switch clickCount {
	case 1: // single click
		if ebiten.IsKeyPressed(ebiten.KeyControl) { // ctrl + click to run hinters
			if g.activeHinter > -1 {
				g.hinters[g.activeHinter].Click(g)
			}
		} else {
			g.terminal.GetActiveBuffer().ClearSelection()
		}

	case 2: //double click
		col := uint16(x / g.fontManager.CharSize().X)
		line := uint64(y / g.fontManager.CharSize().Y)
		g.terminal.GetActiveBuffer().SelectWordAt(termutil.Position{Col: col, Line: line}, wordMatcher)
		return true, nil
	default: // triple click (or more!)
		g.terminal.GetActiveBuffer().ExtendSelectionToEntireLines()
		return true, nil
	}

	return false, nil
}

func alphaMatcher(r rune) bool {
	if r >= 65 && r <= 90 {
		return true
	}
	if r >= 97 && r <= 122 {
		return true
	}
	return false
}

func numberMatcher(r rune) bool {
	if r >= 48 && r <= 57 {
		return true
	}
	return false
}

func alphaNumericMatcher(r rune) bool {
	return alphaMatcher(r) || numberMatcher(r)
}

func wordMatcher(r rune) bool {

	if alphaNumericMatcher(r) {
		return true
	}

	if r == '_' {
		return true
	}

	return false
}

func (g *GUI) handleMouseRemotely(x, y int, pressedLeft, pressedMiddle, pressedRight, released, moved bool) bool {

	tx, ty := 1+(x/g.fontManager.CharSize().X), 1+(y/g.fontManager.CharSize().Y)

	mode := g.terminal.GetMouseMode()

	switch mode {
	case termutil.MouseModeNone:
		return false
	case termutil.MouseModeX10:
		var button rune
		switch true {
		case pressedLeft:
			button = 0
		case pressedMiddle:
			button = 1
		case pressedRight:
			button = 2
		default:
			return true
		}
		packet := fmt.Sprintf("\x1b[M%c%c%c", (rune(button + 32)), (rune(tx + 32)), (rune(ty + 32)))
		_ = g.terminal.WriteToPty([]byte(packet))
		return true
	case termutil.MouseModeVT200, termutil.MouseModeButtonEvent:

		var button rune

		extMode := g.terminal.GetMouseExtMode()

		switch true {
		case pressedLeft:
			button = 0
		case pressedMiddle:
			button = 1
		case pressedRight:
			button = 2
		case released:
			if extMode != termutil.MouseExtSGR {
				button = 3
			}
		default:
			return true
		}

		if moved && mode == termutil.MouseModeButtonEvent {
			button |= 32
		}

		if ebiten.IsKeyPressed(ebiten.KeyShift) {
			button |= 4
		}

		if ebiten.IsKeyPressed(ebiten.KeyMeta) {
			button |= 8
		}

		if ebiten.IsKeyPressed(ebiten.KeyControl) {
			button |= 16
		}

		var packet string

		if extMode == termutil.MouseExtSGR {
			final := 'M'
			if released {
				final = 'm'
			}
			packet = fmt.Sprintf("\x1b[<%d;%d;%d%c", button, tx, ty, final)
		} else {
			packet = fmt.Sprintf("\x1b[M%c%c%c", button+32, tx+32, ty+32)
		}

		g.terminal.WriteToPty([]byte(packet))
		return true

	}

	return false

}

func (g *GUI) SetCursorToPointer() {
	ebiten.SetCursorShape(ebiten.CursorShapePointer)
}

func (g *GUI) ResetCursor() {
	ebiten.SetCursorShape(ebiten.CursorShapeDefault)
}
