package client

import (
	"image"
	"sync"

	"galaxy/client/updater"
	"galaxy/client/widget/calculator"
	"galaxy/client/world"
	"galaxy/connector"
	mc "galaxy/model/client"
	"galaxy/storage"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/lang"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

const version = "1.0.0"

type client struct {
	s      storage.Storage
	conn   connector.Connector
	app    fyne.App
	window fyne.Window

	state   *mc.State
	stateMu sync.RWMutex

	reg *registry

	calculator  *calculator.Calculator
	mapSplitter *container.Split
	accInfo     *widget.AccordionItem
	accCalc     *widget.AccordionItem

	// loadReportFunc func(uint)

	world  *world.World
	drawer *world.GGDrawer
	raster *canvas.Raster
	co     *RasterCoalescer[world.RenderParams]
	pan    *PanController

	// Protected camera/options state (UI-facing). This is the "base" params snapshot.
	// Viewport/margins are NOT stored here; they come from raster draw callback.
	mu          sync.RWMutex
	wp          *world.RenderParams
	canvasScale float32

	// Latest raster geometry metadata for correct event->pixel conversion:
	// - logical size: raster.Size() (Fyne units)
	// - pixel size: last (wPx,hPx) passed to draw callback
	metaMu           sync.RWMutex
	lastRasterLogicW float32
	lastRasterLogicH float32
	lastRasterPxW    int
	lastRasterPxH    int
	lastCanvasScale  float32 // optional, useful for debugging

	// Snapshot of params actually used for the last render (includes viewport/margins).
	// Used for HitTest and to keep UI interactions consistent with what the user sees.
	lastRenderedMu     sync.RWMutex
	lastRenderedParams world.RenderParams

	// Indexing / backing-canvas caches (owned by client because it depends on UI geometry)
	lastIndexedViewportW int
	lastIndexedViewportH int
	lastIndexedZoomFp    int

	lastCanvasW int
	lastCanvasH int

	viewportImg *image.RGBA
	viewportW   int
	viewportH   int

	hits []world.Hit

	updater        *updater.Manager
	backgroundStop chan struct{}
	backgroundOnce sync.Once

	onConnectionFn    func(bool)
	onConnectionErrFn func(error)
	onStorageErrFn    func(error)
	onServiceErrFn    func(error)
}

func NewClient(s storage.Storage, conn connector.Connector, app fyne.App) (mc.Client, error) {
	e := &client{
		s:               s,
		conn:            conn,
		app:             app,
		window:          app.NewWindow("Galaxy Plus"),
		reg:             newRegistry(),
		lastCanvasScale: 1.0,
		world:           nil,
		hits:            make([]world.Hit, 5),
		backgroundStop:  make(chan struct{}),
	}
	e.calculator = calculator.NewCaclulator(calculator.WithCreateHandler(e.createShipClass))
	e.updater = updater.NewManager(e.s, e.conn)

	stateExists, err := e.s.StateExists()
	if err != nil {
		return nil, err
	}
	if stateExists {
		state, err := e.s.LoadState()
		if err != nil {
			return nil, err
		}
		e.state = &state
	} else {
		e.state = &mc.State{
			ClientCurrentVersion: e.Version(),
			CameraZoom:           1.0,
			MapSplitterOffset:    0.5,
			AccordionInfoOpen:    false,
			AccordionCalcOpen:    false,
		}
		if err := e.s.SaveState(*e.state); err != nil {
			return nil, err
		}
	}
	if e.state.CameraZoom <= 0 {
		e.state.CameraZoom = 1.0
	}
	if e.state.MapSplitterOffset <= 0 {
		e.state.MapSplitterOffset = 0.5
	}
	e.wp = &world.RenderParams{
		Options:        &world.RenderOptions{DisableWrapScroll: false},
		CameraZoom:     e.state.CameraZoom,
		CameraXWorldFp: e.state.CameraXFp,
		CameraYWorldFp: e.state.CameraYFp,
	}

	e.drawer = &world.GGDrawer{DC: nil}

	e.raster = canvas.NewRaster(func(wPx, hPx int) image.Image {
		return e.draw(wPx, hPx)
	})

	e.pan = NewPanController(e)

	e.co = NewRasterCoalescer(
		FyneExecutor{},
		e.raster,
		func(wPx, hPx int, p world.RenderParams) image.Image {
			return e.renderRasterImage(wPx, hPx, p)
		},
	)

	return e, nil
}

func (e *client) BuildUI(w fyne.Window) {
	mapCanvasObject := newInteractiveRaster(e.raster, e.onRasterWidgetLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped)

	toolbar := widget.NewToolbar(
		widget.NewToolbarAction(
			theme.FolderIcon(),
			func() { e.initReportAsync("GAME_ID", 0) }),
		widget.NewToolbarSeparator(),
		widget.NewToolbarAction(
			theme.NavigateBackIcon(),
			func() {}),
		widget.NewToolbarAction(
			theme.NavigateNextIcon(),
			func() {}),
	)

	e.accInfo = widget.NewAccordionItem(lang.L("title.info"), container.NewStack())
	e.accInfo.Open = e.state.AccordionInfoOpen
	e.accCalc = widget.NewAccordionItem(lang.L("title.calculator"), e.calculator.CanvasObject)
	e.accCalc.Open = e.state.AccordionCalcOpen

	accordion := widget.NewAccordion()
	accordion.MultiOpen = true
	accordion.Append(e.accCalc)
	accordion.Append(e.accInfo)

	e.mapSplitter = container.NewHSplit(mapCanvasObject, container.NewHScroll(accordion))
	e.mapSplitter.SetOffset(e.state.MapSplitterOffset)

	tabs := container.NewAppTabs(
		container.NewTabItemWithIcon(
			lang.L("title.map"),
			theme.GridIcon(),
			e.mapSplitter),
		container.NewTabItemWithIcon(
			"Calculator",
			theme.ComputerIcon(),
			container.NewStack(widget.NewButton("Calc", func() {})),
		),
	)

	th := tabs.Theme()
	icon := canvas.NewImageFromResource(th.Icon(theme.IconNameInfo))

	statusLeft := widget.NewTextGridFromString("Status")
	statusAd := widget.NewTextGridFromString("")

	statusBar := container.NewBorder(
		nil, // top
		nil, // bottom
		container.NewHBox(statusLeft, widget.NewSeparator()), // left
		container.NewHBox(widget.NewSeparator(), icon),       // right
		statusAd, // center
	)

	content := container.NewBorder(
		toolbar,   // top
		statusBar, // bottom
		nil,       // left
		nil,       // right
		tabs,      // center
	)

	w.CenterOnScreen()
	w.SetContent(content)
	s := statusBar.Size()
	icon.SetMinSize(fyne.NewSize(s.Height, s.Height))
	e.initLatestReport()
}

func (e *client) loadWorld(w *world.World) {
	if w == nil {
		return
	}
	w.SetCircleRadiusScaleFp(world.SCALE / 1000)
	e.world = w
	// TODO: store camera position in user settings
	e.wp.CameraXWorldFp = w.W / 2
	e.wp.CameraYWorldFp = w.H / 2
	e.world.SetTheme(world.ThemeDark)

	e.RequestRefresh()
}

func (e *client) Run() error {
	e.BuildUI(e.window)
	e.startBackground()
	e.RequestRefresh()
	e.window.SetMaster()
	e.window.Resize(fyne.NewSize(800, 600))
	e.window.CenterOnScreen()
	e.window.SetOnClosed(e.Shutdown)
	e.window.ShowAndRun()
	return nil
}

func (e *client) Shutdown() {
	e.stopBackground()
	e.ensureStatePersist()
	e.window.Close()
}

// TODO: remove func?
func (e *client) Version() string { return version }

func (e *client) OnConnection(isGood bool) {
	if e.onConnectionFn != nil {
		e.onConnectionFn(isGood)
	}
}

func (e *client) OnConnectionError(err error) {
	if e.onConnectionErrFn != nil {
		e.onConnectionErrFn(err)
	}
}

func (e *client) OnStorageError(err error) {
	if e.onStorageErrFn != nil {
		e.onStorageErrFn(err)
	}
}

func (e *client) OnServiceError(err error) {
	if e.onServiceErrFn != nil {
		e.onServiceErrFn(err)
	}
}
