package main

import (
	"fmt"
	"log"
	"slices"
	"strconv"
	"strings"
	"time"

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

func main() {
	t := uv.DefaultTerminal()
	scr := t.Screen()
	scr.EnterAltScreen()

	if err := t.Start(); err != nil {
		log.Fatalln("failed to start terminal:", err)
	}

	defer t.Stop()

	var area uv.Rectangle

	ticker := time.NewTicker(time.Second / 60)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			render(scr, area)

			scr.Render()
			scr.Flush()

		case ev := <-t.Events():
			switch ev := ev.(type) {
			case uv.WindowSizeEvent:
				area = ev.Bounds()
				scr.Resize(area.Dx(), area.Dy())
				screen.Clear(scr)

			case uv.KeyPressEvent:
				switch {
				case ev.MatchString("ctrl+c", "q"):
					return
				}
			}
		}
	}
}

func render(sc uv.Screen, area uv.Rectangle) {
	var textArea uv.Rectangle

	layout.Vertical(layout.Len(7), layout.Min(0)).Split(area).Assign(&textArea, &area)

	lines := `Horizontal Layout Example. Press q to quit
Each line has 2 constraints, plus Min(0) to fill the remaining space.
E.g. the second line of the Len/Min box is [Len(2), Min(2), Min(0)]
Note: constraint labels that don't fit are truncated`
	screen.NewContext(sc).DrawStringWrapped(lines, textArea.Min.X, textArea.Min.Y)

	rows := layout.Vertical(
		layout.Len(9),
		layout.Len(9),
		layout.Len(9),
		layout.Len(9),
		layout.Len(9),
		layout.Min(0), // fills remaining space
	).Split(area)

	var areas []uv.Rectangle

	for _, r := range rows {
		cols := layout.Horizontal(
			layout.Len(14),
			layout.Len(14),
			layout.Len(14),
			layout.Len(14),
			layout.Len(14),
			layout.Min(0),
		).Split(r)

		areas = append(areas, cols[:5]...) // ignore Min(0)
	}

	type Named struct {
		Name        string
		Constraints []layout.Constraint
	}

	examples := []Named{
		{
			"Len",
			[]layout.Constraint{
				layout.Len(0),
				layout.Len(2),
				layout.Len(3),
				layout.Len(6),
				layout.Len(10),
				layout.Len(15),
			},
		},
		{
			"Min",
			[]layout.Constraint{
				layout.Min(0),
				layout.Min(2),
				layout.Min(3),
				layout.Min(6),
				layout.Min(10),
				layout.Min(15),
			},
		},
		{
			"Max",
			[]layout.Constraint{
				layout.Max(0),
				layout.Max(2),
				layout.Max(3),
				layout.Max(6),
				layout.Max(10),
				layout.Max(15),
			},
		},
		{
			"Perc",
			[]layout.Constraint{
				layout.Percent(0),
				layout.Percent(25),
				layout.Percent(50),
				layout.Percent(75),
				layout.Percent(100),
				layout.Percent(150),
			},
		},
		{
			"Ratio",
			[]layout.Constraint{
				layout.Ratio{0, 4},
				layout.Ratio{1, 4},
				layout.Ratio{2, 4},
				layout.Ratio{3, 4},
				layout.Ratio{4, 4},
				layout.Ratio{6, 4},
			},
		},
	}

	for i, e := range cartesianProduct(examples, examples) {
		renderExampleCombinations(
			sc,
			areas[i],
			fmt.Sprintf("%s | %s", e.Left.Name, e.Right.Name),
			zip(e.Left.Constraints, e.Right.Constraints),
		)
	}
}

func renderExampleCombinations(
	sc uv.Screen,
	area uv.Rectangle,
	title string,
	constraints []Pair[layout.Constraint, layout.Constraint],
) {
	rows := layout.Vertical(
		slices.Repeat(
			[]layout.Constraint{layout.Len(1)},
			len(constraints)+1,
		)...,
	).
		WithPadding(layout.Pad(1)).
		Split(area)

	screen.NewContext(sc).DrawString(title, rows[0].Min.X, rows[0].Min.Y-1)

	for _, p := range zip(constraints, rows) {
		renderExample(sc, p.Right, p.Left.Left, p.Left.Right, layout.Min(0))
	}

	nums := "123456789012"
	row := rows[len(rows)-1]

	screen.NewContext(sc).DrawString(
		nums[:min(len(nums), row.Dx())],
		row.Min.X,
		row.Min.Y,
	)
}

func renderExample(sc uv.Screen, area uv.Rectangle, constraints ...layout.Constraint) {
	var r, g, b uv.Rectangle

	layout.Horizontal(constraints...).Split(area).Assign(&r, &b, &g)

	screen.FillArea(sc, cell(ansi.Red), r)
	screen.FillArea(sc, cell(ansi.Green), g)
	screen.FillArea(sc, cell(ansi.Blue), b)

	draw := func(bg ansi.Color, s string, r uv.Rectangle) {
		ctx := screen.NewContext(sc).WithStyle(uv.Style{Bg: bg})

		ctx.DrawString(s[:min(len(s), r.Dx())], r.Min.X, r.Min.Y)
	}

	draw(ansi.Red, constraintLabel(constraints[0]), r)
	draw(ansi.Green, strings.Repeat(".", g.Dx()), g)
	draw(ansi.Blue, constraintLabel(constraints[1]), b)
}

func constraintLabel(c layout.Constraint) string {
	switch c := c.(type) {
	case layout.Fill:
		return strconv.Itoa(int(c))

	case layout.Len:
		return strconv.Itoa(int(c))

	case layout.Max:
		return strconv.Itoa(int(c))

	case layout.Min:
		return strconv.Itoa(int(c))

	case layout.Percent:
		return strconv.Itoa(int(c))

	case layout.Ratio:

		return fmt.Sprintf("%d/%d", c.Num, c.Den)
	default:
		return ""
	}
}

func cell(bg ansi.Color) *uv.Cell {
	return &uv.Cell{
		Content: " ",
		Width:   1,
		Style: uv.Style{
			Bg: bg,
		},
	}
}

type Pair[T, U any] struct {
	Left  T
	Right U
}

func zip[T, U any](a []T, b []U) []Pair[T, U] {
	zipped := make([]Pair[T, U], 0, min(len(a), len(b)))

	for i := range min(len(a), len(b)) {
		zipped = append(zipped, Pair[T, U]{Left: a[i], Right: b[i]})
	}

	return zipped
}

func cartesianProduct[T, U any](as []T, bs []U) []Pair[T, U] {
	product := make([]Pair[T, U], 0, len(as)*len(bs))

	for _, a := range as {
		for _, b := range bs {
			product = append(product, Pair[T, U]{Left: a, Right: b})
		}
	}
	return product
}
