// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package flex

import (
	"bytes"
	"fmt"
	"image"
	"image/color"
	"testing"

	"golang.org/x/exp/shiny/unit"
	"golang.org/x/exp/shiny/widget"
	"golang.org/x/exp/shiny/widget/node"
	"golang.org/x/exp/shiny/widget/theme"
)

type layoutTest struct {
	desc         string
	direction    Direction
	wrap         FlexWrap
	alignContent AlignContent
	justify      Justify
	size         image.Point       // size of container
	measured     [][2]float64      // MeasuredSize of child elements
	layoutData   []LayoutData      // LayoutData of child elements
	want         []image.Rectangle // final Rect of child elements
}

func (t *layoutTest) html() string {
	buf := new(bytes.Buffer)
	fmt.Fprintf(buf, `<style>
#container {
	display: flex;
	width:   %dpx;
	height:  %dpx;
`, t.size.X, t.size.Y)

	switch t.direction {
	case Row:
	case RowReverse:
		fmt.Fprintf(buf, "\tflex-direction: row-reverse;\n")
	case Column:
		fmt.Fprintf(buf, "\tflex-direction: column;\n")
	case ColumnReverse:
		fmt.Fprintf(buf, "\tflex-direction: column-reverse;\n")
	}
	switch t.wrap {
	case NoWrap:
	case Wrap:
		fmt.Fprintf(buf, "\tflex-wrap: wrap;\n")
	case WrapReverse:
		fmt.Fprintf(buf, "\tflex-wrap: wrap-reverse;\n")
	}
	switch t.alignContent {
	case AlignContentStart:
	case AlignContentEnd:
		fmt.Fprintf(buf, "\talign-content: flex-end;\n")
	case AlignContentCenter:
		fmt.Fprintf(buf, "\talign-content: center;\n")
	case AlignContentSpaceBetween:
		fmt.Fprintf(buf, "\talign-content: space-between;\n")
	case AlignContentSpaceAround:
		fmt.Fprintf(buf, "\talign-content: space-around;\n")
	case AlignContentStretch:
		fmt.Fprintf(buf, "\talign-content: stretch;\n")
	}
	switch t.justify {
	case JustifyStart:
	case JustifyEnd:
		fmt.Fprintf(buf, "\tjustify-content: flex-end;\n")
	case JustifyCenter:
		fmt.Fprintf(buf, "\tjustify-content: center;\n")
	case JustifySpaceBetween:
		fmt.Fprintf(buf, "\tjustify-content: space-between;\n")
	case JustifySpaceAround:
		fmt.Fprintf(buf, "\tjustify-content: space-around;\n")
	}
	fmt.Fprintf(buf, "}\n")

	for i, m := range t.measured {
		fmt.Fprintf(buf, `#child%d {
	width: %.2fpx;
	height: %.2fpx;
`, i, m[0], m[1])
		c := colors[i%len(colors)]
		fmt.Fprintf(buf, "\tbackground-color: rgb(%d, %d, %d);\n", c.R, c.G, c.B)
		if t.layoutData != nil {
			d := t.layoutData[i]
			if d.MinSize.X != 0 {
				fmt.Fprintf(buf, "\tmin-width: %dpx;\n", d.MinSize.X)
			}
			if d.MinSize.Y != 0 {
				fmt.Fprintf(buf, "\tmin-height: %dpx;\n", d.MinSize.Y)
			}
			if d.MaxSize != nil {
				fmt.Fprintf(buf, "\tmax-width: %dpx;\n", d.MaxSize.X)
				fmt.Fprintf(buf, "\tmax-height: %dpx;\n", d.MaxSize.Y)
			}
			if d.Grow != 0 {
				fmt.Fprintf(buf, "\tflex-grow: %f;\n", d.Grow)
			}
			if d.Shrink != nil {
				fmt.Fprintf(buf, "\tflex-shrink: %f;\n", *d.Shrink)
			}
			// TODO: Basis, Align, BreakAfter
		}
		fmt.Fprintf(buf, "}\n")
	}
	fmt.Fprintf(buf, `</style>
<div id="container">
`)
	for i := range t.measured {
		fmt.Fprintf(buf, "\t<div id=\"child%d\"></div>\n", i)
	}
	fmt.Fprintf(buf, `</div>
<pre id="out"></pre>
<script>
var out = document.getElementById("out");
var container = document.getElementById("container");
for (var i = 0; i < container.children.length; i++) {
	var c = container.children[i];
	var ctop = c.offsetTop - container.offsetTop;
	var cleft = c.offsetLeft - container.offsetLeft;
	var cbottom = ctop + c.offsetHeight;
	var cright = cleft + c.offsetWidth;

	out.innerHTML += "\timage.Rect(" + cleft + ", " + ctop + ", " + cright + ", " + cbottom + "),\n";
}
</script>
`)

	return buf.String()
}

var colors = []color.RGBA{
	{0x00, 0x7f, 0x7f, 0xff}, // Cyan
	{0x7f, 0x00, 0x7f, 0xff}, // Magenta
	{0x7f, 0x7f, 0x00, 0xff}, // Yellow
	{0xff, 0x00, 0x00, 0xff}, // Red
	{0x00, 0xff, 0x00, 0xff}, // Green
	{0x00, 0x00, 0xff, 0xff}, // Blue
}

var layoutTests = []layoutTest{{
	desc: "no children",
}, {
	desc: "no children wrapped",
	wrap: Wrap,
}, {
	desc:     "single unflexed child",
	size:     image.Point{100, 100},
	measured: [][2]float64{{100, 100}},
	want: []image.Rectangle{
		image.Rect(0, 0, 100, 100),
	},
}, {
	desc:     "unflexed children",
	size:     image.Point{350, 100},
	measured: [][2]float64{{100, 100}, {100, 100}, {100, 100}},
	want: []image.Rectangle{
		image.Rect(0, 0, 100, 100),
		image.Rect(100, 0, 200, 100),
		image.Rect(200, 0, 300, 100),
	},
}, {
	desc:     "final child that grows",
	size:     image.Point{300, 100},
	measured: [][2]float64{{100, 100}, {100, 100}},
	want: []image.Rectangle{
		image.Rect(0, 0, 100, 100),
		image.Rect(100, 0, 300, 100),
	},
	layoutData: []LayoutData{{}, {Grow: 1}},
}, {
	desc:     "share growth equally",
	size:     image.Point{300, 100},
	measured: [][2]float64{{50, 50}, {100, 100}, {100, 100}},
	want: []image.Rectangle{
		image.Rect(0, 0, 50, 50),
		image.Rect(50, 0, 175, 100),
		image.Rect(175, 0, 300, 100),
	},
	layoutData: []LayoutData{{}, {Grow: 1}, {Grow: 1}},
}, {
	desc:     "share growth inequally",
	size:     image.Point{300, 100},
	measured: [][2]float64{{20, 100}, {20, 100}, {20, 100}},
	want: []image.Rectangle{
		image.Rect(0, 0, 30, 100),
		image.Rect(30, 0, 130, 100),
		image.Rect(130, 0, 300, 100),
	},
	layoutData: []LayoutData{
		{MaxSize: &image.Point{30, 100}, Grow: 1},
		{MinSize: image.Point{100, 0}, Grow: 1},
		{Grow: 4},
	},
}, {
	desc:     "wrap",
	size:     image.Point{300, 200},
	wrap:     Wrap,
	measured: [][2]float64{{150, 100}, {280, 100}, {20, 100}},
	want: []image.Rectangle{
		image.Rect(0, 0, 30, 100),
		image.Rect(0, 100, 280, 200),
		image.Rect(280, 100, 300, 200),
	},
	layoutData: []LayoutData{
		{MaxSize: &image.Point{30, 100}, Grow: 1},
		{MinSize: image.Point{100, 0}, Grow: 1},
		{Grow: 1},
	},
}, {
	desc:      "align-content default",
	size:      image.Point{300, 200},
	direction: Column,
	wrap:      Wrap,
	measured:  [][2]float64{{150, 100}, {160, 100}, {20, 100}, {300, 300}},
	want: []image.Rectangle{
		image.Rect(0, 0, 30, 100),
		image.Rect(0, 100, 160, 200),
		image.Rect(220, 0, 240, 195),
		image.Rect(220, 195, 225, 200),
	},
	layoutData: []LayoutData{
		{MaxSize: &image.Point{30, 100}, Grow: 1},
		{MinSize: image.Point{100, 0}, Grow: 1},
		{Grow: 1},
		{MaxSize: &image.Point{5, 5}},
	},
}, {
	desc:         "align-content: space-around",
	size:         image.Point{300, 200},
	direction:    Column,
	wrap:         Wrap,
	alignContent: AlignContentSpaceAround,
	measured:     [][2]float64{{150, 100}, {160, 100}, {20, 100}, {300, 300}},
	want: []image.Rectangle{
		image.Rect(30, 0, 60, 100),
		image.Rect(30, 100, 190, 200),
		image.Rect(250, 0, 270, 195),
		image.Rect(250, 195, 255, 200),
	},
	layoutData: []LayoutData{
		{MaxSize: &image.Point{30, 100}, Grow: 1},
		{MinSize: image.Point{100, 0}, Grow: 1},
		{Grow: 1},
		{MaxSize: &image.Point{5, 5}},
	},
}, {
	desc:         "align-content: space-between",
	size:         image.Point{300, 200},
	direction:    Column,
	wrap:         Wrap,
	alignContent: AlignContentSpaceBetween,
	measured:     [][2]float64{{150, 100}, {160, 100}, {20, 100}, {300, 300}},
	want: []image.Rectangle{
		image.Rect(0, 0, 30, 100),
		image.Rect(0, 100, 160, 200),
		image.Rect(280, 0, 300, 195),
		image.Rect(280, 195, 285, 200),
	},
	layoutData: []LayoutData{
		{MaxSize: &image.Point{30, 100}, Grow: 1},
		{MinSize: image.Point{100, 0}, Grow: 1},
		{Grow: 1},
		{MaxSize: &image.Point{5, 5}},
	},
}, {
	desc:         "align-content: end",
	size:         image.Point{300, 200},
	direction:    Column,
	wrap:         Wrap,
	alignContent: AlignContentEnd,
	measured:     [][2]float64{{150, 100}, {160, 100}, {20, 100}, {300, 300}},
	want: []image.Rectangle{
		image.Rect(120, 0, 150, 100),
		image.Rect(120, 100, 280, 200),
		image.Rect(280, 0, 300, 195),
		image.Rect(280, 195, 285, 200),
	},
	layoutData: []LayoutData{
		{MaxSize: &image.Point{30, 100}, Grow: 1},
		{MinSize: image.Point{100, 0}, Grow: 1},
		{Grow: 1},
		{MaxSize: &image.Point{5, 5}},
	},
}, {
	desc:         "align-content: center",
	size:         image.Point{300, 200},
	direction:    Column,
	wrap:         Wrap,
	alignContent: AlignContentCenter,
	measured:     [][2]float64{{150, 100}, {160, 100}, {20, 100}, {300, 300}},
	want: []image.Rectangle{
		image.Rect(60, 0, 90, 100),
		image.Rect(60, 100, 220, 200),
		image.Rect(220, 0, 240, 195),
		image.Rect(220, 195, 225, 200),
	},
	layoutData: []LayoutData{
		{MaxSize: &image.Point{30, 100}, Grow: 1},
		{MinSize: image.Point{100, 0}, Grow: 1},
		{Grow: 1},
		{MaxSize: &image.Point{5, 5}},
	},
}, {
	desc:      "column-reverse",
	size:      image.Point{300, 60},
	direction: ColumnReverse,
	wrap:      Wrap,
	measured:  [][2]float64{{25, 25}, {25, 25}, {25, 25}, {25, 25}, {25, 25}},
	want: []image.Rectangle{
		image.Rect(0, 35, 25, 60),
		image.Rect(0, 0, 25, 35),
		image.Rect(100, 35, 125, 60),
		image.Rect(100, 10, 125, 35),
		image.Rect(200, 0, 225, 60),
	},
	layoutData: []LayoutData{
		{},
		{Grow: 1},
		{},
		{},
		{Grow: 1},
	},
}, {
	desc:     "justify-content: flex-start",
	size:     image.Point{90, 90},
	measured: [][2]float64{{5, 10}, {5, 10}, {10, 10}},
	justify:  JustifyStart,
	want: []image.Rectangle{
		image.Rect(0, 0, 5, 10),
		image.Rect(5, 0, 10, 10),
		image.Rect(10, 0, 20, 10),
	},
}, {
	desc:     "justify-content: flex-end",
	size:     image.Point{90, 90},
	measured: [][2]float64{{5, 10}, {5, 10}, {10, 10}},
	justify:  JustifyEnd,
	want: []image.Rectangle{
		image.Rect(70, 0, 75, 10),
		image.Rect(75, 0, 80, 10),
		image.Rect(80, 0, 90, 10),
	},
}, {
	desc:     "justify-content: center",
	size:     image.Point{90, 90},
	measured: [][2]float64{{5, 10}, {5, 10}, {10, 10}},
	justify:  JustifyCenter,
	want: []image.Rectangle{
		image.Rect(35, 0, 40, 10),
		image.Rect(40, 0, 45, 10),
		image.Rect(45, 0, 55, 10),
	},
}, {
	desc:     "justify-content: space-between",
	size:     image.Point{90, 90},
	measured: [][2]float64{{5, 10}, {5, 10}, {10, 10}},
	justify:  JustifySpaceBetween,
	want: []image.Rectangle{
		image.Rect(0, 0, 5, 10),
		image.Rect(40, 0, 45, 10),
		image.Rect(80, 0, 90, 10),
	},
}, {
	desc:     "justify-content: space-around",
	size:     image.Point{90, 90},
	measured: [][2]float64{{5, 10}, {5, 10}, {10, 10}},
	justify:  JustifySpaceAround,
	want: []image.Rectangle{
		image.Rect(12, 0, 17, 10),
		image.Rect(40, 0, 45, 10),
		image.Rect(68, 0, 78, 10),
	},
}}

func TestLayout(t *testing.T) {
	for testNum, test := range layoutTests {
		var children []node.Node
		for i, sz := range test.measured {
			u := widget.NewUniform(theme.StaticColor(colors[i]), nil)
			n := widget.NewSizer(unit.Pixels(sz[0]), unit.Pixels(sz[1]), u)
			if test.layoutData != nil {
				n.LayoutData = test.layoutData[i]
			}
			children = append(children, n)
		}

		w := NewFlex(children...)
		w.Direction = test.direction
		w.Wrap = test.wrap
		w.AlignContent = test.alignContent
		w.Justify = test.justify

		w.Measure(nil, node.NoHint, node.NoHint)
		w.Rect = image.Rectangle{Max: test.size}
		w.Layout(nil)

		bad := false
		for i, n := range children {
			if n.Wrappee().Rect != test.want[i] {
				bad = true
				break
			}
		}
		if bad {
			t.Logf("Bad test %d, %q:\n%s", testNum, test.desc, test.html())
		}
		for i, n := range children {
			if got, want := n.Wrappee().Rect, test.want[i]; got != want {
				t.Errorf("[%d].Rect=%v, want %v", i, got, want)
			}
		}
	}
}
