// SPDX-License-Identifier: Unlicense OR MIT

package layout

import (
	"image"

	"gioui.org/f32"
	"gioui.org/op"
	"gioui.org/unit"
)

// Constraints represent the minimum and maximum size of a widget.
//
// A widget does not have to treat its constraints as "hard". For
// example, if it's passed a constraint with a minimum size that's
// smaller than its actual minimum size, it should return its minimum
// size dimensions instead. Parent widgets should deal appropriately
// with child widgets that return dimensions that do not fit their
// constraints (for example, by clipping).
type Constraints struct {
	Min, Max image.Point
}

// Dimensions are the resolved size and baseline for a widget.
//
// Baseline is the distance from the bottom of a widget to the baseline of
// any text it contains (or 0). The purpose is to be able to align text
// that span multiple widgets.
type Dimensions struct {
	Size     image.Point
	Baseline int
}

// Axis is the Horizontal or Vertical direction.
type Axis uint8

// Alignment is the mutual alignment of a list of widgets.
type Alignment uint8

// Direction is the alignment of widgets relative to a containing
// space.
type Direction uint8

// Widget is a function scope for drawing, processing events and
// computing dimensions for a user interface element.
type Widget func(gtx Context) Dimensions

const (
	Start Alignment = iota
	End
	Middle
	Baseline
)

const (
	NW Direction = iota
	N
	NE
	E
	SE
	S
	SW
	W
	Center
)

const (
	Horizontal Axis = iota
	Vertical
)

// Exact returns the Constraints with the minimum and maximum size
// set to size.
func Exact(size image.Point) Constraints {
	return Constraints{
		Min: size, Max: size,
	}
}

// FPt converts an point to a f32.Point.
func FPt(p image.Point) f32.Point {
	return f32.Point{
		X: float32(p.X), Y: float32(p.Y),
	}
}

// Constrain a size so each dimension is in the range [min;max].
func (c Constraints) Constrain(size image.Point) image.Point {
	if min := c.Min.X; size.X < min {
		size.X = min
	}
	if min := c.Min.Y; size.Y < min {
		size.Y = min
	}
	if max := c.Max.X; size.X > max {
		size.X = max
	}
	if max := c.Max.Y; size.Y > max {
		size.Y = max
	}
	return size
}

// AddMin returns a copy of Constraints with the Min constraint enlarged by up to delta
// while still fitting within the Max constraint. The Max is unchanged, and the Min constraint
// will not go negative.
func (c Constraints) AddMin(delta image.Point) Constraints {
	c.Min = c.Min.Add(delta)
	if c.Min.X < 0 {
		c.Min.X = 0
	}
	if c.Min.Y < 0 {
		c.Min.Y = 0
	}
	c.Min = c.Constrain(c.Min)
	return c
}

// SubMax returns a copy of Constraints with the Max constraint shrunk by up to delta
// while not going negative. The values of delta are expected to be positive.
// The Min constraint is adjusted to fit within the new Max constraint.
func (c Constraints) SubMax(delta image.Point) Constraints {
	c.Max = c.Max.Sub(delta)
	if c.Max.X < 0 {
		c.Max.X = 0
	}
	if c.Max.Y < 0 {
		c.Max.Y = 0
	}
	c.Min = c.Constrain(c.Min)
	return c
}

// Inset adds space around a widget by decreasing its maximum
// constraints. The minimum constraints will be adjusted to ensure
// they do not exceed the maximum.
type Inset struct {
	Top, Bottom, Left, Right unit.Dp
}

// Layout a widget.
func (in Inset) Layout(gtx Context, w Widget) Dimensions {
	top := gtx.Dp(in.Top)
	right := gtx.Dp(in.Right)
	bottom := gtx.Dp(in.Bottom)
	left := gtx.Dp(in.Left)
	mcs := gtx.Constraints
	mcs.Max.X -= left + right
	if mcs.Max.X < 0 {
		left = 0
		right = 0
		mcs.Max.X = 0
	}
	if mcs.Min.X > mcs.Max.X {
		mcs.Min.X = mcs.Max.X
	}
	mcs.Max.Y -= top + bottom
	if mcs.Max.Y < 0 {
		bottom = 0
		top = 0
		mcs.Max.Y = 0
	}
	if mcs.Min.Y > mcs.Max.Y {
		mcs.Min.Y = mcs.Max.Y
	}
	gtx.Constraints = mcs
	trans := op.Offset(image.Pt(left, top)).Push(gtx.Ops)
	dims := w(gtx)
	trans.Pop()
	return Dimensions{
		Size:     dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
		Baseline: dims.Baseline + bottom,
	}
}

// UniformInset returns an Inset with a single inset applied to all
// edges.
func UniformInset(v unit.Dp) Inset {
	return Inset{Top: v, Right: v, Bottom: v, Left: v}
}

// Layout a widget according to the direction.
// The widget is called with the context constraints minimum cleared.
func (d Direction) Layout(gtx Context, w Widget) Dimensions {
	macro := op.Record(gtx.Ops)
	csn := gtx.Constraints.Min
	switch d {
	case N, S:
		gtx.Constraints.Min.Y = 0
	case E, W:
		gtx.Constraints.Min.X = 0
	default:
		gtx.Constraints.Min = image.Point{}
	}
	dims := w(gtx)
	call := macro.Stop()
	sz := dims.Size
	if sz.X < csn.X {
		sz.X = csn.X
	}
	if sz.Y < csn.Y {
		sz.Y = csn.Y
	}

	p := d.Position(dims.Size, sz)
	defer op.Offset(p).Push(gtx.Ops).Pop()
	call.Add(gtx.Ops)

	return Dimensions{
		Size:     sz,
		Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
	}
}

// Position calculates widget position according to the direction.
func (d Direction) Position(widget, bounds image.Point) image.Point {
	var p image.Point

	switch d {
	case N, S, Center:
		p.X = (bounds.X - widget.X) / 2
	case NE, SE, E:
		p.X = bounds.X - widget.X
	}

	switch d {
	case W, Center, E:
		p.Y = (bounds.Y - widget.Y) / 2
	case SW, S, SE:
		p.Y = bounds.Y - widget.Y
	}

	return p
}

// Spacer adds space between widgets.
type Spacer struct {
	Width, Height unit.Dp
}

func (s Spacer) Layout(gtx Context) Dimensions {
	return Dimensions{
		Size: gtx.Constraints.Constrain(image.Point{
			X: gtx.Dp(s.Width),
			Y: gtx.Dp(s.Height),
		}),
	}
}

func (a Alignment) String() string {
	switch a {
	case Start:
		return "Start"
	case End:
		return "End"
	case Middle:
		return "Middle"
	case Baseline:
		return "Baseline"
	default:
		panic("unreachable")
	}
}

// Convert a point in (x, y) coordinates to (main, cross) coordinates,
// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged
// for the horizontal axis, or (y, x) for the vertical axis.
func (a Axis) Convert(pt image.Point) image.Point {
	if a == Horizontal {
		return pt
	}
	return image.Pt(pt.Y, pt.X)
}

// FConvert a point in (x, y) coordinates to (main, cross) coordinates,
// or vice versa. Specifically, FConvert((x, y)) returns (x, y) unchanged
// for the horizontal axis, or (y, x) for the vertical axis.
func (a Axis) FConvert(pt f32.Point) f32.Point {
	if a == Horizontal {
		return pt
	}
	return f32.Pt(pt.Y, pt.X)
}

// mainConstraint returns the min and max main constraints for axis a.
func (a Axis) mainConstraint(cs Constraints) (int, int) {
	if a == Horizontal {
		return cs.Min.X, cs.Max.X
	}
	return cs.Min.Y, cs.Max.Y
}

// crossConstraint returns the min and max cross constraints for axis a.
func (a Axis) crossConstraint(cs Constraints) (int, int) {
	if a == Horizontal {
		return cs.Min.Y, cs.Max.Y
	}
	return cs.Min.X, cs.Max.X
}

// constraints returns the constraints for axis a.
func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
	if a == Horizontal {
		return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)}
	}
	return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)}
}

func (a Axis) String() string {
	switch a {
	case Horizontal:
		return "Horizontal"
	case Vertical:
		return "Vertical"
	default:
		panic("unreachable")
	}
}

func (d Direction) String() string {
	switch d {
	case NW:
		return "NW"
	case N:
		return "N"
	case NE:
		return "NE"
	case E:
		return "E"
	case SE:
		return "SE"
	case S:
		return "S"
	case SW:
		return "SW"
	case W:
		return "W"
	case Center:
		return "Center"
	default:
		panic("unreachable")
	}
}
