// 25 february 2014

package ui

import (
	"fmt"
)

// A Grid arranges Controls in a two-dimensional grid.
// The height of each row and the width of each column is the maximum preferred height and width (respectively) of all the controls in that row or column (respectively).
// Controls are aligned to the top left corner of each cell.
// All Controls in a Grid maintain their preferred sizes by default; if a Control is marked as being "filling", it will be sized to fill its cell.
// Even if a Control is marked as filling, its preferred size is used to calculate cell sizes.
// One Control can be marked as "stretchy": when the Window containing the Grid is resized, the cell containing that Control resizes to take any remaining space; its row and column are adjusted accordingly (so other filling controls in the same row and column will fill to the new height and width, respectively).
// A stretchy Control implicitly fills its cell.
// All cooridnates in a Grid are given in (row,column) form with (0,0) being the top-left cell.
type Grid struct {
	created                  bool
	controls                 [][]Control
	filling                  [][]bool
	stretchyrow, stretchycol int
	widths, heights          [][]int // caches to avoid reallocating each time
	rowheights, colwidths    []int
}

// NewGrid creates a new Grid with the given Controls.
// NewGrid needs to know the number of Controls in a row (alternatively, the number of columns); it will determine the number in a column from the number of Controls given.
// NewGrid panics if not given a full grid of Controls.
// Example:
// 	grid := NewGrid(3,
// 		control00, control01, control02,
// 		control10, control11, control12,
// 		control20, control21, control22)
func NewGrid(nPerRow int, controls ...Control) *Grid {
	if len(controls)%nPerRow != 0 {
		panic(fmt.Errorf("incomplete grid given to NewGrid() (not enough controls to evenly divide %d controls into rows of %d controls each)", len(controls), nPerRow))
	}
	nRows := len(controls) / nPerRow
	cc := make([][]Control, nRows)
	cf := make([][]bool, nRows)
	cw := make([][]int, nRows)
	ch := make([][]int, nRows)
	i := 0
	for row := 0; row < nRows; row++ {
		cc[row] = make([]Control, nPerRow)
		cf[row] = make([]bool, nPerRow)
		cw[row] = make([]int, nPerRow)
		ch[row] = make([]int, nPerRow)
		for x := 0; x < nPerRow; x++ {
			cc[row][x] = controls[i]
			i++
		}
	}
	return &Grid{
		controls:    cc,
		filling:     cf,
		stretchyrow: -1,
		stretchycol: -1,
		widths:      cw,
		heights:     ch,
		rowheights:  make([]int, nRows),
		colwidths:   make([]int, nPerRow),
	}
}

// SetFilling marks the given Control of the Grid as filling its cell instead of staying at its preferred size.
// This function cannot be called after the Window that contains the Grid has been created.
// It panics if the given coordinate is invalid.
func (g *Grid) SetFilling(row int, column int) {
	if g.created {
		panic(fmt.Errorf("Grid.SetFilling() called after window create"))
	}
	if row < 0 || column < 0 || row > len(g.filling) || column > len(g.filling[row]) {
		panic(fmt.Errorf("coordinate (%d,%d) out of range passed to Grid.SetFilling()", row, column))
	}
	g.filling[row][column] = true
}

// SetStretchy marks the given Control of the Grid as stretchy.
// Stretchy implies filling.
// Only one control can be stretchy per Grid; calling SetStretchy multiple times merely changes which control is stretchy.
// This function cannot be called after the Window that contains the Grid has been created.
// It panics if the given coordinate is invalid.
func (g *Grid) SetStretchy(row int, column int) {
	if g.created {
		panic(fmt.Errorf("Grid.SetFilling() called after window create"))
	}
	if row < 0 || column < 0 || row > len(g.filling) || column > len(g.filling[row]) {
		panic(fmt.Errorf("coordinate (%d,%d) out of range passed to Grid.SetStretchy()", row, column))
	}
	g.stretchyrow = row
	g.stretchycol = column
	// don't set filling here in case we call SetStretchy() multiple times; the filling is committed in make() below
}

func (g *Grid) make(window *sysData) error {
	// commit filling for the stretchy control now (see SetStretchy() above)
	if g.stretchyrow != -1 && g.stretchycol != -1 {
		g.filling[g.stretchyrow][g.stretchycol] = true
	} else if (g.stretchyrow == -1 && g.stretchycol != -1) || // sanity check
		(g.stretchyrow != -1 && g.stretchycol == -1) {
		panic(fmt.Errorf("internal inconsistency in Grid: stretchy (%d,%d) impossible (one component, not both, is -1/no stretchy control) in Grid.make()", g.stretchyrow, g.stretchycol))
	}
	for row, xcol := range g.controls {
		for col, c := range xcol {
			err := c.make(window)
			if err != nil {
				return fmt.Errorf("error adding control (%d,%d) to Grid: %v", row, col, err)
			}
		}
	}
	g.created = true
	return nil
}

func (g *Grid) allocate(x int, y int, width int, height int, d *sysSizeData) (allocations []*allocation) {
	max := func(a int, b int) int {
		if a > b {
			return a
		}
		return b
	}

	var current *allocation		// for neighboring

	// TODO return if nControls == 0?
	// before we do anything, steal the margin so nested Stacks/Grids don't double down
	xmargin := d.xmargin
	ymargin := d.ymargin
	d.xmargin = 0
	d.ymargin = 0
	// 0) inset the available rect by the margins and needed padding
	x += xmargin
	y += ymargin
	width -= xmargin * 2
	height -= ymargin * 2
	width -= (len(g.colwidths) - 1) * d.xpadding
	height -= (len(g.rowheights) - 1) * d.ypadding
	// 1) clear data structures
	for i := range g.rowheights {
		g.rowheights[i] = 0
	}
	for i := range g.colwidths {
		g.colwidths[i] = 0
	}
	// 2) get preferred sizes; compute row/column sizes
	for row, xcol := range g.controls {
		for col, c := range xcol {
			w, h := c.preferredSize(d)
			g.widths[row][col] = w
			g.heights[row][col] = h
			g.rowheights[row] = max(g.rowheights[row], h)
			g.colwidths[col] = max(g.colwidths[col], w)
		}
	}
	// 3) handle the stretchy control
	if g.stretchyrow != -1 && g.stretchycol != -1 {
		for i, w := range g.colwidths {
			if i != g.stretchycol {
				width -= w
			}
		}
		for i, h := range g.rowheights {
			if i != g.stretchyrow {
				height -= h
			}
		}
		g.colwidths[g.stretchycol] = width
		g.rowheights[g.stretchyrow] = height
	}
	// 4) draw
	startx := x
	for row, xcol := range g.controls {
		current = nil		// reset on new columns
		for col, c := range xcol {
			w := g.widths[row][col]
			h := g.heights[row][col]
			if g.filling[row][col] {
				w = g.colwidths[col]
				h = g.rowheights[row]
			}
			as := c.allocate(x, y, w, h, d)
			if current != nil {			// connect first left to first right
				current.neighbor = c
			}
			if len(as) != 0 {
				current = as[0]			// next left is first subwidget
			} else {
				current = nil			// spaces don't have allocation data
			}
			allocations = append(allocations, as...)
			x += g.colwidths[col] + d.xpadding
		}
		x = startx
		y += g.rowheights[row] + d.ypadding
	}
	return
}

// filling and stretchy are ignored for preferred size calculation
// We don't consider the margins here, but will need to if Window.SizeToFit() is ever made a thing.
func (g *Grid) preferredSize(d *sysSizeData) (width int, height int) {
	max := func(a int, b int) int {
		if a > b {
			return a
		}
		return b
	}

	width -= (len(g.colwidths) - 1) * d.xpadding
	height -= (len(g.rowheights) - 1) * d.ypadding
	// 1) clear data structures
	for i := range g.rowheights {
		g.rowheights[i] = 0
	}
	for i := range g.colwidths {
		g.colwidths[i] = 0
	}
	// 2) get preferred sizes; compute row/column sizes
	for row, xcol := range g.controls {
		for col, c := range xcol {
			w, h := c.preferredSize(d)
			g.widths[row][col] = w
			g.heights[row][col] = h
			g.rowheights[row] = max(g.rowheights[row], h)
			g.colwidths[col] = max(g.colwidths[col], w)
		}
	}
	// 3) now compute
	for _, w := range g.colwidths {
		width += w
	}
	for _, h := range g.rowheights {
		height += h
	}
	return width, height
}

func (g *Grid) commitResize(c *allocation, d *sysSizeData) {
	// this is to satisfy Control; nothing to do here
}

func (g *Grid) getAuxResizeInfo(d *sysSizeData) {
	// this is to satisfy Control; nothing to do here
}