package views

import (
	"image"
	"math"
	"time"

	"git.milar.in/milarin/gui"
	"github.com/hajimehoshi/ebiten/v2"
)

// ScrollView is a gui.View which can hold an arbitrary large view.
// If the sub view does not fit into its bounds, scroll bars will be shown
type ScrollView struct {
	gui.WrapperTmpl

	buf *gui.Image

	viewportWidth, viewportHeight int
	viewWidth, viewHeight         int

	VerticalScrollOffset   float64
	HorizontalScrollOffset float64
	ScrollStep             int

	targetOffset gui.Point
	ScrollSpeed  int
	lastDraw     time.Time

	verticalScrollBar   *ScrollbarView
	horizontalScrollBar *ScrollbarView
}

var _ gui.Wrapper = &ScrollView{}

func NewScrollView(view gui.View) *ScrollView {
	v := new(ScrollView)
	v.SetView(view)
	v.ScrollStep = 100
	v.ScrollSpeed = 1000
	v.lastDraw = time.Now()
	v.verticalScrollBar = NewScrollbarView(gui.Vertical)
	v.horizontalScrollBar = NewScrollbarView(gui.Horizontal)
	return v
}

func (v *ScrollView) update(deltaTime float64) {
	deltaSpeed := deltaTime * float64(v.ScrollSpeed)

	if math.Abs(v.VerticalScrollOffset-float64(v.targetOffset.Y)) >= deltaSpeed {
		vDir := iff(v.VerticalScrollOffset-float64(v.targetOffset.Y) < 0, 1.0, -1.0)
		v.VerticalScrollOffset += deltaSpeed * vDir
	} else {
		v.VerticalScrollOffset = float64(v.targetOffset.Y)
	}

	if math.Abs(v.HorizontalScrollOffset-float64(v.targetOffset.X)) >= deltaSpeed {
		hDir := iff(v.HorizontalScrollOffset-float64(v.targetOffset.X) < 0, 1.0, -1.0)
		v.HorizontalScrollOffset += deltaSpeed * hDir
	} else {
		v.HorizontalScrollOffset = float64(v.targetOffset.X)
	}
}

func (v *ScrollView) Layout(ctx gui.AppContext) (prefWidth, prefHeight int) {
	return -1, -1
}

func (v *ScrollView) Draw(img *gui.Image, ctx gui.AppContext) {
	now := time.Now()
	deltaTime := now.Sub(v.lastDraw).Seconds()
	v.lastDraw = now

	v.viewWidth = img.Bounds().Dx()
	v.viewHeight = img.Bounds().Dy()

	v.update(deltaTime)

	v.ViewTmpl.Draw(img, ctx)

	w, h := v.View().Layout(ctx)
	w = iff(w >= 0, w, img.Bounds().Dx()-v.verticalScrollBar.Thickness)
	h = iff(h >= 0, h, img.Bounds().Dy()-v.horizontalScrollBar.Thickness)

	if v.buf == nil || v.buf.Bounds().Dx() != w || v.buf.Bounds().Dy() != h {
		v.buf = gui.NewImage(w, h)
	}

	v.limit()
	v.View().Draw(v.buf, ctx)

	scrollH, scrollV := v.determineViewportSize(img)
	copyBufferWidth, copyBufferHeight := 0, 0

	if scrollH {
		copyBufferWidth = v.viewportWidth
	} else {
		copyBufferWidth = v.buf.Bounds().Dx()
	}

	if scrollV {
		copyBufferHeight = v.viewportHeight
	} else {
		copyBufferHeight = v.buf.Bounds().Dy()
	}

	// copy buffer
	min := image.Pt(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset))
	size := image.Pt(copyBufferWidth, copyBufferHeight)
	max := min.Add(size)
	rect := image.Rectangle{min, max}
	op := new(ebiten.DrawImageOptions)
	//op.GeoM.Translate(-float64(min.X), -float64(min.Y))
	img.DrawImage(v.buf.SubImage(rect).(*gui.Image), op) //-min.X, -min.Y)

	if scrollV {
		v.verticalScrollBar.ViewSize = v.buf.Bounds().Dy()
		v.verticalScrollBar.ViewportSize = size.Y
		v.verticalScrollBar.ViewportPos = int(v.VerticalScrollOffset)
		v.verticalScrollBar.Draw(img.SubImage(image.Rect(img.Bounds().Max.X-v.verticalScrollBar.Thickness, 0, img.Bounds().Max.X, img.Bounds().Max.Y)).(*gui.Image), ctx)
	}

	if scrollH {
		v.horizontalScrollBar.ViewSize = v.buf.Bounds().Dx()
		v.horizontalScrollBar.ViewportSize = size.X
		v.horizontalScrollBar.ViewportPos = int(v.HorizontalScrollOffset)
		v.horizontalScrollBar.Draw(img.SubImage(image.Rect(0, img.Bounds().Max.Y-v.horizontalScrollBar.Thickness, img.Bounds().Max.X, img.Bounds().Max.Y)).(*gui.Image), ctx)
	}
}

func (v *ScrollView) limit() {
	if v.buf != nil {
		v.targetOffset.Y = limit(v.targetOffset.Y, 0, max(v.buf.Bounds().Dy()-v.viewportHeight, 0))
		v.targetOffset.X = limit(v.targetOffset.X, 0, max(v.buf.Bounds().Dx()-v.viewportWidth, 0))
		v.VerticalScrollOffset = limit(v.VerticalScrollOffset, 0, float64(max(v.buf.Bounds().Dy()-v.viewportHeight, 0)))
		v.HorizontalScrollOffset = limit(v.HorizontalScrollOffset, 0, float64(max(v.buf.Bounds().Dx()-v.viewportWidth, 0)))
	}
}

func (v *ScrollView) Scroll(horizontalOffset, verticalOffset int) {
	v.targetOffset.Y = v.targetOffset.Y + verticalOffset
	v.targetOffset.X = v.targetOffset.X + horizontalOffset
}

func (v *ScrollView) determineViewportSize(img *gui.Image) (scrollbarH, scrollbarV bool) {
	v.viewportWidth, v.viewportHeight = img.Bounds().Dx()-v.verticalScrollBar.Thickness, img.Bounds().Dy()-v.horizontalScrollBar.Thickness
	scrollbarV = v.buf.Bounds().Dy() > v.viewportHeight
	scrollbarH = v.buf.Bounds().Dx() > v.viewportWidth

	if scrollbarV && !scrollbarH {
		v.viewportHeight += v.horizontalScrollBar.Thickness
		scrollbarV = v.buf.Bounds().Dy() > v.viewportHeight
	}

	if !scrollbarV && scrollbarH {
		v.viewportWidth += v.verticalScrollBar.Thickness
		scrollbarH = v.buf.Bounds().Dx() > v.viewportWidth
	}

	return
}

func (v *ScrollView) OnMouseClick(event gui.MouseEvent) (consumed bool) {
	return v.View().OnMouseClick(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset))))
}

func (v *ScrollView) OnMouseMove(event gui.MouseEvent) (consumed bool) {
	if event.Position.In(gui.D(v.viewportWidth, 0, v.viewWidth-v.viewportWidth, v.viewHeight)) {
		return v.verticalScrollBar.OnMouseMove(event.SubtractPos(gui.P(v.viewportWidth, 0)))
	}

	if event.Position.In(gui.D(0, v.viewportHeight, v.viewWidth, v.viewHeight-v.viewportHeight)) {
		return v.horizontalScrollBar.OnMouseMove(event.SubtractPos(gui.P(0, v.viewportHeight)))
	}

	return v.View().OnMouseMove(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset))))
}

func (v *ScrollView) OnMouseScroll(event gui.MouseEvent) (consumed bool) {
	if v.View().OnMouseScroll(event.AddPos(gui.P(int(v.HorizontalScrollOffset), int(v.VerticalScrollOffset)))) {
		return true
	}

	if event.Wheel.X != 0 || event.Wheel.Y != 0 {
		v.Scroll(-event.Wheel.X*v.ScrollStep, -event.Wheel.Y*v.ScrollStep)
		return true
	}

	return false
}
