package basicElements

import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"

// ScrollContainer is a container that is capable of holding a scrollable
// element.
type ScrollContainer struct {
	*core.Core
	core core.CoreControl
	focused bool
	
	child elements.Scrollable
	childWidth, childHeight int
	
	horizontal struct {
		theme theme.Wrapped
		exists bool
		enabled bool
		dragging bool
		dragOffset int
		gutter image.Rectangle
		track image.Rectangle
		bar image.Rectangle
	}

	vertical struct {
		theme theme.Wrapped
		exists bool
		enabled bool
		dragging bool
		dragOffset int
		gutter image.Rectangle
		track image.Rectangle
		bar image.Rectangle
	}
	
	config config.Wrapped
	theme  theme.Wrapped

	onFocusRequest func () (granted bool)
	onFocusMotionRequest func (input.KeynavDirection) (granted bool)
}

// NewScrollContainer creates a new scroll container with the specified scroll
// bars.
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
	element = &ScrollContainer { }
	element.theme.Case = theme.C("basic", "scrollContainer")
	element.horizontal.theme.Case = theme.C("basic", "scrollBarHorizontal")
	element.vertical.theme.Case   = theme.C("basic", "scrollBarVertical")
	
	element.Core, element.core = core.NewCore(element.handleResize)
	element.horizontal.exists = horizontal
	element.vertical.exists   = vertical
	return
}

func (element *ScrollContainer) handleResize () {
	element.recalculate()
	element.resizeChildToFit()
	element.draw()
}

// Adopt adds a scrollable element to the scroll container. The container can
// only contain one scrollable element at a time, and when a new one is adopted
// it replaces the last one.
func (element *ScrollContainer) Adopt (child elements.Scrollable) {
	// disown previous child if it exists
	if element.child != nil {
		element.clearChildEventHandlers(child)
	}

	// adopt new child
	element.child = child
	if child != nil {
		if child0, ok := child.(elements.Themeable); ok {
			child0.SetTheme(element.theme.Theme)
		}
		if child0, ok := child.(elements.Configurable); ok {
			child0.SetConfig(element.config.Config)
		}
		child.OnDamage(element.childDamageCallback)
		child.OnMinimumSizeChange(element.updateMinimumSize)
		child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
		if newChild, ok := child.(elements.Focusable); ok {
			newChild.OnFocusRequest (
				element.childFocusRequestCallback)
			newChild.OnFocusMotionRequest (
				element.childFocusMotionRequestCallback)
		}

		element.updateMinimumSize()
		
		element.horizontal.enabled,
		element.vertical.enabled = element.child.ScrollAxes()

		if element.core.HasImage() {
			element.resizeChildToFit()
		}
	}
}

// SetTheme sets the element's theme.
func (element *ScrollContainer) SetTheme (new theme.Theme) {
	if new == element.theme.Theme { return }
	element.theme.Theme = new
	if child, ok := element.child.(elements.Themeable); ok {
		child.SetTheme(element.theme.Theme)
	}
	if element.core.HasImage() {
		element.recalculate()
		element.resizeChildToFit()
		element.draw()
	}
}

// SetConfig sets the element's configuration.
func (element *ScrollContainer) SetConfig (new config.Config) {
	if new == element.config.Config { return }
	element.config.Config = new
	if child, ok := element.child.(elements.Configurable); ok {
		child.SetConfig(element.config.Config)
	}
	if element.core.HasImage() {
		element.recalculate()
		element.resizeChildToFit()
		element.draw()
	}
}

func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
	if child, ok := element.child.(elements.KeyboardTarget); ok {
		child.HandleKeyDown(key, modifiers)
	}
}

func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
	if child, ok := element.child.(elements.KeyboardTarget); ok {
		child.HandleKeyUp(key, modifiers)
	}
}

func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) {
	velocity := element.config.ScrollVelocity()
	point := image.Pt(x, y)
	if point.In(element.horizontal.bar) {
		element.horizontal.dragging = true
		element.horizontal.dragOffset =
			x - element.horizontal.bar.Min.X +
			element.Bounds().Min.X
		element.dragHorizontalBar(point)
		
	} else if point.In(element.horizontal.gutter) {
		switch button {
		case input.ButtonLeft:
			element.horizontal.dragging = true
			element.horizontal.dragOffset =
				element.horizontal.bar.Dx() / 2 +
				element.Bounds().Min.X
			element.dragHorizontalBar(point)
		case input.ButtonMiddle:
			viewport := element.child.ScrollViewportBounds().Dx()
			if x > element.horizontal.bar.Min.X {
				element.scrollChildBy(viewport, 0)
			} else {
				element.scrollChildBy(-viewport, 0)
			}
		case input.ButtonRight:
			if x > element.horizontal.bar.Min.X {
				element.scrollChildBy(velocity, 0)
			} else {
				element.scrollChildBy(-velocity, 0)
			}
		}
		
	} else if point.In(element.vertical.bar) {
		element.vertical.dragging = true
		element.vertical.dragOffset =
			y - element.vertical.bar.Min.Y +
				element.Bounds().Min.Y
		element.dragVerticalBar(point)
		
	} else if point.In(element.vertical.gutter) {
		switch button {
		case input.ButtonLeft:
			element.vertical.dragging = true
			element.vertical.dragOffset =
				element.vertical.bar.Dy() / 2 +
				element.Bounds().Min.Y
			element.dragVerticalBar(point)
		case input.ButtonMiddle:
			viewport := element.child.ScrollViewportBounds().Dy()
			if y > element.vertical.bar.Min.Y {
				element.scrollChildBy(0, viewport)
			} else {
				element.scrollChildBy(0, -viewport)
			}
		case input.ButtonRight:
			if y > element.vertical.bar.Min.Y {
				element.scrollChildBy(0, velocity)
			} else {
				element.scrollChildBy(0, -velocity)
			}
		}
		
	} else if child, ok := element.child.(elements.MouseTarget); ok {
		child.HandleMouseDown(x, y, button)
	}
}

func (element *ScrollContainer) HandleMouseUp (x, y int, button input.Button) {
	if element.horizontal.dragging {
		element.horizontal.dragging = false
		element.drawHorizontalBar()
		element.core.DamageRegion(element.horizontal.bar)
		
	} else if element.vertical.dragging {
		element.vertical.dragging = false
		element.drawVerticalBar()
		element.core.DamageRegion(element.vertical.bar)
		
	} else if child, ok := element.child.(elements.MouseTarget); ok {
		child.HandleMouseUp(x, y, button)
	}
}

func (element *ScrollContainer) HandleMouseMove (x, y int) {
	if element.horizontal.dragging {
		element.dragHorizontalBar(image.Pt(x, y))
		
	} else if element.vertical.dragging {
		element.dragVerticalBar(image.Pt(x, y))
		
	} else if child, ok := element.child.(elements.MouseTarget); ok {
		child.HandleMouseMove(x, y)
	}
}

func (element *ScrollContainer) HandleMouseScroll (
	x, y int,
	deltaX, deltaY float64,
) {
	element.scrollChildBy(int(deltaX), int(deltaY))
}

func (element *ScrollContainer) scrollChildBy (x, y int) {
	if element.child == nil { return }
	scrollPoint :=
		element.child.ScrollViewportBounds().Min.
		Add(image.Pt(x, y))
	element.child.ScrollTo(scrollPoint)
}

func (element *ScrollContainer) Focused () (focused bool) {
	return element.focused
}

func (element *ScrollContainer) Focus () {
	if element.onFocusRequest != nil {
		if element.onFocusRequest() {
			element.focused = true
		}
	}
}

func (element *ScrollContainer) HandleFocus (
	direction input.KeynavDirection,
) (
	accepted bool,
) {
	if child, ok := element.child.(elements.Focusable); ok {
		element.focused = child.HandleFocus(direction)
		return element.focused
	} else {
		element.focused = false
		return false
	}
}

func (element *ScrollContainer) HandleUnfocus () {
	if child, ok := element.child.(elements.Focusable); ok {
		child.HandleUnfocus()
	}
	element.focused = false
}

func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool)) {
	element.onFocusRequest = callback
}

func (element *ScrollContainer) OnFocusMotionRequest (
	callback func (direction input.KeynavDirection) (granted bool),
) {
	element.onFocusMotionRequest = callback
}

func (element *ScrollContainer) childDamageCallback (region canvas.Canvas) {
	element.core.DamageRegion(region.Bounds())
}

func (element *ScrollContainer) childFocusRequestCallback () (granted bool) {
	if element.onFocusRequest != nil {
		element.focused = element.onFocusRequest()
		return element.focused
	} else {
		return false
	}
}

func (element *ScrollContainer) childFocusMotionRequestCallback (
	direction input.KeynavDirection,
) (
	granted bool,
) {
	if element.onFocusMotionRequest == nil { return }
	return element.onFocusMotionRequest(direction)
}

func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) {
	child.DrawTo(nil)
	child.OnDamage(nil)
	child.OnMinimumSizeChange(nil)
	child.OnScrollBoundsChange(nil)
	if child0, ok := child.(elements.Focusable); ok {
		child0.OnFocusRequest(nil)
		child0.OnFocusMotionRequest(nil)
		if child0.Focused() {
			child0.HandleUnfocus()
		}
	}
	if child0, ok := child.(elements.Flexible); ok {
		child0.OnFlexibleHeightChange(nil)
	}
}

func (element *ScrollContainer) resizeChildToFit () {
	childBounds := image.Rect (
		0, 0,
		element.childWidth,
		element.childHeight).Add(element.Bounds().Min)
	element.child.DrawTo(canvas.Cut(element.core, childBounds))
}

func (element *ScrollContainer) recalculate () {
	horizontal := &element.horizontal
	vertical   := &element.vertical
	
	gutterInsetHorizontal := horizontal.theme.Padding(theme.PatternGutter)
	gutterInsetVertical   := vertical.theme.Padding(theme.PatternGutter)

	bounds     := element.Bounds()
	thicknessHorizontal :=
		element.config.HandleWidth() +
		gutterInsetHorizontal[3] +
		gutterInsetHorizontal[1]
	thicknessVertical :=
		element.config.HandleWidth() +
		gutterInsetVertical[3] +
		gutterInsetVertical[1]

	// calculate child size
	element.childWidth  = bounds.Dx()
	element.childHeight = bounds.Dy()

	// reset bounds
	horizontal.gutter = image.Rectangle { }
	vertical.gutter   = image.Rectangle { }
	horizontal.bar    = image.Rectangle { }
	vertical.bar      = image.Rectangle { }

	// if enabled, give substance to the gutters
	if horizontal.exists {
		horizontal.gutter.Min.X = bounds.Min.X
		horizontal.gutter.Min.Y = bounds.Max.Y - thicknessHorizontal
		horizontal.gutter.Max.X = bounds.Max.X
		horizontal.gutter.Max.Y = bounds.Max.Y
		if vertical.exists {
			horizontal.gutter.Max.X -= thicknessVertical
		}
		element.childHeight -= thicknessHorizontal
		horizontal.track = gutterInsetHorizontal.Apply(horizontal.gutter)
	}
	if vertical.exists {
		vertical.gutter.Min.X = bounds.Max.X - thicknessVertical
		vertical.gutter.Max.X = bounds.Max.X
		vertical.gutter.Min.Y = bounds.Min.Y
		vertical.gutter.Max.Y = bounds.Max.Y
		if horizontal.exists {
			vertical.gutter.Max.Y -= thicknessHorizontal
		}
		element.childWidth -= thicknessVertical
		vertical.track = gutterInsetVertical.Apply(vertical.gutter)
	}

	// if enabled, calculate the positions of the bars
	contentBounds  := element.child.ScrollContentBounds()
	viewportBounds := element.child.ScrollViewportBounds()
	if horizontal.exists && horizontal.enabled {
		horizontal.bar.Min.Y = horizontal.track.Min.Y
		horizontal.bar.Max.Y = horizontal.track.Max.Y

		scale := float64(horizontal.track.Dx()) /
			float64(contentBounds.Dx())
		horizontal.bar.Min.X = int(float64(viewportBounds.Min.X) * scale)
		horizontal.bar.Max.X = int(float64(viewportBounds.Max.X) * scale)
		
		horizontal.bar.Min.X += horizontal.track.Min.X
		horizontal.bar.Max.X += horizontal.track.Min.X
	}
	if vertical.exists && vertical.enabled {
		vertical.bar.Min.X = vertical.track.Min.X
		vertical.bar.Max.X = vertical.track.Max.X

		scale := float64(vertical.track.Dy()) /
			float64(contentBounds.Dy())
		vertical.bar.Min.Y = int(float64(viewportBounds.Min.Y) * scale)
		vertical.bar.Max.Y = int(float64(viewportBounds.Max.Y) * scale)
		
		vertical.bar.Min.Y += vertical.track.Min.Y
		vertical.bar.Max.Y += vertical.track.Min.Y
	}

	// if the scroll bars are out of bounds, don't display them.
	if horizontal.bar.Dx() >= horizontal.track.Dx() {
		horizontal.bar = image.Rectangle { }
	}
	if vertical.bar.Dy() >= vertical.track.Dy() {
		vertical.bar = image.Rectangle { }
	}
}

func (element *ScrollContainer) draw () {
	deadPattern := element.theme.Pattern (
		theme.PatternDead, theme.State { })
	artist.DrawBounds (
		element.core, deadPattern,
		image.Rect (
			element.vertical.gutter.Min.X,
			element.horizontal.gutter.Min.Y,
			element.vertical.gutter.Max.X,
			element.horizontal.gutter.Max.Y))
	element.drawHorizontalBar()
	element.drawVerticalBar()
}

func (element *ScrollContainer) drawHorizontalBar () {
	state := theme.State {
		Disabled: !element.horizontal.enabled,
		Pressed:  element.horizontal.dragging,
	}
	gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state)
	artist.DrawBounds(element.core, gutterPattern, element.horizontal.gutter)
	
	handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state)
	artist.DrawBounds(element.core, handlePattern, element.horizontal.bar)
}

func (element *ScrollContainer) drawVerticalBar () {
	state := theme.State {
		Disabled: !element.vertical.enabled,
		Pressed:  element.vertical.dragging,
	}
	gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state)
	artist.DrawBounds(element.core, gutterPattern, element.vertical.gutter)
	
	handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state)
	artist.DrawBounds(element.core, handlePattern, element.vertical.bar)
}

func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) {
	scrollX :=
		float64(element.child.ScrollContentBounds().Dx()) /
		float64(element.horizontal.track.Dx()) *
		float64(mousePosition.X - element.horizontal.dragOffset)
	scrollY := element.child.ScrollViewportBounds().Min.Y
	element.child.ScrollTo(image.Pt(int(scrollX), scrollY))
}

func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) {
	scrollY :=
		float64(element.child.ScrollContentBounds().Dy()) /
		float64(element.vertical.track.Dy()) *
		float64(mousePosition.Y - element.vertical.dragOffset)
	scrollX := element.child.ScrollViewportBounds().Min.X
	element.child.ScrollTo(image.Pt(scrollX, int(scrollY)))
}

func (element *ScrollContainer) updateMinimumSize () {
	gutterInsetHorizontal := element.horizontal.theme.Padding(theme.PatternGutter)
	gutterInsetVertical   := element.vertical.theme.Padding(theme.PatternGutter)

	thicknessHorizontal :=
		element.config.HandleWidth() +
		gutterInsetHorizontal[3] +
		gutterInsetHorizontal[1]
	thicknessVertical :=
		element.config.HandleWidth() +
		gutterInsetVertical[3] +
		gutterInsetVertical[1]
	
	width  := thicknessHorizontal
	height := thicknessVertical
	if element.child != nil {
		childWidth, childHeight := element.child.MinimumSize()
		width  += childWidth
		height += childHeight
	}
	element.core.SetMinimumSize(width, height)
}

func (element *ScrollContainer) childScrollBoundsChangeCallback () {
	element.horizontal.enabled,
	element.vertical.enabled = element.child.ScrollAxes()
	if element.core.HasImage() {
		element.recalculate()
		element.drawHorizontalBar()
		element.drawVerticalBar()
		element.core.DamageRegion(element.horizontal.gutter)
		element.core.DamageRegion(element.vertical.gutter)
	}
}