package main

import (
	"math"
	"github.com/gen2brain/raylib-go/raylib"
)

type Sketch struct {
	sourceWidth int32
	sourceHeight int32
	cam          *TextureCam
	composite rl.RenderTexture2D
	layerTools map[string]*LayerTools
	layerToolsOrdered []*LayerTools
}

type TextureCam struct {
	LookAt       rl.Vector2
	Zoom         float32
}

/** RenderCtx **/
type RenderCtx struct {
	TargetBounds rl.Rectangle
	SourceWidth  int32
	SourceHeight int32
	Time         float64
	Ports        map[string]float64
}

type LayerTools struct {
	name    string
	layer   Layer
	texture rl.RenderTexture2D
	config  *LayerConfig
}

type LayerConfig struct {
	visible bool
	a uint8
	rVisible bool
	r uint8
	gVisible bool
	g uint8
	bVisible bool
	b uint8
	desaturate bool
	saturation float32
	kValue float32
}

func NewSketch(sourceWidth, sourceHeight int32) Sketch {

	// point at source center
	// put source center at center of screen
	var camera = TextureCam {
		LookAt:       rl.Vector2{X: float32(sourceWidth) / 2.0, Y: float32(sourceHeight) / 2.0},
		Zoom:         1.0,
	}

	return Sketch {
		sourceWidth: sourceWidth,
		sourceHeight: sourceHeight,
		layerTools: make(map[string]*LayerTools),
		layerToolsOrdered: []*LayerTools {},
		composite: rl.LoadRenderTexture(sourceWidth, sourceHeight),
		cam: &camera,
	}
}

func (s *Sketch) AddLayer(name string, layer Layer) {
	texture := rl.LoadRenderTexture(s.sourceWidth, s.sourceHeight)
	config := NewLayerConfig()
	layerTools :=
		LayerTools {
			name:    name,
			texture: texture,
			layer:   layer,
			config:  &config,
		}
	s.layerToolsOrdered = append(s.layerToolsOrdered, &layerTools)
	s.layerTools[name] = &layerTools
}

func (s *Sketch) AddColorLayer(name string, c rl.Color) {
	colorLayer := &ColorLayer {
		color: c,
		dirty: true,
	}
	s.AddLayer(name, colorLayer)
}

func (s *Sketch) Redraw(ctx *RenderCtx) {
	// render onto all layer textures
	for _, instance := range s.layerToolsOrdered {
		layer := instance.layer
		// ignore this layer entirely unless it's visible
		if instance.config.visible {
			layer.Update(ctx)
			// re-render to texture if dirty
			if instance.layer.IsDirty() {
				rl.BeginTextureMode(instance.texture)
				rl.PushMatrix()
				layer.Draw(ctx)
				rl.PopMatrix()
				rl.EndTextureMode()
			}
		}
	}
}

func (s *Sketch) Draw(ctx *RenderCtx) {

	s.Redraw(ctx)

	// copy from full texture for compositing, with vertical flipping
	src := rl.Rectangle {
		X: 0, Y: 0,
		Width: float32(ctx.SourceWidth),
		Height: -float32(ctx.SourceHeight),
	}
	dst := rl.Rectangle {
		X: 0, Y: 0,
		Width: float32(ctx.SourceWidth),
		Height: float32(ctx.SourceHeight),
	}

	viewport := s.CalcViewport(ctx)

	rl.BeginTextureMode(s.composite)
	rl.ClearBackground(rl.Black)
	for _, instance := range s.layerToolsOrdered {
		config := instance.config
		if config.visible {
			var r uint8 = 0
			if config.rVisible {
				r = config.r
			}
			var g uint8 = 0
			if config.gVisible {
				g = config.g
			}
			var b uint8 = 0
			if config.bVisible {
				b = config.b
			}
			tint := rl.NewColor(r, g, b, config.a)
			rl.DrawTexturePro(instance.texture.Texture, src, dst, rl.Vector2{}, 0, tint)
		}
	}
	rl.EndTextureMode()

	outputRect := s.calcOutputRectKeepingAspectRatio(ctx)
	rl.DrawTexturePro(s.composite.Texture, viewport, outputRect, rl.Vector2{}, 0, rl.White)

	outlineRect := outputRect.ToInt32()
	rl.DrawRectangleLines(outlineRect.X, outlineRect.Y, outlineRect.Width, outlineRect.Height, rl.Gray)
}

func (s *Sketch) calcOutputRectKeepingAspectRatio(ctx *RenderCtx) rl.Rectangle {
	sourceAspect := float32(ctx.SourceWidth) / float32(ctx.SourceHeight)
	targetAspect := ctx.TargetBounds.Width / ctx.TargetBounds.Height

	outputWidth := ctx.TargetBounds.Width
	outputHeight := ctx.TargetBounds.Height
	
	if sourceAspect < targetAspect {
		// source is relatively taller than the target
		// so we set the output height to the target height
		// and calculate the width based on source aspect and center
		outputWidth = outputHeight * sourceAspect
	} else {
		// source is relatively wider than the target
		// so we set the output width to the target width
		// and calculate the height based on source aspect and center
		outputHeight = outputWidth / sourceAspect
	}

	// output width and height are correct -- center within TargetBounds
	x := ctx.TargetBounds.X + ctx.TargetBounds.Width / 2.0 - outputWidth / 2.0 
	y := ctx.TargetBounds.Y + ctx.TargetBounds.Height / 2.0 - outputHeight / 2.0 

	return rl.Rectangle {
		X: x, Y: y,
		Width: outputWidth,
		Height: outputHeight,
	}
}

func (s *Sketch) CalcViewport(ctx *RenderCtx) rl.Rectangle {
	viewportWidth := rl.Clamp(float32(ctx.SourceWidth) / s.cam.Zoom, 0, float32(ctx.SourceWidth))
	viewportHeight := rl.Clamp(float32(ctx.SourceHeight) / s.cam.Zoom, 0, float32(ctx.SourceHeight))
	return rl.Rectangle{
		X:      rl.Clamp(s.cam.LookAt.X - viewportWidth/2.0, 0, float32(ctx.SourceWidth)-viewportWidth),
		Y:      rl.Clamp(s.cam.LookAt.Y - viewportHeight/2.0, 0, float32(ctx.SourceHeight)-viewportHeight),
		Width:  float32(viewportWidth),
		Height: -float32(viewportHeight),
	}
}

func (s *Sketch) Update(ctx *RenderCtx) {

	if rl.IsMouseButtonDown(rl.MouseRightButton) {
		// get mouse delta from last frame
		delta := rl.GetMouseDelta()
		sourceScale := float32(ctx.SourceWidth) / ctx.TargetBounds.Width
		// compute the amount to move scaled by the camera zoom
		delta = rl.Vector2Scale(delta, -sourceScale/s.cam.Zoom)
		delta.Y = -delta.Y
		s.cam.LookAt = rl.Vector2Add(s.cam.LookAt, delta)
	}

	// clamp LookAt to be somewhere on the texture
	s.cam.LookAt.X = rl.Clamp(s.cam.LookAt.X, 0, float32(ctx.SourceWidth-1))
	s.cam.LookAt.Y = rl.Clamp(s.cam.LookAt.Y, 0, float32(ctx.SourceHeight-1))

	// Zoom based on mouse wheel
	wheel := rl.GetMouseWheelMove()
	if wheel != 0 {
		const zoomIncrement float32 = 0.05
		if wheel > 0 {
			s.cam.Zoom *= 1+zoomIncrement
		} else {
			s.cam.Zoom *= 1-zoomIncrement
		}
	}

	// clamp zoom to > 1 so we don't ever zoom out more than necessary
	s.cam.Zoom = rl.Clamp(s.cam.Zoom, 1, math.MaxInt64)
}

func (s *Sketch) ResetCamera() {
	s.cam.LookAt = rl.Vector2{X: float32(s.sourceWidth) / 2.0, Y: float32(s.sourceHeight) / 2.0}
	s.cam.Zoom = 1.0
}

type SketchCapture struct {
	compositeImage *rl.Image
	layerImages []*rl.Image
}

func (s *Sketch) Capture() *SketchCapture {
	composite := rl.LoadImageFromTexture(s.composite.Texture)
	rl.ImageFlipVertical(composite)
	layerImages := make([]*rl.Image, len(s.layerToolsOrdered))
	for i, layerTool := range s.layerToolsOrdered {
		layerImages[i] = rl.LoadImageFromTexture(layerTool.texture.Texture)
		rl.ImageFlipVertical(layerImages[i])
	}
	return &SketchCapture {
		compositeImage: composite,
		layerImages: layerImages,
	}
}

func NewLayerConfig() LayerConfig {
	return LayerConfig {
		visible: true,
		a: 255,
		rVisible: true,
		r: 255,
		gVisible: true,
		g: 255,
		bVisible: true,
		b: 255,
		desaturate: false,
		kValue: 1.0,
	}
}

/** Layer **/

type Layer interface {
	Update(ctx *RenderCtx)
	Draw(ctx *RenderCtx)
	IsDirty() bool
}

type ColorLayer struct {
	color rl.Color
	dirty bool
}

func (cl *ColorLayer) Update(ctx *RenderCtx) {
	;
}

func (cl *ColorLayer) Draw(ctx *RenderCtx) {
	rl.ClearBackground(cl.color)
	cl.dirty = false
}

func (cl *ColorLayer) IsDirty() bool {
	return cl.dirty
}

type ImageLayer struct {
	texture rl.Texture2D
	dirty bool
}

func NewImageLayer(path string) *ImageLayer {
	image := rl.LoadImage(path)
	tex := rl.LoadTextureFromImage(image)
	return &ImageLayer {
		texture: tex,
		dirty: true,
	}
}

func (il *ImageLayer) Update(ctx *RenderCtx) {
	;
}

func (il *ImageLayer) Draw(ctx *RenderCtx) {
	rl.Translatef(float32(ctx.SourceWidth) / 2.0 - float32(il.texture.Width) / 2.0, float32(ctx.SourceHeight) / 2.0 - float32(il.texture.Height) / 2.0, 0)
	rl.DrawTexture(il.texture, 0, 0, rl.White)
}

func (il *ImageLayer) IsDirty() bool {
	return il.dirty
}

type TestPattern struct{
	dirty bool
}

func (tp *TestPattern) Update(ctx *RenderCtx) {
	;
}

func (tp *TestPattern) Draw(ctx *RenderCtx) {

	rl.ClearBackground(rl.Black)
	centerX := float32(ctx.SourceWidth) / 2
	centerY := float32(ctx.SourceHeight) / 2

	rl.DrawRectangleRec(rl.Rectangle{X: 0, Y: 0, Width: centerX, Height: centerY}, rl.Red)
	rl.DrawRectangleRec(rl.Rectangle{X: centerX, Y: 0, Width: centerX, Height: centerY}, rl.Green)
	rl.DrawRectangleRec(rl.Rectangle{X: 0, Y: centerY, Width: centerX, Height: centerY}, rl.Blue)
	rl.DrawRectangleRec(rl.Rectangle{X: centerX, Y: centerY, Width: centerX, Height: centerY}, rl.White)

	rl.DrawLine(0, 0, ctx.SourceWidth, ctx.SourceHeight, rl.Black)

	rl.PushMatrix()
	rl.Translatef(centerX, centerY, 0)
	rl.SetLineWidth(4.0)
	rl.DrawLine(-10000, 0, 10000, 0, rl.Red)
	rl.DrawLine(0, -10000, 0, 10000, rl.Green)
	rl.DrawRectangleLines(-50, -50, 100, 100, rl.Magenta)
	rl.PopMatrix()

	tp.dirty = false
}

func (tp *TestPattern) IsDirty() bool {
	return tp.dirty
}

/** Ports **/

type Ports map[string]Signal

func MakePorts() Ports {
	return make(Ports)
}

/**
 * materialize current value for all ports
 **/
func (p Ports) Eval(t float64) map[string]float64 {
	out := make(map[string]float64, len(p))
	for name, sig := range p {
		out[name] = sig.Eval(t)
	}
	return out
}
