package flow

import (
	"errors"
	"fmt"
	"log"
	"strings"

	"github.com/hajimehoshi/ebiten"

	"code.ur.gs/lupine/ordoor/internal/assetstore"
	"code.ur.gs/lupine/ordoor/internal/config"
	"code.ur.gs/lupine/ordoor/internal/data"
	"code.ur.gs/lupine/ordoor/internal/scenario"
	"code.ur.gs/lupine/ordoor/internal/ship"
	"code.ur.gs/lupine/ordoor/internal/ui"
)

// type Flow is responsible for wiring up UI elements to each other and ensuring
// they behave as expected. This includes forward / back buttons to switch
// between screens, loading and saving options, launching a scenario, etc
type Flow struct {
	assets  *assetstore.AssetStore
	config  *config.Config
	current *ui.Driver
	drivers map[driverName]*ui.Driver
	generic *data.Generic

	// Some screens can be returned to from more than one place. Where this is
	// the case, instead of hardcoding it, we'll store an entry in here so we
	// know where we're going back to
	//
	// FIXME: this really suggests wiring everything up at the start is wrong.
	returns map[driverName]driverName

	// If we're currently playing a scenario, it it placed here
	scenario *scenario.Scenario

	ship *ship.Ship

	exit error
}

var (
	ErrExit = errors.New("exiting gracefully")

	// Constants used for sliders

	h3Slider = map[int]int{1: 8, 2: 56, 3: 110, 4: 120}

	v10Slider = map[int]int{
		0:  0,
		10: 9, 20: 18, 30: 27, 40: 36, 50: 45,
		60: 54, 70: 63, 80: 72, 90: 81, 100: 90,
	}

	h9Slider = map[int]int{
		0:  0,
		10: 10, 20: 20, 30: 30, 40: 40,
		50: 50, 60: 60, 70: 70, 80: 80,
	}
)

func New(assets *assetstore.AssetStore, config *config.Config, ship *ship.Ship) (*Flow, error) {
	generic, err := assets.Generic()
	if err != nil {
		return nil, fmt.Errorf("Failed to read generic data: %v", err)
	}

	out := &Flow{
		assets:  assets,
		config:  config,
		generic: generic,
		drivers: make(map[driverName]*ui.Driver, len(driverNames)),
		returns: make(map[driverName]driverName),
		ship:    ship,
	}

	// Load all the drivers upfront
	for _, name := range driverNames {
		driver, err := buildDriver(assets, name)
		if err != nil {
			return nil, err
		}

		out.drivers[name] = driver
	}

	out.linkDrivers()
	out.reset()

	return out, out.exit
}

func (f *Flow) Update(screenX, screenY int) error {
	if f.exit != nil {
		return f.exit
	}

	// Keybindings for map control
	// FIXME: this needs a big rethink
	if f.current != nil && f.scenario != nil && !f.current.IsInDialogue() {
		step := 32
		if ebiten.IsKeyPressed(ebiten.KeyLeft) {
			f.scenario.Viewpoint.X -= step
		}
		if ebiten.IsKeyPressed(ebiten.KeyRight) {
			f.scenario.Viewpoint.X += step
		}
		if ebiten.IsKeyPressed(ebiten.KeyUp) {
			f.scenario.Viewpoint.Y -= step
		}
		if ebiten.IsKeyPressed(ebiten.KeyDown) {
			f.scenario.Viewpoint.Y += step
		}
	}

	if f.scenario != nil {
		if err := f.scenario.Update(screenX, screenY); err != nil {
			return err
		}

	}

	if f.current != nil {
		if err := f.current.Update(screenX, screenY); err != nil {
			return err
		}
	}

	return nil
}

func (f *Flow) Draw(screen *ebiten.Image) error {
	if f.exit != nil {
		return f.exit
	}

	if f.scenario != nil {
		if err := f.scenario.Draw(screen); err != nil {
			return err
		}

	}

	if f.current != nil {
		if err := f.current.Draw(screen); err != nil {
			return err
		}
	}

	return nil
}

func (f *Flow) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
	if f.current != nil {
		return f.current.Cursor()
	}

	// FIXME: we should get a cursor from current all the time.
	return nil, nil, nil
}

func (f *Flow) linkDrivers() {
	// linkMain
	f.onClick(main, "2.1", f.setReturningDriver(main, newGame))  // New game
	f.onClick(main, "2.2", f.setReturningDriver(main, loadGame)) // Load game
	f.setFreeze(main, "2.3", true)                               // Multiplayer - disable for now
	f.onClick(main, "2.4", f.setReturningDriver(main, options))  // Options
	f.onClick(main, "2.5", f.setExit)                            // Quit

	// Now link immediate children. They will link their children, and so on
	f.linkNewGame()
	f.linkLoadGame()
	// TODO: link multiplayer
	f.linkOptions()
}

func maybeErr(driver driverName, err error) error {
	if err != nil {
		return fmt.Errorf("%v: %v", driver, err)
	}

	return nil
}

func (f *Flow) configureSlider(driver driverName, id string, steps map[int]int) {
	if f.exit != nil {
		return
	}

	f.exit = f.drivers[driver].ConfigureSlider(id, steps)
}

func (f *Flow) onClick(driver driverName, id string, fn func()) {
	if f.exit != nil {
		return
	}

	f.exit = maybeErr(driver, f.drivers[driver].OnClick(id, fn))
}

func (f *Flow) setFreeze(driver driverName, id string, value bool) {
	if f.exit != nil {
		return
	}

	f.exit = maybeErr(driver, f.drivers[driver].SetFreeze(id, value))
}

func (f *Flow) setValueBool(driver driverName, id string, value bool) {
	if f.exit != nil {
		return
	}

	f.exit = f.drivers[driver].SetValueBool(id, value)
}

func (f *Flow) valueBool(driver driverName, id string) bool {
	if f.exit != nil {
		return false
	}

	var value bool

	f.exit = f.drivers[driver].ValueBool(id, &value)

	return value
}

func (f *Flow) playNextScenario(from driverName) func() {
	return func() {
		log.Printf("Loading scenario: %v", f.ship.NextScenario)

		// TODO: we *could* load scenario assets in a separate assetstore to
		// make it easier to chuck them away at the end?
		scenario, err := scenario.NewScenario(f.assets, f.ship.NextScenario)
		if err != nil {
			f.exit = err
			return
		}

		f.setReturningDriverNow(from, mainGame)
		f.scenario = scenario
	}
}

func (f *Flow) setActive(driver driverName, id string, value bool) func() {
	return func() {
		if f.exit != nil {
			return
		}

		f.exit = maybeErr(driver, f.setActiveNow(driver, id, value))
	}
}

func (f *Flow) setActiveNow(driver driverName, id string, value bool) error {
	return f.drivers[driver].SetActive(locator(driver, id), value)
}

func (f *Flow) toggleActive(driver driverName, id string) func() {
	return func() {
		if f.exit != nil {
			return
		}

		f.exit = maybeErr(driver, f.drivers[driver].ToggleActive(locator(driver, id)))
	}
}

func (f *Flow) showDialogue(driver driverName, id string) func() {
	return func() {
		if f.exit != nil {
			return
		}

		f.exit = maybeErr(driver, f.drivers[driver].ShowDialogue(locator(driver, id)))
	}
}

func (f *Flow) hideDialogue(driver driverName) func() {
	return f.drivers[driver].HideDialogue
}

func (f *Flow) withScenario(then func()) func() {
	return func() {
		if f.scenario != nil {
			then()
		}
	}
}

func (f *Flow) reset() {
	if f.exit != nil {
		return
	}

	f.setDriverNow(main) // Back to the main interface

	// Wipe out any returns that may exist
	f.returns = make(map[driverName]driverName)

	// FIXME: these should really happen via data binding.
	f.resetLevelPlyInventorySelect()
	f.exit = f.configIntoOptions()
}

func (f *Flow) setExit() {
	f.exit = ErrExit
}

// TODO: convert all to locators
func locator(driver driverName, id string) string {
	return fmt.Sprintf("%v:%v", strings.ToLower(string(driver)), id)
}
