package main

import (
	"fmt"
	sg "github.com/d2fn/sumi/internal/graphics"
	"github.com/gen2brain/raylib-go/raylib"
	"math"
)

type Sketch struct {
	env               *Env
	cam               *TextureCam
	composite         rl.RenderTexture2D
	layerTools        map[string]*LayerTools
	layerToolsOrdered []*LayerTools
}

type TextureCam struct {
	LookAt rl.Vector2
	Zoom   float32
}

type LayerTools struct {
	name    string
	layer   Layer
	texture rl.RenderTexture2D
	capture *rl.Image
	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(env *Env) Sketch {

	fmt.Printf("Creating new sketch with env: %v\n", env)

	// point at source center
	// put source center at center of screen
	var camera = TextureCam{
		LookAt: rl.Vector2{X: env.Offscreen.Width() / 2.0, Y: env.Offscreen.Height() / 2.0},
		Zoom:   1.0,
	}

	s := Sketch {
		env:               env,
		layerTools:        make(map[string]*LayerTools),
		layerToolsOrdered: []*LayerTools{},
		composite:         rl.LoadRenderTexture(env.Offscreen.WidthInt32(), env.Offscreen.HeightInt32()),
		cam:               &camera,
	}

	return s
}

func (s *Sketch) AddLayer(name string, layer Layer) {
	texture := rl.LoadRenderTexture(s.env.Offscreen.WidthInt32(), s.env.Offscreen.HeightInt32())
	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) RedrawLayers(env *Env, g *sg.Graphics) {
	// 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 {
			w := float32(instance.texture.Texture.Width)
			h := float32(instance.texture.Texture.Height)
			lg := sg.CreateGraphics(sg.Rect { X: 0, Y: 0, Width: w, Height: h })
			layer.Update(env, g)
			// re-render to texture if dirty
			if instance.layer.IsDirty() {
				lg.BeginTexture(instance.texture)
				lg.Begin()
				layer.Draw(env, lg)
				lg.End()
				lg.EndTexture()
			}
		}
	}
}

func (s *Sketch) Draw(env *Env) {

	offscreen := env.Offscreen
	s.RedrawLayers(env, offscreen) 

	/*
		src := g.Rect {
			X: 0, Y: 0,
			Width:  float32(ctx.SourceWidth),
			Height: -float32(ctx.SourceHeight),
		}
		dst := g.Rect{
			X: 0, Y: 0,
			Width:  float32(ctx.SourceWidth),
			Height: float32(ctx.SourceHeight),
		}
	*/

	// calculate the viewable region of the offscreen buffer
	viewport := s.CalcViewport(env)

	// scale the offscreen buffer to the viewport maintaining aspect ratio
	outputRect := offscreen.Bounds.ScaleTo(env.Viewport.Bounds)
	//fmt.Printf("scaling %v to %v => outputRect = %v\n", offscreen.Bounds, env.Viewport.Bounds, outputRect)

	x := float32(0)
	y := float32(0)
	w := outputRect.Width
	h := outputRect.Height

	output := sg.CreateGraphics(outputRect)

	// render a checker pattern indicating transparency
	output.Begin()
	output.SetFill(true)
	output.SetStroke(false)
	checkSize := float32(outputRect.Width / 50.0)
	grey := rl.NewColor(220, 220, 220, 255)
	cellX := 0
	cellY := 0
	for y < h {
		x = 0
		cellX = 0
		for x < w {
			c := rl.White
			if ((cellX + cellY) & 1) == 1 {
				c = grey
			}
			output.SetFillColor(c)
			output.DrawRect(sg.Rect { X: x, Y: y, Width: checkSize, Height: checkSize })
			//rl.DrawRectangle(int32(x), int32(y), int32(checkSize), int32(checkSize), c)
			x += checkSize
			cellX++
		}
		y += checkSize
		cellY++
	}
	output.End()

	rl.GenTextureMipmaps(&s.composite.Texture)
	rl.SetTextureFilter(s.composite.Texture, rl.FilterTrilinear)

	// render each layer onto the composite
	offscreen.BeginPremultiplyBlend()
	offscreen.BeginTexture(s.composite)
	offscreen.Begin()
	offscreen.Clear()

	//rl.BeginBlendMode(rl.BlendAlphaPremultiply)
	//rl.BeginBlendMode(rl.BlendAlpha)
	//rl.BeginTextureMode(s.composite)
	
	//rl.ClearBackground(rl.Blank)
	//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
			}
			r = uint8(float32(r) * (float32(config.a) / 255.0))
			g = uint8(float32(g) * (float32(config.a) / 255.0))
			b = uint8(float32(b) * (float32(config.a) / 255.0))
			tint := rl.NewColor(r, g, b, config.a)
			offscreen.TransferTexture(instance.texture.Texture, offscreen.Bounds, offscreen.Bounds, tint)
			//rl.DrawTexturePro(instance.texture.Texture, src.ToRL(), dst.ToRL(), rl.Vector2{}, 0, tint)
		}
	}
	offscreen.End()
	offscreen.EndTexture()
	offscreen.EndBlend()

	// minimap indexing for composite so that it scales down nicely
	//rl.DrawTexturePro(s.composite.Texture, rl.Rectangle { X: viewport.X, Y: viewport.Y, Width: viewport.Width, Height: viewport.Height}, dst.ToRL(), rl.Vector2{}, 0, rl.White)

	//fmt.Printf("viewport -> %v\n", viewport)

	// finally, transfer composite texture to output rect on screen
	/**
	rl.PushMatrix()
	rl.BeginScissorMode(int32(offscreen.Bounds.X), int32(offscreen.Bounds.Y), int32(offscreen.Bounds.Width), int32(offscreen.Bounds.Height))
	rl.Translatef(float32(offscreen.Bounds.UL().X), offscreen.Bounds.UL().Y, 0)
	rl.DrawTexturePro(s.composite.Texture, viewport.ToRL(), outputRect.ToRL(), rl.Vector2{}, 0, rl.White)
	rl.EndScissorMode()
	rl.PopMatrix()
	**/

	env.Window.Begin()
	env.Window.TransferTexture(s.composite.Texture, viewport, outputRect, rl.White)
	env.Window.SetFill(false)
	env.Window.SetStroke(true)
	env.Window.SetStrokeColor(rl.Gray)
	env.Window.DrawRect(outputRect)
	env.Window.End()

	/*
	env.Window.Begin()
	env.Window.SetStroke(true)
	env.Window.SetFill(false)
	env.Window.SetStrokeWeight(5.0)
	env.Window.SetStrokeColor(rl.Magenta)
	env.Window.DrawRect(outputRect)
	env.Window.End()
	*/
}

// calculate the visible clip of the offscreen buffer based on the camera /zoom
func (s *Sketch) CalcViewport(env *Env) sg.Rect {
	viewportWidth  := rl.Clamp(env.Offscreen.Width()/s.cam.Zoom, 0, env.Offscreen.Width())
	viewportHeight := rl.Clamp(env.Offscreen.Height()/s.cam.Zoom, 0, env.Offscreen.Height())
	return sg.Rect{
		X:      rl.Clamp(s.cam.LookAt.X-viewportWidth/2.0, 0, env.Layout.Offscreen.Width-viewportWidth),
		Y:      rl.Clamp(s.cam.LookAt.Y-viewportHeight/2.0, 0, env.Layout.Offscreen.Height-viewportHeight),
		Width:  viewportWidth,
		Height: viewportHeight,
	}
}

func (s *Sketch) Update(env *Env) {

	if rl.IsMouseButtonDown(rl.MouseRightButton) {
		// get mouse delta from last frame
		delta := rl.GetMouseDelta()
		sourceScale := env.Offscreen.Width() / env.Viewport.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, env.Offscreen.Width())
	s.cam.LookAt.Y = rl.Clamp(s.cam.LookAt.Y, 0, env.Offscreen.Height())

	// Zoom based on mouse wheel
	wheel := rl.GetMouseWheelMove()
	if wheel != 0 {
		const zoomIncrement float32 = 0.20
		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(env *Env) {
	s.cam.LookAt =
		rl.Vector2 {
			X: env.Offscreen.Width() / 2.0,
			Y: env.Offscreen.Height() / 2.0,
		}
	s.cam.Zoom = 1.0
}

type SketchCapture struct {
	width, height     int32
	compositeImage    *rl.Image
	layerTools        map[string]*LayerTools
	layerToolsOrdered []*LayerTools
}

func (s *Sketch) Capture(env *Env) *SketchCapture {
	composite := rl.LoadImageFromTexture(s.composite.Texture)
	rl.ImageFlipVertical(composite)
	for _, layerTool := range s.layerToolsOrdered {
		layerTool.capture = rl.LoadImageFromTexture(layerTool.texture.Texture)
		rl.ImageFlipVertical(layerTool.capture)
	}
	return &SketchCapture{
		width:             env.Offscreen.WidthInt32(),
		height:            env.Offscreen.HeightInt32(),
		compositeImage:    composite,
		layerTools:        s.layerTools,
		layerToolsOrdered: s.layerToolsOrdered,
	}
}

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 *Env, g *sg.Graphics)
	Draw(ctx *Env, g *sg.Graphics)
	IsDirty() bool
}

type ColorLayer struct {
	color rl.Color
	dirty bool
}

func (cl *ColorLayer) Update(ctx *Env, g *sg.Graphics) {

}

func (cl *ColorLayer) Draw(ctx *Env, g *sg.Graphics) {
	g.SetFill(true)
	g.SetFillColor(cl.color)
	g.SetStroke(false)
	g.DrawRect(g.Bounds)
	//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 *Env, g *sg.Graphics) {

}

func (il *ImageLayer) Draw(env *Env, g *sg.Graphics) {
	rl.PushMatrix()
	rl.Translatef(
		g.Width()/2.0-float32(il.texture.Width)/2.0,
		g.Height()/2.0-float32(il.texture.Height)/2.0, 0)
	g.DrawTexture(il.texture, sg.Origin, rl.White)
	rl.PopMatrix()
}

func (il *ImageLayer) IsDirty() bool {
	return il.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
}
