package main

import (
	"fmt"
	"image/color"
	"log"
	"math/rand"
	"os"
	"slices"
	"sync"

	uv "github.com/charmbracelet/ultraviolet"
	"github.com/charmbracelet/ultraviolet/screen"
	"github.com/charmbracelet/x/ansi"
	"github.com/charmbracelet/x/term"
)

const rootID = "root"

// AppWindow represents a window in the application.
type AppWindow struct {
	id  string
	win *uv.Window
	ctx *screen.Context
	z   int
	st  uv.Style
}

// Bounds returns the bounds of the window.
func (aw *AppWindow) Bounds() uv.Rectangle {
	return aw.win.Bounds()
}

// Draw draws the window onto the given target window at the specified position.
func (aw *AppWindow) Draw(scr uv.Screen, rect uv.Rectangle) {
	aw.win.Draw(scr, rect)
}

// Resize resizes the window to the given width and height.
func (aw *AppWindow) Resize(width, height int) {
	aw.win.Resize(width, height)
}

// Context returns a new drawing context for the window.
func (aw *AppWindow) Context() *screen.Context {
	return aw.ctx
}

type App struct {
	scr         *uv.Window
	root        *AppWindow
	wins        map[string]*AppWindow
	zwins       []*AppWindow
	active      string
	mtx         sync.RWMutex
	quit        bool
	lastClicked string
	mouseDown   bool
}

// EventHandler represents an event handler function. It receives the focused
// window and the event as parameters. It returns true if the event was
// handled, false otherwise.
type EventHandler func(win *uv.Window, ev uv.Event) bool

// NewApp creates a new [App] instance.
func NewApp(width, height int) *App {
	a := new(App)
	a.active = rootID
	a.scr = uv.NewWindow(width, height, ansi.GraphemeWidth)
	root := &AppWindow{
		id:  rootID,
		win: a.scr.NewWindow(0, 0, width, height),
		ctx: screen.NewContext(a.scr),
		z:   0,
	}
	a.root = root
	a.wins = map[string]*AppWindow{
		root.id: root,
	}
	a.zwins = []*AppWindow{root}
	return a
}

// BringToFront brings the window with the given id to the front.
func (a *App) BringToFront(id string) {
	a.mtx.Lock()
	defer a.mtx.Unlock()

	awin, ok := a.wins[id]
	if !ok {
		return
	}

	// Remove the window from its current position.
	for i, zw := range a.zwins {
		if zw.id == id {
			a.zwins = append(a.zwins[:i], a.zwins[i+1:]...)
			break
		}
	}

	// Append it to the end of the slice.
	a.zwins = append(a.zwins, awin)
}

// CreateWindow creates a new window with the given id, position and size.
func (a *App) CreateWindow(id string, x, y, width, height int) *AppWindow {
	a.mtx.Lock()
	defer a.mtx.Unlock()

	var style uv.Style
	style.Bg = ansi.IndexedColor(rand.Intn(256))

	win := a.root.win.NewWindow(x, y, width, height)
	win.Fill(&uv.Cell{
		Content: " ",
		Width:   1,
		Style:   style,
	})

	awin := &AppWindow{
		id:  id,
		win: win,
		ctx: screen.NewContext(win),
		st:  style,
	}
	a.wins[id] = awin
	a.zwins = append(a.zwins, awin)
	return awin
}

// DestroyWindow destroys the window with the given id.
func (a *App) DestroyWindow(id string) {
	a.mtx.Lock()
	defer a.mtx.Unlock()
	_, ok := a.wins[id]
	if !ok {
		return
	}
	log.Printf("destroying window %q", id)
	delete(a.wins, id)
	for i, zw := range a.zwins {
		if zw.id == id {
			a.zwins = append(a.zwins[:i], a.zwins[i+1:]...)
			break
		}
	}
	if a.active == id {
		a.active = rootID
	}
}

// SetActiveID sets the currently active window to the given id.
func (a *App) SetActiveID(id string) {
	a.mtx.Lock()
	defer a.mtx.Unlock()
	a.active = id
}

// ActiveID returns the currently active window. If no active window is set, it
// returns an empty string.
func (a *App) ActiveID() string {
	return a.active
}

// Window returns the window associated with the given id. If no window is
// found, it returns nil.
func (a *App) Window(id string) *uv.Window {
	a.mtx.RLock()
	defer a.mtx.RUnlock()
	if win, ok := a.wins[id]; ok {
		return win.win
	}
	return nil
}

// ParentID returns the parent window ID of the given window ID. If no parent is
// found, it returns an empty string.
func (a *App) ParentID(id string) string {
	a.mtx.RLock()
	defer a.mtx.RUnlock()
	win, ok := a.wins[id]
	if !ok {
		return ""
	}
	parent := win.win.Parent()
	if parent == nil {
		return ""
	}
	for _, aw := range a.wins {
		if aw.win == parent {
			return aw.id
		}
	}
	return ""
}

// Draw draws the applications windows to the root window.
func (a *App) Draw(scr uv.Screen, area uv.Rectangle) {
	a.mtx.Lock()
	defer a.mtx.Unlock()

	slices.SortStableFunc(a.zwins, func(aw1, aw2 *AppWindow) int {
		return aw1.z - aw2.z
	})

	screen.Clear(a.root.win)
	for _, zw := range a.zwins {
		if zw.id == rootID {
			continue
		}
		cell0 := zw.win.CellAt(0, 0)
		var bg color.Color
		if cell0 != nil {
			bg = cell0.Style.Bg
		}
		log.Printf("drawing window %q at z-index %d bg color %v", zw.id, zw.z, bg)
		zw.Draw(a.root.win, zw.Bounds())
	}

	a.root.Draw(scr, area)
}

// HandleEvent implements an [EventHandler] for the application.
func (a *App) HandleEvent(id string, ev uv.Event) bool {
	win := a.Window(id)
	if win == nil {
		return false
	}

	switch ev := ev.(type) {
	case uv.KeyPressEvent:
		switch {
		case ev.MatchString("ctrl+c", "esc"):
			a.quit = true
			return true
		default:
			activeWin, ok := a.wins[a.ActiveID()]
			if !ok {
				log.Printf("no active window found for id %q", a.ActiveID())
				break
			}

			ctx := activeWin.Context()
			ctx.SetStyle(activeWin.st)
			ctx.SetForeground(color.Black)

			switch {
			case ev.MatchString("backspace"):
				x, y := ctx.Position()
				x--
				if x < 0 {
					x = activeWin.Bounds().Dx() - 1
					y--
					if y < 0 {
						y = 0
					}
				}
				ctx.SetPosition(x, y)
				ctx.Printf(" ")
				ctx.SetPosition(x, y)
				return true
			case ev.MatchString("enter"):
				ctx.Printf("\n")
				return true
			case len(ev.Text) > 0:
				ctx.Print(ev.Text)
				return true
			}
		}
	case uv.MouseMotionEvent:
		if a.mouseDown && a.lastClicked == id {
			// Move the window.
			bounds := win.Bounds()
			newX := ev.X - bounds.Dx()/2
			newY := ev.Y - bounds.Dy()/2
			log.Printf("moving window %q from %v to (%d, %d)", id, bounds.Min, newX, newY)
			win.MoveTo(newX, newY)
			return true
		}
	case uv.MouseReleaseEvent:
		a.mouseDown = false
		a.lastClicked = ""
		return true
	case uv.MouseClickEvent:
		a.mouseDown = true

		switch ev.Button {
		case uv.MouseLeft:
			log.Printf("mouse left click for %q at (%d, %d)", id, ev.X, ev.Y)

			for i := len(a.zwins) - 1; i >= 0; i-- {
				zw := a.zwins[i]
				pos := uv.Pos(ev.X, ev.Y)
				bounds := zw.win.Bounds()
				if zw.id != rootID && pos.In(bounds) {
					log.Printf("clicked window %s at %v (bounds: %v)", zw.id, pos, bounds)
					a.SetActiveID(zw.id)
					a.BringToFront(zw.id)
					a.lastClicked = zw.id
					return true
				}
			}

			log.Printf("no window clicked on at (%d, %d)", ev.X, ev.Y)

			if id == rootID {
				log.Printf("clicked root window at (%d, %d)", ev.X, ev.Y)

				// Create a new window when we click anywhere in the root window.
				rootSize := a.root.Bounds().Size()
				width := rand.Intn(20)
				height := rand.Intn(10)
				if width == 0 || height == 0 {
					// Try again
					return a.HandleEvent(id, ev)
				}

				x := ev.X - width/2
				y := ev.Y - height/2
				if x < 0 {
					x = 0
				}
				if y < 0 {
					y = 0
				}
				if x+width > rootSize.X {
					x = rootSize.X - width
				}
				if y+height > rootSize.Y {
					y = rootSize.Y - height
				}

				winID := fmt.Sprintf("win-%d", len(a.wins))
				a.CreateWindow(winID, x, y, width, height)
				a.SetActiveID(winID)

				return true
			}

		case uv.MouseRight:
			// Destroy the clicked window.
			for i := len(a.zwins) - 1; i >= 0; i-- {
				zw := a.zwins[i]
				pos := uv.Pos(ev.X, ev.Y)
				if pos.In(zw.win.Bounds()) && zw.id != rootID {
					log.Printf("right-clicked window %q at (%d, %d), destroying", zw.id, ev.X, ev.Y)
					a.DestroyWindow(zw.id)
				}
			}

			return true
		}
	}

	return false
}

// Resize resizes the screen to the given width and height.
func (a *App) Resize(width, height int) error {
	a.mtx.Lock()
	defer a.mtx.Unlock()
	a.scr.Resize(width, height)
	return nil
}

// Run starts the application event loop on the given terminal.
func (a *App) Run(input uv.File, output uv.File, environ []string) error {
	term := uv.NewTerminal(uv.NewConsole(input, output, environ), nil)
	// We're using the alternate screen buffer, we need to ensure we're using
	// the fullscreen and absolute cursor movement flags.
	scr := term.Screen()
	scr.EnterAltScreen()
	scr.HideCursor()

	if err := term.Start(); err != nil {
		return fmt.Errorf("failed to start terminal: %w", err)
	}

	defer term.Stop()

	scr.SetMouseMode(uv.MouseModeDrag)

	for !a.quit {
		select {
		case ev := <-term.Events():
			log.Printf("event: %#v", ev)

			switch ev := ev.(type) {
			case uv.WindowSizeEvent:
				// We need to update our terminal size and root window size.
				scr.Resize(ev.Width, ev.Height)
				a.root.Resize(ev.Width, ev.Height)
			}

			focusedID := a.ActiveID()
			if len(focusedID) == 0 {
				// Ignore events if no window is focused.
				continue
			}

			for !a.HandleEvent(focusedID, ev) {
				if parentID := a.ParentID(focusedID); parentID != "" {
					log.Printf("event not handled by %q, passing to parent %q", focusedID, parentID)
					focusedID = parentID
				} else {
					break
				}
			}

			if err := scr.Display(a); err != nil {
				return fmt.Errorf("failed to display terminal: %w", err)
			}
		}
	}

	return nil
}

func init() {
	f, err := os.OpenFile("uv.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
	if err != nil {
		log.Fatalf("failed to open log file: %v", err)
	}
	log.SetOutput(f)
	log.SetFlags(log.LstdFlags | log.Lshortfile)
}

func main() {
	stdin, stdout, environ := os.Stdin, os.Stdout, os.Environ()
	physicalWidth, physicalHeight, err := term.GetSize(stdout.Fd())
	if err != nil {
		log.Fatalf("failed to get terminal size: %v", err)
	}

	app := NewApp(physicalWidth, physicalHeight)
	if err := app.Run(stdin, stdout, environ); err != nil {
		log.Fatalf("application error: %v", err)
	}
}
