package gg

import (
	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
	"github.com/di4f/gods/maps"
	//"fmt"
	"time"
	"slices"
	"sync"
)

const (
	LayerBufSize = 100
)

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

// The type represents order of drawing.
// Higher values are drawn later.
type Layer float64

func (l Layer) GetLayer() Layer {
	return l
}

// 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
}

type Objects struct {
	store []Objecter
}

// 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

	// The same delta time for all frames
	// and all objects.
	lastTime time.Time
	dt Float

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

	bufs [LayerBufSize]*Image
}

type engine Engine

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

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

// Returns currently pressed buttons.
func (e *Engine) MouseButtons() []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 {
	/*w := Float(cfg.Width)
	h := Float(cfg.Height)*/

	ret := &Engine{}

	ret.wcfg = cfg
	ret.Camera = ret.NewCamera()
	ret.outerEvents = make(EventChan)
	ret.handleEvents = make(EventChan)
	ret.Objects = &Objects{}
	ret.buttons = MouseButtonMap{}

	siz := ret.RealWinSize()
	w, h := int(siz.X), int(siz.Y)
	for i:=0 ; i<len(ret.bufs) ; i++ {
		ret.bufs[i] = NewImage(w, h)
	}
	return ret
}

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

func (c *Engine) AbsWinSize() Vector {
	return c.RealWinSize().Div(c.Camera.Scale)
}

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

// Add new object considering what
// interfaces it implements.
func (e *Engine) Spawn(b Objecter) error {
	/*if e.Objects.Has(b) {
		return ObjectExistErr
	}*/

	b.Start(&Context{Engine: e})
	obj := b.GetObject()
	obj.input = make(chan *Context)
	go func() {
		for c := range obj.input {
			b.Update(c)
			e.wg.Done()
		}
	}()
	e.Objects.store = append(e.Objects.store, b)

	return nil
}

// Delete object from Engine.
func (e *Engine) Del(b Objecter) error {
	/*if !e.Objects.Has(b) {
		return ObjectNotExistErr
	}

	b.Delete(&Context{Engine: e})
	e.Objects.Del(b)*/

	return nil
}

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

func (e *Engine) IsPressed(k Key) bool {
	keys := e.Keys()
	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) Wheel() Vector {
	return e.wheel
}

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

func (e *Engine) CursorPosition() Vector {
	return e.cursorPos
}

func (e *Engine) AbsCursorPosition() Vector {
	return e.CursorPosition().Apply(e.Camera.AbsMatrix())
}

func (e *engine) Update() error {
	eng := (*Engine)(e)

	// Buffering the context for faster.

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

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

	x, y := ebiten.Wheel()
	eng.wheel = V(x, y)
	if !(eng.wheel.Eq(ZV)) {
		events = append(events, &WheelChange{
			Offset: eng.wheel,
		})
	}

	keyDiff := diffEm(e.prevKeys, e.keys)
	for _, key := range keyDiff {
		var event any
		if eng.IsPressed(key) {
			event = &KeyDown{
				Key: key,
			}
		} else {
			event = &KeyUp{
				Key: key,
			}
		}
		events = append(events, event)
	}

	realPos := eng.cursorPosition()
	if !realPos.Eq(eng.cursorPos) {
		absM := eng.Camera.AbsMatrix()

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

		events = append(events, &MouseMove{
			Real: realPos.Sub(eng.cursorPos),
			Abs: absPos.Sub(absPrevPos),
		})
		eng.cursorPos = realPos
	}

	// Providing the events to the objects.
	// Maybe should think of the better way,
	// but for it is simple enough.
	c := &Context{
		Engine: eng,
		typ: updateContext,
		Events: events,
	}
	for _, object := range e.Objects.store {
		e.wg.Add(1)
		object.Input() <- c
	}
	e.wg.Wait()

	return nil
}

func (e *engine) Draw(image *ebiten.Image) {
	var wg sync.WaitGroup
	e.dt = time.Since(e.lastTime).Seconds()
	eng := (*Engine)(e)
	m := map[Layer][]Drawer{}
	for _, object := range eng.Objects.store {
		// Skipping the ones we do not need to draw.
		if !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.
	layers := maps.NewSparse[Layer, []Drawer](nil, m)
	ln := layers.Size()
	pageN := ln / LayerBufSize
	mod := ln % LayerBufSize
	chn := layers.Chan()
	opts := &ebiten.DrawImageOptions{GeoM: Matrix{}}
	for n := 0 ; n<pageN ; n++ {
		for i:=0 ; i<LayerBufSize ; i++ {
			layer := <-chn
			buf := e.bufs[i]
			c := &Context{Engine: eng, typ: drawContext, Image: buf}
			wg.Add(1)
			go func() {
				for _, drawer := range layer {
					drawer.Draw(c)
				}
				wg.Done()
			}()
		}
		wg.Wait()
		for _, buf := range e.bufs {
			image.DrawImage(buf, opts)
			buf.Clear()
		}
	}

	for i:=0 ; i<mod ; i++ {
		layer := <-chn
		buf := e.bufs[i]
		c := &Context{Engine: eng, typ: drawContext, Image: buf}
		wg.Add(1)
		go func() {
			for _, drawer := range layer {
				drawer.Draw(c)
			}
			wg.Done()
		}()
	}
	wg.Wait()
	for i:=0 ; i<mod ; i++ {
		buf := e.bufs[i]
		image.DrawImage(buf, opts)
		buf.Clear()
	}

	// Empty the camera buffer to generate it again.
	eng.Camera.buffered = false

	e.lastTime = time.Now()
}

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 duration value.
func (e *Engine) DT() Float {
	return e.dt
}
func (e *Engine) FPS() float64 {
	return ebiten.ActualFPS()
}

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

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)
}