package system

import "image"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/internal/util"

type containerBox struct {
	*box

	contentBounds image.Rectangle
	scroll        image.Point
	mask          bool

	attrGap      attrHierarchy[tomo.AttrGap]
	attrAlign    attrHierarchy[tomo.AttrAlign]
	attrOverflow attrHierarchy[tomo.AttrOverflow]
	attrLayout   attrHierarchy[tomo.AttrLayout]

	children []anyBox

	on struct {
		contentBoundsChange event.FuncBroadcaster
	}
}

func (this *System) NewContainerBox () tomo.ContainerBox {
	box := &containerBox { }
	box.box = this.newBox(box)
	return box
}

// ----- public methods ----------------------------------------------------- //

func (this *containerBox) ContentBounds () image.Rectangle {
	return this.contentBounds
}

func (this *containerBox) ScrollTo (point image.Point) {
	if this.scroll == point { return }
	this.scroll = point
	this.invalidateLayout()
}

func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie {
	return this.on.contentBoundsChange.Connect(callback)
}

func (this *containerBox) Add (child tomo.Object) {
	box := assertAnyBox(child.GetBox())
	if util.IndexOf(this.children, box) > -1 { return }

	box.setParent(this)
	box.flushActionQueue()
	this.children = append(this.children, box)
	this.invalidateLayout()
	this.invalidateMinimum()
}

func (this *containerBox) Remove (child tomo.Object) {
	box := assertAnyBox(child.GetBox())
	index := util.IndexOf(this.children, box)
	if index < 0 { return }

	box.setParent(nil)
	this.children = util.Remove(this.children, index)
	this.invalidateLayout()
	this.invalidateMinimum()
}

func (this *containerBox) Insert (child, before tomo.Object) {
	box := assertAnyBox(child.GetBox())
	if util.IndexOf(this.children, box) > -1 { return }

	beforeBox := assertAnyBox(before.GetBox())
	index     := util.IndexOf(this.children, beforeBox)
	
	if index < 0 {
		this.children = append(this.children, box)
	} else {
		this.children = util.Insert(this.children, index, box)
	}
	box.setParent(this)

	this.invalidateLayout()
	this.invalidateMinimum()
}

func (this *containerBox) Clear () {
	for _, box := range this.children {
		box.(anyBox).setParent(nil)
	}
	this.children = nil
	this.invalidateLayout()
	this.invalidateMinimum()
}

func (this *containerBox) Len () int {
	return len(this.children)
}

func (this *containerBox) At (index int) tomo.Object {
	if index < 0 || index >= len(this.children) {
		return nil
	}
	return this.children[index]
}

func (this *containerBox) SetInputMask (mask bool) {
	this.mask = mask
}

// ----- private methods ---------------------------------------------------- //

func (this *containerBox) Draw (can canvas.Canvas) {
	if can == nil { return }
	
	// textureMode := tomo.TextureMode(this.attrTextureMode.Value())
	texture     := this.attrTexture.Value().Texture
	col         := this.attrColor.Value().Color
	if col == nil { col = color.Transparent }

	rocks := make([]image.Rectangle, len(this.children))
	for index, box := range this.children {
		rocks[index] = box.Bounds()
	}
	for _, tile := range canvas.Shatter(this.bounds, rocks...) {
		clipped := can.SubCanvas(tile)
		if this.transparent() && this.parent != nil {
			this.parent.drawBackgroundPart(clipped)
		}
		if clipped == nil { continue }
		pen := clipped.Pen()
		pen.Fill(col)
		pen.Texture(texture)
		pen.Rectangle(this.innerClippingBounds)
	}
}

func (this *containerBox) setAttr (attr tomo.Attr, user bool) {
	switch attr := attr.(type) {
	case tomo.AttrColor:
		if this.attrColor.Set(attr, user) {
			this.invalidateTransparentChildren()
			this.invalidateDraw()
		}
		
	case tomo.AttrTexture:
		if this.attrTexture.Set(attr, user) {
			this.invalidateTransparentChildren()
			this.invalidateDraw()
		}
		
	case tomo.AttrTextureMode:
		if this.attrTextureMode.Set(attr, user) {
			this.invalidateTransparentChildren()
			this.invalidateDraw()
		}

	case tomo.AttrGap:
		if this.attrGap.Set(attr, user) {
			this.invalidateLayout()
			this.invalidateMinimum()
		}
		
	case tomo.AttrAlign:
		if this.attrAlign.Set(attr, user) {
			this.invalidateLayout()
		}
		
	case tomo.AttrOverflow:
		if this.attrOverflow.Set(attr, user) {
			this.invalidateLayout()
			this.invalidateMinimum()
		}
		
	case tomo.AttrLayout:
		if this.attrLayout.Set(attr, user) {
			this.invalidateLayout()
			this.invalidateMinimum()
		}

	default: this.box.setAttr(attr, user)
	}
}

func (this *containerBox) unsetAttr (kind tomo.AttrKind, user bool) {
	switch kind {
	case tomo.AttrKindColor:
		if this.attrColor.Unset(user) {
			this.invalidateTransparentChildren()
			this.invalidateDraw()
		}
		
	case tomo.AttrKindTexture:
		if this.attrTexture.Unset(user) {
			this.invalidateTransparentChildren()
			this.invalidateDraw()
		}
		
	case tomo.AttrKindTextureMode:
		if this.attrTextureMode.Unset(user) {
			this.invalidateTransparentChildren()
			this.invalidateDraw()
		}

	case tomo.AttrKindGap:
		if this.attrGap.Unset(user) {
			this.invalidateLayout()
			this.invalidateMinimum()
		}
		
	case tomo.AttrKindAlign:
		if this.attrAlign.Unset(user) {
			this.invalidateLayout()
		}
		
	case tomo.AttrKindOverflow:
		if this.attrOverflow.Unset(user) {
			this.invalidateLayout()
			this.invalidateMinimum()
		}
		
	case tomo.AttrKindLayout:
		if this.attrLayout.Unset(user) {
			this.invalidateLayout()
			this.invalidateMinimum()
		}

	default: this.box.unsetAttr(kind, user)
	}
}

func (this *containerBox) recommendedHeight (width int) int {
	layout := this.attrLayout.Value().Layout
	if layout == nil || !this.attrOverflow.Value().Y {
		return this.minSize.Value().Y
	} else {
		return layout.RecommendedHeight(this.layoutHints(), this.boxQuerier(), width) +
			this.borderAndPaddingSum().Vertical()
	}
}

func (this *containerBox) recommendedWidth (height int) int {
	layout := this.attrLayout.Value().Layout
	if layout == nil || !this.attrOverflow.Value().X {
		return this.minSize.Value().X
	} else {
		return layout.RecommendedWidth(this.layoutHints(), this.boxQuerier(), height) +
			this.borderAndPaddingSum().Horizontal()
	}
}

func (this *containerBox) drawBackgroundPart (can canvas.Canvas) {
	if can == nil { return }
	pen := can.Pen()
	
	// textureMode := tomo.TextureMode(this.attrTextureMode.Value())
	texture     := this.attrTexture.Value().Texture
	col         := this.attrColor.Value().Color
	if col == nil { col = color.Transparent }
	
	pen.Fill(col)
	pen.Texture(texture)

	if this.transparent() && this.parent != nil {
		this.parent.drawBackgroundPart(can)
	}
	pen.Rectangle(this.innerClippingBounds)
}

func (this *containerBox) invalidateTransparentChildren () {
	hierarchy := this.getHierarchy()
	if hierarchy == nil { return }
	for _, box := range this.children {
		box := assertAnyBox(box)
		if box.transparent() {
			hierarchy.invalidateDraw(box)
		}
	}
}

func (this *containerBox) flushActionQueue () {
	for _, box := range this.children {
		box.(anyBox).flushActionQueue()
	}
	this.box.flushActionQueue()
}

func (this *containerBox) getHierarchy () *Hierarchy {
	if this.parent == nil { return nil }
	return this.parent.getHierarchy()
}

func (this *containerBox) getCanvas () canvas.Canvas {
	return this.canvas.Value()
}

func (this *containerBox) getInnerClippingBounds () image.Rectangle {
	return this.innerClippingBounds
}

func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
	this.invalidateMinimum()
	size   := child.minimumSize()
	bounds := child.Bounds()
	if bounds.Dx() < size.X || bounds.Dy() < size.Y {
		this.invalidateLayout()
	}
}

func (this *containerBox) layoutHints () tomo.LayoutHints {
	overflow := this.attrOverflow.Value()
	align    := this.attrAlign.Value()
	gap      := image.Point(this.attrGap.Value())
	return tomo.LayoutHints {
		OverflowX: overflow.X,
		OverflowY: overflow.Y,
		AlignX:    align.X,
		AlignY:    align.Y,
		Gap:       gap,
	}
}

func (this *containerBox) contentMinimum () image.Point {
	overflow := this.attrOverflow.Value()
	minimum  := this.box.contentMinimum()
	layout := this.attrLayout.Value().Layout
	if layout != nil {
		layoutMinimum := layout.MinimumSize (
			this.layoutHints(),
			this.boxQuerier())
		if overflow.X { layoutMinimum.X = 0 }
		if overflow.Y { layoutMinimum.Y = 0 }
		minimum = minimum.Add(layoutMinimum)
	}
	return minimum
}

func (this *containerBox) doLayout () {
	this.box.doLayout()
	previousContentBounds := this.contentBounds
	layout := this.attrLayout.Value().Layout

	// by default, use innerBounds (translated to 0, 0) for contentBounds.
	// if a direction overflows, use the layout's minimum size for it.
	var minimum image.Point
	if layout != nil {
		minimum = layout.MinimumSize (
			this.layoutHints(),
			this.boxQuerier())
	}
	innerBounds := this.InnerBounds()
	overflow    := this.attrOverflow.Value()
	this.contentBounds = innerBounds.Sub(innerBounds.Min)
	if overflow.X { this.contentBounds.Max.X = this.contentBounds.Min.X + minimum.X }
	if overflow.Y { this.contentBounds.Max.Y = this.contentBounds.Min.Y + minimum.Y }
	
	// arrange children
	if layout != nil {
		layoutHints := this.layoutHints()
		layoutHints.Bounds = this.contentBounds
		layout.Arrange(layoutHints, this.boxArranger())
	}

	// build an accurate contentBounds by unioning the bounds of all child
	// boxes
	this.contentBounds = image.Rectangle { }
	for _, box := range this.children {
		bounds := box.Bounds()
		this.contentBounds = this.contentBounds.Union(bounds)
	}

	// constrain the scroll
	this.constrainScroll()

	// offset children and contentBounds by scroll
	for _, box := range this.children {
		box.setBounds(box.Bounds().Add(this.scroll).Add(innerBounds.Min))
	}
	this.contentBounds = this.contentBounds.Add(this.scroll)
	
	if previousContentBounds != this.contentBounds {
		this.on.contentBoundsChange.Broadcast()
	}
}

func (this *containerBox) constrainScroll () {
	innerBounds := this.InnerBounds()
	width  := this.contentBounds.Dx()
	height := this.contentBounds.Dy()

	// X
	if width <= innerBounds.Dx() {
		this.scroll.X = 0
	} else if this.scroll.X > 0 {
		this.scroll.X = 0
	} else if this.scroll.X < innerBounds.Dx() - width  {
		this.scroll.X =  innerBounds.Dx() - width
	}

	// Y
	if height <= innerBounds.Dy() {
		this.scroll.Y = 0
	} else if this.scroll.Y > 0 {
		this.scroll.Y = 0
	} else if this.scroll.Y < innerBounds.Dy() - height {
		this.scroll.Y = innerBounds.Dy() - height
	}
}

func (this *containerBox) boxQuerier () boxQuerier {
	return boxQuerier(this.children)
}

func (this *containerBox) boxArranger () boxArranger {
	return boxArranger(this.children)
}

func (this *containerBox) recursiveRedo () {
	this.doLayout()
	this.doDraw()
	for _, child := range this.children {
		child.(anyBox).recursiveRedo()
	}
}

func (this *containerBox) recursiveLoseCanvas () {
	this.box.recursiveLoseCanvas()
	for _, child := range this.children {
		child.(anyBox).recursiveLoseCanvas()
	}
}

func (this *containerBox) recursiveReApply () {
	this.box.recursiveReApply()
	for _, child := range this.children {
		child.(anyBox).recursiveReApply()
	}
}

func (this *containerBox) boxUnder (point image.Point) anyBox {
	if !point.In(this.bounds) { return nil }

	if !this.mask {
		for _, box := range this.children {
			candidate := box.(anyBox).boxUnder(point)
			if candidate != nil { return candidate }
		}
	}

	return this
}

func (this *containerBox) propagate (callback func (anyBox) bool) bool {
	for _, box := range this.children {
		box := box.(anyBox)
		if !box.propagate(callback) { return false }
	}

	return callback(this)
}

func (this *containerBox) propagateAlt (callback func (anyBox) bool) bool {
	if !callback(this) { return false}

	for _, box := range this.children {
		box := box.(anyBox)
		if !box.propagateAlt(callback) { return false }
	}

	return true
}

func (this *containerBox) masks () bool {
	return this.mask
}