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

// +build windows

package walk

import (
	"math"
	"sort"
	"sync"

	"github.com/lxn/win"
)

type Orientation byte

const (
	NoOrientation Orientation = 0
	Horizontal                = 1 << 0
	Vertical                  = 1 << 1
)

type BoxLayout struct {
	LayoutBase
	orientation        Orientation
	hwnd2StretchFactor map[win.HWND]int
}

func newBoxLayout(orientation Orientation) *BoxLayout {
	l := &BoxLayout{
		LayoutBase: LayoutBase{
			margins96dpi: Margins{9, 9, 9, 9},
			spacing96dpi: 6,
		},
		orientation:        orientation,
		hwnd2StretchFactor: make(map[win.HWND]int),
	}
	l.layout = l

	return l
}

func NewHBoxLayout() *BoxLayout {
	return newBoxLayout(Horizontal)
}

func NewVBoxLayout() *BoxLayout {
	return newBoxLayout(Vertical)
}

func (l *BoxLayout) Orientation() Orientation {
	return l.orientation
}

func (l *BoxLayout) SetOrientation(value Orientation) error {
	if value != l.orientation {
		switch value {
		case Horizontal, Vertical:

		default:
			return newError("invalid Orientation value")
		}

		l.orientation = value

		l.container.RequestLayout()
	}

	return nil
}

func (l *BoxLayout) StretchFactor(widget Widget) int {
	if factor, ok := l.hwnd2StretchFactor[widget.Handle()]; ok {
		return factor
	}

	return 1
}

func (l *BoxLayout) SetStretchFactor(widget Widget, factor int) error {
	if factor != l.StretchFactor(widget) {
		if l.container == nil {
			return newError("container required")
		}

		handle := widget.Handle()

		if !l.container.Children().containsHandle(handle) {
			return newError("unknown widget")
		}
		if factor < 1 {
			return newError("factor must be >= 1")
		}

		l.hwnd2StretchFactor[handle] = factor

		l.container.RequestLayout()
	}

	return nil
}

func (l *BoxLayout) CreateLayoutItem(ctx *LayoutContext) ContainerLayoutItem {
	li := &boxLayoutItem{
		size2MinSize:       make(map[Size]Size),
		orientation:        l.orientation,
		hwnd2StretchFactor: make(map[win.HWND]int),
	}

	for hwnd, sf := range l.hwnd2StretchFactor {
		li.hwnd2StretchFactor[hwnd] = sf
	}

	return li
}

type boxLayoutItemInfo struct {
	item     LayoutItem
	index    int
	prefSize int // in native pixels
	minSize  int // in native pixels
	maxSize  int // in native pixels
	stretch  int
	greedy   bool
}

type boxLayoutItemInfoList []boxLayoutItemInfo

func (l boxLayoutItemInfoList) Len() int {
	return len(l)
}

func (l boxLayoutItemInfoList) Less(i, j int) bool {
	_, iIsSpacer := l[i].item.(*spacerLayoutItem)
	_, jIsSpacer := l[j].item.(*spacerLayoutItem)

	if l[i].greedy == l[j].greedy {
		if iIsSpacer == jIsSpacer {
			minDiff := l[i].minSize - l[j].minSize

			if minDiff == 0 {
				return l[i].maxSize/l[i].stretch < l[j].maxSize/l[j].stretch
			}

			return minDiff > 0
		}

		return jIsSpacer
	}

	return l[i].greedy
}

func (l boxLayoutItemInfoList) Swap(i, j int) {
	l[i], l[j] = l[j], l[i]
}

type boxLayoutItem struct {
	ContainerLayoutItemBase
	mutex              sync.Mutex
	size2MinSize       map[Size]Size // in native pixels
	orientation        Orientation
	hwnd2StretchFactor map[win.HWND]int
}

func (li *boxLayoutItem) LayoutFlags() LayoutFlags {
	return boxLayoutFlags(li.orientation, li.children)
}

func (li *boxLayoutItem) IdealSize() Size {
	return li.MinSize()
}

func (li *boxLayoutItem) MinSize() Size {
	return li.MinSizeForSize(li.geometry.ClientSize)
}

func (li *boxLayoutItem) HeightForWidth(width int) int {
	return li.MinSizeForSize(Size{width, li.geometry.ClientSize.Height}).Height
}

func (li *boxLayoutItem) MinSizeForSize(size Size) Size {
	li.mutex.Lock()
	defer li.mutex.Unlock()

	if min, ok := li.size2MinSize[size]; ok {
		return min
	}

	bounds := Rectangle{Width: size.Width, Height: size.Height}

	items := boxLayoutItems(li, itemsToLayout(li.children), li.orientation, li.alignment, bounds, li.margins96dpi, li.spacing96dpi, li.hwnd2StretchFactor)

	margins := MarginsFrom96DPI(li.margins96dpi, li.ctx.dpi)
	spacing := IntFrom96DPI(li.spacing96dpi, li.ctx.dpi)
	s := Size{margins.HNear + margins.HFar, margins.VNear + margins.VFar}

	var maxSecondary int
	for _, item := range items {
		min := li.MinSizeEffectiveForChild(item.Item)

		if hfw, ok := item.Item.(HeightForWidther); ok && hfw.HasHeightForWidth() {
			item.Bounds.Height = hfw.HeightForWidth(item.Bounds.Width)
		} else {
			item.Bounds.Height = min.Height
		}
		item.Bounds.Width = min.Width

		if li.orientation == Horizontal {
			maxSecondary = maxi(maxSecondary, item.Bounds.Height)

			s.Width += item.Bounds.Width
		} else {
			maxSecondary = maxi(maxSecondary, item.Bounds.Width)

			s.Height += item.Bounds.Height
		}
	}

	if li.orientation == Horizontal {
		s.Width += (len(items) - 1) * spacing
		s.Height += maxSecondary
	} else {
		s.Height += (len(items) - 1) * spacing
		s.Width += maxSecondary
	}

	if s.Width > 0 && s.Height > 0 {
		li.size2MinSize[size] = s
	}

	return s
}

func (li *boxLayoutItem) PerformLayout() []LayoutResultItem {
	cb := Rectangle{Width: li.geometry.ClientSize.Width, Height: li.geometry.ClientSize.Height}
	return boxLayoutItems(li, itemsToLayout(li.children), li.orientation, li.alignment, cb, li.margins96dpi, li.spacing96dpi, li.hwnd2StretchFactor)
}

func boxLayoutFlags(orientation Orientation, children []LayoutItem) LayoutFlags {
	if len(children) == 0 {
		return ShrinkableHorz | ShrinkableVert | GrowableHorz | GrowableVert
	}

	var flags LayoutFlags
	for i := 0; i < len(children); i++ {
		item := children[i]

		if _, ok := item.(*splitterHandleLayoutItem); ok || !shouldLayoutItem(item) {
			continue
		}

		if s, ok := item.(*spacerLayoutItem); ok {
			if s.greedyLocallyOnly {
				continue
			}
		}

		f := item.LayoutFlags()
		flags |= f
	}

	return flags
}

// boxLayoutItems lays out items. bounds parameter is in native pixels.
func boxLayoutItems(container ContainerLayoutItem, items []LayoutItem, orientation Orientation, alignment Alignment2D, bounds Rectangle, margins96dpi Margins, spacing96dpi int, hwnd2StretchFactor map[win.HWND]int) []LayoutResultItem {
	if len(items) == 0 {
		return nil
	}

	dpi := container.Context().dpi
	margins := MarginsFrom96DPI(margins96dpi, dpi)
	spacing := IntFrom96DPI(spacing96dpi, dpi)

	var greedyNonSpacerCount int
	var greedySpacerCount int
	var stretchFactorsTotal [3]int
	stretchFactors := make([]int, len(items))
	var minSizesRemaining int
	minSizes := make([]int, len(items))
	maxSizes := make([]int, len(items))
	sizes := make([]int, len(items))
	prefSizes2 := make([]int, len(items))
	var shrinkableAmount1Total int
	shrinkableAmount1 := make([]int, len(items))
	shrinkable2 := make([]bool, len(items))
	growable2 := make([]bool, len(items))
	sortedItemInfo := boxLayoutItemInfoList(make([]boxLayoutItemInfo, len(items)))

	for i, item := range items {
		sf := hwnd2StretchFactor[item.Handle()]
		if sf == 0 {
			sf = 1
		}
		stretchFactors[i] = sf

		geometry := item.Geometry()

		flags := item.LayoutFlags()

		max := geometry.MaxSize
		var pref Size
		if hfw, ok := item.(HeightForWidther); !ok || !hfw.HasHeightForWidth() {
			if is, ok := item.(IdealSizer); ok {
				pref = is.IdealSize()
			}
		}

		if orientation == Horizontal {
			growable2[i] = flags&GrowableVert > 0

			minSizes[i] = container.MinSizeEffectiveForChild(item).Width

			if max.Width > 0 {
				maxSizes[i] = max.Width
			} else if pref.Width > 0 && flags&GrowableHorz == 0 {
				maxSizes[i] = pref.Width
			} else {
				maxSizes[i] = 32768
			}

			prefSizes2[i] = pref.Height

			sortedItemInfo[i].prefSize = pref.Width
			sortedItemInfo[i].greedy = flags&GreedyHorz > 0
		} else {
			growable2[i] = flags&GrowableHorz > 0

			if hfw, ok := item.(HeightForWidther); ok && hfw.HasHeightForWidth() {
				minSizes[i] = hfw.HeightForWidth(bounds.Width - margins.HNear - margins.HFar)
			} else {
				minSizes[i] = container.MinSizeEffectiveForChild(item).Height
			}

			if max.Height > 0 {
				maxSizes[i] = max.Height
			} else if hfw, ok := item.(HeightForWidther); ok && flags&GrowableVert == 0 && hfw.HasHeightForWidth() {
				maxSizes[i] = minSizes[i]
			} else if pref.Height > 0 && flags&GrowableVert == 0 {
				maxSizes[i] = pref.Height
			} else {
				maxSizes[i] = 32768
			}

			prefSizes2[i] = pref.Width

			sortedItemInfo[i].prefSize = pref.Height
			sortedItemInfo[i].greedy = flags&GreedyVert > 0
		}

		sortedItemInfo[i].index = i
		sortedItemInfo[i].minSize = minSizes[i]
		sortedItemInfo[i].maxSize = maxSizes[i]
		sortedItemInfo[i].stretch = sf
		sortedItemInfo[i].item = item

		if orientation == Horizontal && flags&(ShrinkableHorz|GrowableHorz|GreedyHorz) == ShrinkableHorz ||
			orientation == Vertical && flags&(ShrinkableVert|GrowableVert|GreedyVert) == ShrinkableVert {
			if amount := sortedItemInfo[i].prefSize - minSizes[i]; amount > 0 {
				shrinkableAmount1[i] = amount
				shrinkableAmount1Total += amount
			}
		}
		shrinkable2[i] = orientation == Horizontal && flags&ShrinkableVert != 0 || orientation == Vertical && flags&ShrinkableHorz != 0

		if shrinkableAmount1[i] > 0 {
			minSizesRemaining += sortedItemInfo[i].prefSize
		} else {
			minSizesRemaining += minSizes[i]
		}

		if sortedItemInfo[i].greedy {
			if _, isSpacer := item.(*spacerLayoutItem); !isSpacer {
				greedyNonSpacerCount++
				stretchFactorsTotal[0] += sf
			} else {
				greedySpacerCount++
				stretchFactorsTotal[1] += sf
			}
		} else {
			stretchFactorsTotal[2] += sf
		}
	}

	sort.Stable(sortedItemInfo)

	var start1, start2, space1, space2 int
	if orientation == Horizontal {
		start1 = bounds.X + margins.HNear
		start2 = bounds.Y + margins.VNear
		space1 = bounds.Width - margins.HNear - margins.HFar
		space2 = bounds.Height - margins.VNear - margins.VFar
	} else {
		start1 = bounds.Y + margins.VNear
		start2 = bounds.X + margins.HNear
		space1 = bounds.Height - margins.VNear - margins.VFar
		space2 = bounds.Width - margins.HNear - margins.HFar
	}

	spacingRemaining := spacing * (len(items) - 1)
	excess := float64(space1 - minSizesRemaining - spacingRemaining)

	offsets := [3]int{0, greedyNonSpacerCount, greedyNonSpacerCount + greedySpacerCount}
	counts := [3]int{greedyNonSpacerCount, greedySpacerCount, len(items) - greedyNonSpacerCount - greedySpacerCount}

	for i := 0; i < 3; i++ {
		stretchFactorsRemaining := stretchFactorsTotal[i]

		for j := 0; j < counts[i]; j++ {
			info := sortedItemInfo[offsets[i]+j]
			k := info.index

			stretch := stretchFactors[k]
			min := info.minSize
			max := info.maxSize
			var size int
			var corrected bool
			if shrinkableAmount1[k] > 0 {
				size = info.prefSize
				if excess < 0.0 {
					size -= mini(shrinkableAmount1[k], int(math.Round(-excess/float64(shrinkableAmount1Total)*float64(shrinkableAmount1[k]))))
					corrected = true
				}
			} else {
				size = min
			}

			if !corrected && min < max {
				excessSpace := float64(space1 - minSizesRemaining - spacingRemaining)
				size += int(math.Round(excessSpace * float64(stretch) / float64(stretchFactorsRemaining)))
				if size < min {
					size = min
				} else if size > max {
					size = max
				}
			}

			sizes[k] = size

			if shrinkableAmount1[k] > 0 {
				minSizesRemaining -= info.prefSize
			} else {
				minSizesRemaining -= min
			}
			stretchFactorsRemaining -= stretch
			space1 -= (size + spacing)
			spacingRemaining -= spacing
		}
	}

	results := make([]LayoutResultItem, 0, len(items))

	excessTotal := space1 - minSizesRemaining - spacingRemaining
	excessShare := excessTotal / len(items)
	halfExcessShare := excessTotal / (len(items) * 2)
	p1 := start1
	for i, item := range items {
		s1 := sizes[i]

		var s2 int
		if hfw, ok := item.(HeightForWidther); ok && orientation == Horizontal && hfw.HasHeightForWidth() {
			s2 = hfw.HeightForWidth(s1)
		} else if shrinkable2[i] || growable2[i] {
			s2 = space2
		} else {
			s2 = prefSizes2[i]
		}

		align := item.Geometry().Alignment
		if align == AlignHVDefault {
			align = alignment
		}

		var x, y, w, h, p2 int
		if orientation == Horizontal {
			switch align {
			case AlignHNearVNear, AlignHNearVCenter, AlignHNearVFar:
				// nop

			case AlignHFarVNear, AlignHFarVCenter, AlignHFarVFar:
				p1 += excessShare

			default:
				p1 += halfExcessShare
			}

			switch align {
			case AlignHNearVNear, AlignHCenterVNear, AlignHFarVNear:
				p2 = start2

			case AlignHNearVFar, AlignHCenterVFar, AlignHFarVFar:
				p2 = start2 + space2 - s2

			default:
				p2 = start2 + (space2-s2)/2
			}

			x, y, w, h = p1, p2, s1, s2
		} else {
			switch align {
			case AlignHNearVNear, AlignHCenterVNear, AlignHFarVNear:
				// nop

			case AlignHNearVFar, AlignHCenterVFar, AlignHFarVFar:
				p1 += excessShare

			default:
				p1 += halfExcessShare
			}

			switch align {
			case AlignHNearVNear, AlignHNearVCenter, AlignHNearVFar:
				p2 = start2

			case AlignHFarVNear, AlignHFarVCenter, AlignHFarVFar:
				p2 = start2 + space2 - s2

			default:
				p2 = start2 + (space2-s2)/2
			}

			x, y, w, h = p2, p1, s2, s1
		}

		if orientation == Horizontal {
			switch align {
			case AlignHNearVNear, AlignHNearVCenter, AlignHNearVFar:
				p1 += excessShare

			case AlignHFarVNear, AlignHFarVCenter, AlignHFarVFar:
				// nop

			default:
				p1 += halfExcessShare
			}

		} else {
			switch align {
			case AlignHNearVNear, AlignHCenterVNear, AlignHFarVNear:
				p1 += excessShare

			case AlignHNearVFar, AlignHCenterVFar, AlignHFarVFar:
				// nop

			default:
				p1 += halfExcessShare
			}
		}

		p1 += s1 + spacing

		results = append(results, LayoutResultItem{Item: item, Bounds: Rectangle{X: x, Y: y, Width: w, Height: h}})
	}

	return results
}
