// Command todo is the todo example, written against the widget layer.
//
// It exercises the framework end to end: stateful widgets with keyed
// reconciliation, per-row hover animations (anim.Controller tickers),
// tap-to-focus with focus-aware visuals, a scrolling viewport, and text
// input — all testable headless (see render_test.go).
package main

import (
	"fmt"
	"log"
	"strings"
	"time"

	"golang.org/x/image/font/gofont/goregular"

	"github.com/doug/gophics/anim"
	"github.com/doug/gophics/app"
	"github.com/doug/gophics/geom"
	"github.com/doug/gophics/layout"
	"github.com/doug/gophics/paint"
	"github.com/doug/gophics/theme"
	"github.com/doug/gophics/widget"
)

// BG is the window background used at Start, before a widget context exists
// (passed as Config.Background). Inside the tree every color comes from
// theme.Of(ctx), so the app follows the platform light/dark scheme for free.
var BG = theme.Light().Bg

type item struct {
	text string
	done bool
}

// Todo is the root widget.
type Todo struct{}

func (Todo) CreateState() widget.State { return &todoState{} }

type todoState struct {
	widget.StateBase[Todo]
	items []item
	input string
	hover int // last hovered row index (observed by tests)
	// editing is the row being edited, or -1. It lives here rather than in the
	// row because closing it is not the row's business: a tap on the page
	// background has to end the edit, and gophics leaves focus alone when a
	// press lands on nothing focusable, so there is no blur event to hang it
	// on. Keeping it here also makes two editors at once unrepresentable.
	editing int
	draft   string
}

// stateHook lets tests observe the mounted state.
var stateHook func(*todoState)

func (s *todoState) Init(widget.Ctx) {
	if stateHook != nil {
		stateHook(s)
	}
	s.hover = -1
	s.editing = -1
	s.items = []item{
		{text: "Plan the port", done: true},
		{text: "Open a window", done: true},
		{text: "Build the todo example", done: true},
		{text: "Grow widgets until this file shrinks", done: true},
		{text: "Animate, focus, scroll", done: true},
		{text: "Ship the web shell", done: false},
	}
}

func (s *todoState) Build(ctx widget.Ctx) widget.Widget {
	// Resolve the theme from the platform color scheme and provide it to the
	// tree, so every widget below reads colors with theme.Of(ctx) and the whole
	// app follows light/dark automatically.
	th := theme.Auto(ctx)
	left := 0
	rows := make([]widget.Widget, 0, 2*len(s.items))
	for i, it := range s.items {
		if !it.done {
			left++
		}
		rows = append(rows, widget.WithKey{Key: it.text, Child: widget.Dismissible{
			OnDismissed: func() { s.SetState(func() { s.remove(i) }) },
			Background: widget.Fill{Color: th.Danger, Child: widget.Padding{
				Insets: geom.Insets{Left: 16, Right: 16},
				Child: widget.Row(
					widget.Text{Value: "delete", Size: th.Type.Label, Color: th.OnPrimary},
					widget.Expand(widget.Sized{}),
					widget.Text{Value: "delete", Size: th.Type.Label, Color: th.OnPrimary},
				),
			}},
			Child: todoRow{
				Item:    it,
				Editing: s.editing == i,
				Draft:   s.draft,
				// Toggling also ends an open edit, so clicking another row
				// commits the one in progress instead of leaving it behind.
				OnToggle: func() { s.SetState(func() { s.commitEdit(); s.items[i].done = !s.items[i].done }) },
				OnDelete: func() { s.SetState(func() { s.remove(i) }) },
				OnBegin:  func() { s.SetState(func() { s.editing, s.draft = i, s.items[i].text }) },
				OnDraft:  func(v string) { s.SetState(func() { s.draft = v }) },
				OnCommit: func() { s.SetState(s.commitEdit) },
				OnHover:  func() { s.hover = i },
				OnLeave:  func() { s.leave(i) },
			},
		}}, widget.Sized{H: 8})
	}
	list := widget.Column(rows...)
	list.CrossAlign = layout.CrossStretch

	footer := fmt.Sprintf("%d left · click toggles · double-click edits · hover shows delete", left)
	col := widget.Column(
		widget.Text{Value: "gophics · todo", Size: th.Type.Body, Color: th.Muted},
		widget.Sized{H: 16},
		theme.Field{
			Value:       s.input,
			Placeholder: "What needs doing?  (Enter adds)",
			OnChange:    func(v string) { s.SetState(func() { s.input = v }) },
			OnSubmit:    s.submit,
		},
		widget.Sized{H: 12},
		widget.Expand(widget.Scroll{Child: list}),
		widget.Sized{H: 12},
		widget.Text{Value: footer, Size: th.Type.Caption, Color: th.Muted},
	)
	col.CrossAlign = layout.CrossStretch
	// A tap that reaches the page background ends an open edit. gophics leaves
	// keyboard focus alone when a press lands on nothing focusable, so the
	// field itself never hears about the click — the page has to notice.
	var page widget.Widget = widget.Fill{Color: th.Bg, Child: widget.Padding{All: 20, Child: col}}
	if s.editing >= 0 {
		page = widget.Interactive{
			Gestures: widget.Gestures{OnTap: func() { s.SetState(s.commitEdit) }},
			Child:    page,
		}
	}
	return widget.Provide[theme.Theme]{Value: th, Child: page}
}

// todoRow is one list row, with its own animated hover state.
type todoRow struct {
	Item     item
	Editing  bool
	Draft    string
	OnToggle func()
	OnDelete func()
	OnBegin  func()
	OnDraft  func(string)
	OnCommit func()
	OnHover  func()
	OnLeave  func()
}

func (r todoRow) CreateState() widget.State { return &rowState{} }

type rowState struct {
	widget.StateBase[todoRow]
	ctx     widget.Ctx
	hover   *anim.Controller
	hovered bool
}

func (s *rowState) Init(ctx widget.Ctx) {
	s.ctx = ctx
	s.hover = &anim.Controller{
		Duration: 120 * time.Millisecond,
		OnChange: func() { s.SetState(nil) },
	}
	ctx.AddTicker(s.hover)
}

func (s *rowState) Dispose() { s.ctx.RemoveTicker(s.hover) }

func (s *rowState) Build(ctx widget.Ctx) widget.Widget {
	th := theme.Of(ctx)
	r := s.W()
	bg := paint.Lerp(th.Surface, th.SurfaceHover, s.hover.Value())

	// Editing replaces the whole row rather than just the label: the row's own
	// tap-to-toggle and swipe-to-delete would otherwise still be live under a
	// text field, so dragging to select a word would delete the item.
	if r.Editing {
		return widget.Decorated{
			Color: bg, Radius: 8,
			Child: widget.Padding{
				Insets: geom.InsetsSymmetric(6, 12),
				Child: theme.Field{
					Value:     r.Draft,
					Autofocus: true,
					OnChange:  r.OnDraft,
					OnSubmit:  func(string) { r.OnCommit() },
				},
			},
		}
	}

	text := widget.Text{Value: r.Item.text, Color: th.Text}
	if r.Item.done {
		text.Color, text.Strike = th.Muted, true
	}
	// The double-click target is the text itself, not the row. Setting
	// OnDoubleTap defers that handler's single tap by the double-tap window to
	// disambiguate, and paying that delay on the checkbox — the one control
	// that should feel instant — to buy editing on the label is a bad trade.
	// Scoped here, a click on the box or the empty right-hand side still
	// toggles immediately.
	var label widget.Widget = widget.Interactive{
		Gestures: widget.Gestures{
			OnTap:       r.OnToggle,
			OnDoubleTap: r.OnBegin,
		},
		Child: text,
	}
	var del widget.Widget = widget.Sized{W: 20, H: 20}
	if s.hovered {
		del = widget.Interactive{
			Gestures: widget.Gestures{OnTap: r.OnDelete},
			Child:    widget.Text{Value: "×", Size: 16, Color: th.Danger},
		}
	}
	return widget.Interactive{
		Gestures: widget.Gestures{
			OnTap: r.OnToggle,
			OnEnter: func() {
				r.OnHover()
				s.SetState(func() { s.hovered = true })
				s.hover.Forward()
				s.ctx.Invalidate()
			},
			OnExit: func() {
				r.OnLeave()
				s.SetState(func() { s.hovered = false })
				s.hover.Reverse()
				s.ctx.Invalidate()
			},
		},
		Child: widget.Decorated{
			Color: bg, Radius: 8,
			Child: widget.Padding{
				Insets: geom.InsetsSymmetric(12, 12),
				Child: widget.Row(
					checkbox(th, r.Item.done),
					widget.Sized{W: 12},
					label,
					widget.Expand(widget.Sized{}),
					del,
				),
			},
		},
	}
}

func checkbox(th theme.Theme, done bool) widget.Widget {
	if done {
		return widget.Decorated{
			Color: th.Primary, Radius: 5,
			Child: widget.Canvas{W: 20, H: 20, Draw: func(c paint.Canvas, size geom.Size) {
				r := geom.Rect{Max: size.Pt()}
				c.Line(r.Min.Add(geom.Pt{X: 4, Y: 10}), r.Min.Add(geom.Pt{X: 8, Y: 14}), 2, th.OnPrimary)
				c.Line(r.Min.Add(geom.Pt{X: 8, Y: 14}), r.Min.Add(geom.Pt{X: 16, Y: 5}), 2, th.OnPrimary)
			}},
		}
	}
	return widget.Decorated{
		BorderColor: th.Border, BorderWidth: 1.5, Radius: 5,
		Child: widget.Sized{W: 20, H: 20},
	}
}

func (s *todoState) submit(v string) {
	if t := strings.TrimSpace(v); t != "" {
		s.SetState(func() {
			s.items = append(s.items, item{text: t})
			s.input = ""
		})
	}
}

// commitEdit closes any open editor, keeping the draft if it says anything.
// An empty draft is "leave it alone", not a delete: deleting is the swipe or
// the ×, and losing a todo to a stray double-click and a held backspace would
// be a nasty surprise. Callers already hold SetState.
func (s *todoState) commitEdit() {
	if s.editing < 0 {
		return
	}
	if text := strings.TrimSpace(s.draft); text != "" && s.editing < len(s.items) {
		s.items[s.editing].text = text
	}
	s.editing, s.draft = -1, ""
}

func (s *todoState) remove(i int) {
	s.items = append(s.items[:i], s.items[i+1:]...)
	s.hover = -1
}

func (s *todoState) leave(i int) {
	if s.hover == i {
		s.hover = -1
	}
}

func main() {
	err := app.Run(Todo{}, app.Config{
		Title:      "gophics todo",
		Size:       geom.Size{W: 440, H: 560},
		Background: BG,
		Font:       goregular.TTF,
	})
	if err != nil {
		log.Fatal(err)
	}
}