package gg

import (
	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
	"surdeus.su/core/gods/maps"
	"time"
	"slices"
)

import "surdeus.su/core/gg/mx"

const (
	MaxVertices = 1 << 16
)

type GraphicsLibrary = ebiten.GraphicsLibrary
type RunOptions = ebiten.RunGameOptions


// Window configuration type.
type WindowConfig struct {
	DebugInfo ebiten.DebugInfo
	Options *RunOptions
	// The title of the window.
	Title string
	
	// Width and height of the window
	// in pixels.
	Width,
	Height int
	
	// Optional settings with
	// self describing names.
	FixedSize,
	Fullscreen,
	VSync bool
}

// The main structure that represents
// current state of [game] engine.
type Engine struct {
	wcfg WindowConfig

	// The main holder for objects.
	// Uses the map structure to quickly
	// delete and create new objects.
	objects *Objects

	// The main camera to display in window.
	// If is set to nil then the engine will panic.
	camera Camera

	drawLastTime time.Time
	drawdt Duration

	// Frame delta time.
	dt Duration
	lastTime time.Time

	// Temporary stuff 
	keys, prevKeys []Key
	buttons MouseButtonMap
	wheel mx.Vector
	cursorPos mx.Vector
	outerEvents, handleEvents EventChan

	//bufs [LayerBufSize]*Image
	vertices map[Layer] []ebiten.Vertex
	//vindices []uint16

	// Draw frame.
	dframe uint

	// Frame.
	frame uint

	runes []rune
}

type engine Engine

// Get currently pressed keys.
func (e *Engine) GetKeyboardKeys() []Key {
	return e.keys
}

func (e *Engine) GraphicsLibrary() GraphicsLibrary {
	return e.wcfg.DebugInfo.GraphicsLibrary
}

// Returns currently pressed buttons.
func (e *Engine) GetMouseButtons() []MouseButton {
	ret := make([]MouseButton, len(e.buttons))
	i := 0
	for v := range e.buttons {
		ret[i] = v
		i++
	}
	slices.Sort(ret)
	return ret
}

// Returns new empty Engine.
func NewEngine(
	cfg WindowConfig,
) *Engine {
	ret := &Engine{}

	ret.wcfg = cfg
	ret.outerEvents = make(EventChan)
	ret.handleEvents = make(EventChan)
	ret.objects = NewObjects()
	ret.buttons = MouseButtonMap{}
	return ret
}

func (e *Engine) Camera() Camera {
	return e.camera
}

func (e *Engine) SetCamera(c Camera) *Engine {
	e.camera = c
	return e
}


func (e *Engine) EventInput() EventChan {
	return e.outerEvents
}

func (e *Engine) Exist(object Object) bool {
	return e.objects.has(object)
}

// Add new objects to the Engine's view.
func (e *Engine) Spawn(object Object) bool {

	ctx := Context{
		engine: e,
	}
	object.OnStart(ctx)
	ok := e.objects.add(object)

	return ok
}

// Delete object from Engine.
func (e *Engine) Delete(object Object) bool {

	ctx := Context{
		engine: e,
	}
	object.OnDelete(ctx)
	ok := e.objects.remove(object)

	return ok
}

var (
	allButtons = []MouseButton{
		MouseButton0,
		MouseButton1,
		MouseButton2,
		MouseButton3,
		MouseButton4,
	}
)

func (e *Engine) IsPressed(k Key) bool {
	keys := e.GetKeyboardKeys()
	for _, v := range keys {
		if v == k {
			return true
		}
	}

	return false
}

func (e *Engine) IsButtoned(b MouseButton) bool {
	_, ok := e.buttons[b]
	return ok
}

func (e *Engine) GetMouseWheel() mx.Vector {
	return e.wheel
}

func (e *Engine) cursorPosition() mx.Vector {
	x, y := ebiten.CursorPosition()
	return mx.Vector{mx.Float(x), mx.Float(y)}
}

// Get the real cursor position.
func (e *Engine) GetRealCursorPosition() mx.Vector {
	return e.cursorPos
}

// Get the absolute cursor position in the world
// of the engine.
func (e *Engine) GetAbsCursorPosition(
) mx.Vector {
	return e.GetRealCursorPosition().
		Apply(e.camera.GetAbsMatrice(Context{
			engine: e,
		}))
}

// Get the real window size in the current context.
func (e *Engine) GetRealWinSize() mx.Vector {
	var w, h int
	if e.wcfg.Fullscreen {
		w, h = ebiten.ScreenSizeInFullscreen()
	} else {
		w, h = e.wcfg.Width, e.wcfg.Height
	}
	return mx.Vector{
		mx.Float(w),
		mx.Float(h),
	}
}

func (e *Engine) GetAbsWinSize() mx.Vector {
	return e.camera.GetAbsWinSize(Context{
		engine: e,
	})
}
func (e *Engine) Runes() []rune {
	return e.runes
}

func (e *engine) updateEvents() Events {

	eng := (*Engine)(e)
	e.prevKeys = e.keys
	e.keys = inpututil.
		AppendPressedKeys(e.keys[:0])

	events := Events{}

	// Mouse buttons.
	btns := e.buttons
	for _, btn := range allButtons {
		if inpututil.IsMouseButtonJustPressed(btn) {
			btns[btn] = struct{}{}
			events.Mouse.ButtonDowns = append(
				events.Mouse.ButtonDowns,
				MouseButtonDown{
					MouseButton: btn,
				},
			)
		} else if inpututil.IsMouseButtonJustReleased(btn) {
			delete(btns, btn)
			events.Mouse.ButtonUps = append(
				events.Mouse.ButtonUps,
				MouseButtonUp{
					MouseButton: btn,
				},
			)
		}
	}

	// Mouse wheel.
	x, y := ebiten.Wheel()
	e.wheel = mx.Vector{x, y}
	if !(e.wheel.Eq(mx.ZV)) {
		events.Mouse.WheelChange = &WheelChange{
			Offset: e.wheel,
		}
	}

	// Cursor position.
	realPos := eng.cursorPosition()
	if !realPos.Eq(e.cursorPos) {
		absM := eng.camera.GetAbsMatrice(Context{
			engine: eng,
		})

		absPrevPos := e.cursorPos.Apply(absM)
		absPos := realPos.Apply(absM)

		events.Mouse.Move = &MouseMove{
			RealDelta: realPos.Sub(e.cursorPos),
			AbsDelta: absPos.Sub(absPrevPos),
		}
		e.cursorPos = realPos
	}

	// Keyboard.
	keyDiff := diffEm(e.prevKeys, e.keys)
	for _, key := range keyDiff {
		if eng.IsPressed(key) {
			events.Keyboard.KeyDowns = append(
				events.Keyboard.KeyDowns,
				KeyDown{
					Key: key,
				},
			)
		} else {
			events.Keyboard.KeyUps = append(
				events.Keyboard.KeyUps,
				KeyUp{
					Key: key,
				},
			)
		}
	}

	return events
}

func (e *engine) Update() error {

	eng := (*Engine)(e)
	e.dt = time.Since(e.lastTime)

	e.runes = ebiten.AppendInputChars(e.runes[:0])
	//fmt.Println("runes:", e.runes)
	// Buffering the context for faster.
	// Providing the events to the objects.
	// Maybe should think of the better way,
	// but for it is simple enough.

	events := e.updateEvents()
	c := Context{
		engine: eng,
		events: events,
	}
	for _, object := range e.objects.store {
		if object == nil {
			continue
		}
		object.OnUpdate(c)
	}

	e.lastTime = time.Now()
	e.frame++
	return nil
}


var (
	fullPageIndexes = func() [MaxVertices]uint16 {
		ret := [MaxVertices]uint16{}
		for i:=0 ; i<len(ret) ; i++ {
			ret[i] = uint16(i)
		}
		return ret
	}()
	defaultPageImg = func() *Image {
		img := NewImage(1, 1)
		img.Set(0, 0, RGBA(1, 1, 1, 1))
		return img
	}()
	defaultTriOptions = &ebiten.DrawTrianglesOptions{}
)
func (e *engine) Draw(img *ebiten.Image) {
	e.drawdt = time.Since(e.drawLastTime)
	eng := (*Engine)(e)
	m := map[Layer][]Drawer{}
	for _, object := range eng.objects.store {
		// Skipping the ones we do not need to draw.
		if object == nil || !object.IsVisible() {
			continue
		}
		l := object.GetLayer()
		layer, ok := m[l]
		// Create new if has no the layer
		if !ok {
			m[l] = []Drawer{object}
			continue
		}

		m[l] = append(layer, object)
	}

	// Drawing layers via the sparse array.
	// First drawing via the inside function
	// and then the returned []EVertex.
	layers := maps.NewSparse[Layer, []Drawer](nil, m)
	c := Context{engine: eng, image: img}
	for layer := range layers.Chan() {

		vertices := Vertices{}
		for _, drawer := range layer {
			drawing := drawer.Draw(c)
			if drawing != nil {
				vertices = append(
					vertices,
					drawing.Vertices...,
				)
			}
		}

		pn := len(vertices) / MaxVertices
		mod := len(vertices) % MaxVertices
		for i := 0 ; i<pn ; i++ {
			cur := i*MaxVertices
			img.DrawTriangles(
				vertices[cur:cur+MaxVertices].ToAPI(),
				fullPageIndexes[:],
				defaultPageImg,
				defaultTriOptions,
			)
		}

		st := pn*MaxVertices
		img.DrawTriangles(
			vertices[st:].ToAPI(),
			fullPageIndexes[:mod],
			defaultPageImg,
			defaultTriOptions,
		)
	}


	// Empty the buff to generate it again.
	e.drawLastTime = time.Now()
	e.dframe++
}

func (e *engine) Layout(ow, oh int) (int, int) {
	if e.wcfg.FixedSize {
		return e.wcfg.Width, e.wcfg.Height
	}

	return ow, oh
}

// Return the delta time between Draw calls.
func (e *Engine) DrawDT() Duration {
	return e.drawdt
}

// Current Drawing frame.
func (e *Engine) Dframe() uint {
	return e.dframe
}

// Return the real delta time.
// Please, prefer the DT().
func (e *Engine) RealDT() Duration {
	return e.dt
}

// Returns the current delta time. (btw frames)
// (By the Ebiten community convention
//  currently it is a fixed value)
func (e *Engine) DT() Duration {
	return time.Second/60
}

// Current frame 
func (e *Engine) Frame() uint {
	return e.frame
}

// Current FPS.
func (e *Engine) FPS() float64 {
	return ebiten.ActualFPS()
}

// Current TPS.
func (e *Engine) TPS() float64 {
	return ebiten.ActualTPS()
}

// Run the engine.
func (e *Engine) Run() error {
	ebiten.ReadDebugInfo(&e.wcfg.DebugInfo)
	ebiten.SetWindowTitle(e.wcfg.Title)
	ebiten.SetWindowSize(e.wcfg.Width, e.wcfg.Height)
	ebiten.SetWindowSizeLimits(1, 1, e.wcfg.Width, e.wcfg.Height)
	
	ebiten.SetFullscreen(e.wcfg.Fullscreen)
	ebiten.SetVsyncEnabled(e.wcfg.VSync)

	e.lastTime = time.Now()
	//fmt.Println(e.Objects)
	return ebiten.RunGameWithOptions((*engine)(e), e.wcfg.Options)
}

func (e *Engine) GetWindowConfig() WindowConfig {
	return e.wcfg
}