package menus

import (
	"fmt"
	"image"
	"image/color"
	"io/ioutil"
	"path/filepath"
	"strconv"
	"strings"

	"code.ur.gs/lupine/ordoor/internal/palettes"
	"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
)

// MenuType tells us what sort of Group we have
type MenuType int

// SubMenuType tells us what sort of Record we have
type SubMenuType int

const (
	TypeStatic         MenuType = 0
	TypeMenu           MenuType = 1
	TypeDragMenu       MenuType = 2  // Only seen in Configure_Vehicle_{Chaos,Ultra}
	TypeRadioMenu      MenuType = 3  // ???
	TypeMainBackground MenuType = 45 // ???
	TypeDialogue       MenuType = 300

	SubTypeSimpleButton    SubMenuType = 3
	SubTypeDoorHotspot1    SubMenuType = 30 // Like a button I guess? "FONTTYPE is animation speed"
	SubTypeDoorHotspot2    SubMenuType = 31 // Seems like a duplicate of the above? What's different?
	SubTypeLineKbd         SubMenuType = 40
	SubTypeLineBriefing    SubMenuType = 41
	SubTypeThumb           SubMenuType = 45 // A "thumb" appears to be a vertical slider
	SubTypeInvokeButton    SubMenuType = 50
	SubTypeDoorHotspot3    SubMenuType = 60 // Maybe? Appears in Arrange.mnu
	SubTypeOverlay         SubMenuType = 61
	SubTypeHypertext       SubMenuType = 70
	SubTypeCheckbox        SubMenuType = 91
	SubTypeEditBox         SubMenuType = 100
	SubTypeInventorySelect SubMenuType = 110
	SubTypeRadioButton     SubMenuType = 120
	SubTypeDropdownButton  SubMenuType = 200
	SubTypeComboBoxItem    SubMenuType = 205
	SubTypeAnimationSample SubMenuType = 220
	SubTypeAnimationHover  SubMenuType = 221 // FONTTYPE is animation speed. Only animate when hovered
	SubTypeMainButton      SubMenuType = 228
	SubTypeSlider          SubMenuType = 232
	SubTypeStatusBar       SubMenuType = 233

	SubTypeListBoxUp   SubMenuType = 400 // FIXME: these have multiple items in SUBMENUTYPE
	SubTypeListBoxDown SubMenuType = 405
)

// FIXME: certain elements - especially overlays - don't have a DESC specified
// in the .mnu file, but display text specified with a number in i18n. The only
// conclusion I can draw is that they're hardcoded in the binary and set from
// outside. So, do that here.
var DescOverrides = map[string]int{
	"main:2.6":     50992,
	"newgame:2.5":  50993,
	"keyboard:3.3": 50995,
	"levelply:2.6": 50996,
}

// FIXME: Same idea with text overrides, only these aren't mentioned in the .dta
// file at all!
var TextOverrides = map[string]string{
	"main:2.7": "0.1-ordoor",
}

var TypeOverrides = map[string]SubMenuType{
	// FIXME: These are put down as simple buttons, but it's a *lot* easier to
	// understand them as list box buttons.
	"configure_ultequip:7.5": SubTypeListBoxUp,
	"configure_ultequip:7.6": SubTypeListBoxDown,
}

type Menu struct {
	Name string
	// TODO: load these
	ObjectFiles []string
	FontNames   []string

	// These are properties set in the menu header. We don't know what they're
	// all for.
	BackgroundColor color.Color
	HypertextColor  color.Color
	FontType        int

	// The actual menu records. There are multiple top-level items. Submenus are
	// only ever nested one deep.
	Groups []*Group
}

// Group represents an element with a MENUTYPE. It is part of a Menu and may
// have children.
type Group struct {
	Menu    *Menu
	Records []*Record

	Properties
	Type MenuType
}

type Record struct {
	Menu  *Menu
	Group *Group

	Properties
	Type SubMenuType
}

type Properties struct {
	Locator string // Not strictly a property. Set for tracking.

	ID        int
	ObjectIdx int // Can be specified in MENUID, defaults to 0

	Accelerator int
	Active      bool
	Desc        string
	DrawType    int
	FontType    int
	Moveable    bool
	Share       int
	SoundType   int
	SpriteId    []int
	X           int
	Y           int

	// From i18n
	Text string
	Help string
}

func (p *Properties) Point() image.Point {
	if p.X > 0 || p.Y > 0 {
		return image.Pt(p.X, p.Y)
	}

	return image.Point{}
}

func LoadMenu(filename string) (*Menu, error) {
	name := filepath.Base(filename)
	name = strings.TrimSuffix(name, filepath.Ext(name))
	name = strings.ToLower(name)

	scanner, err := asciiscan.New(filename)
	if err != nil {
		return nil, err
	}

	defer scanner.Close()

	out := &Menu{
		Name: name,
	}

	if err := loadObjects(out, scanner); err != nil {
		return nil, err
	}

	if err := loadProperties(out, scanner); err != nil {
		return nil, err
	}

	if err := loadFonts(out, scanner); err != nil {
		return nil, err
	}

	if err := loadRecords(filepath.Dir(filename), out, scanner); err != nil {
		return nil, err
	}

	return out, nil
}

func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error {
	strs, err := scanner.ConsumeStringList()
	if err != nil {
		return err
	}

	menu.ObjectFiles = strs

	return nil
}

func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
	for {
		ok, err := scanner.PeekProperty()

		if err != nil {
			return err
		}
		if !ok {
			break
		}

		k, v, err := scanner.ConsumeProperty()
		if err != nil {
			return err
		}

		vInt, err := strconv.Atoi(v) // All properties have been int
		if err != nil {
			return err
		}

		// DeBrief.mnu misspells these
		parts := strings.SplitN(strings.ToUpper(k), " ", 3)
		if len(parts) > 2 {
			k = strings.Join(parts[0:2], " ")
		}

		switch strings.ToUpper(k) {
		case "BACKGROUND COLOR":
			menu.BackgroundColor = palettes.DefaultPalette()[vInt]
		case "HYPERTEXT COLOR":
			menu.HypertextColor = palettes.DefaultPalette()[vInt]
		case "FONT TYPE":
			menu.FontType = vInt
		default:
			return fmt.Errorf("Unhandled menu property in %v: %q=%q", menu.Name, k, v)
		}
	}

	return nil
}

func loadFonts(menu *Menu, scanner *asciiscan.Scanner) error {
	// FIXME: Can we just ignore NULL, or does the index matter?
	strs, err := scanner.ConsumeStringList("NULL")
	if err != nil {
		return err
	}

	menu.FontNames = strs

	return nil
}

func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
	// We build things up line by line in these variables
	var group *Group
	var record *Record
	var properties *Properties

	for {
		str, err := scanner.ConsumeString()
		if err != nil {
			return err
		}

		if strings.HasPrefix(str, "$") {
			subScanner, err := asciiscan.New(filepath.Join(baseDir, str[1:]))
			if err != nil {
				return err
			}

			err = loadRecords(baseDir, menu, subScanner)
			subScanner.Close() // Don't keep this around for all of loadRecords

			if err != nil {
				return fmt.Errorf("Processing child %q: %v", str, err)
			}

			continue
		}

		if str == "*" {
			if record != nil {
				group.Records = append(group.Records, record)
				record = nil
			}

			if group != nil {
				menu.Groups = append(menu.Groups, group)
				group = nil
			}

			continue // New group
		}

		if str == "~" {
			break // THE END
		}

		k, v := asciiscan.ConsumeProperty(str)
		switch strings.ToUpper(k) {
		case "MENUID":
			if group != nil {
				menu.Groups = append(menu.Groups, group)
			}

			group = newGroup(menu, v)
			properties = &group.Properties
		case "SUBMENUID":
			if record != nil {
				group.Records = append(group.Records, record)
			}

			record = newRecord(group, v)
			properties = &record.Properties
		case "MENUTYPE":
			group.setMenuType(v)
		case "SUBMENUTYPE":
			record.setSubMenuType(v)
		default:
			if err := properties.setProperty(k, v); err != nil {
				return err
			}
		}
	}

	return nil
}

func LoadMenus(dir string) (map[string]*Menu, error) {
	fis, err := ioutil.ReadDir(dir)
	if err != nil {
		return nil, err
	}

	out := make(map[string]*Menu, len(fis))

	for _, fi := range fis {
		relname := fi.Name()
		basename := filepath.Base(relname)
		extname := filepath.Ext(relname)

		// Skip anything that isn't a .mnu file
		if !strings.EqualFold(extname, ".mnu") {
			continue
		}

		built, err := LoadMenu(filepath.Join(dir, relname))
		if err != nil {
			return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err)
		}

		out[basename] = built
	}

	return out, nil
}

func listOfInts(s string) []int {
	vSplit := strings.Split(s, ",")
	vSplitInt := make([]int, len(vSplit))

	for i, subV := range vSplit {
		vSplitInt[i], _ = strconv.Atoi(subV)
	}

	return vSplitInt
}

func newGroup(menu *Menu, idStr string) *Group {
	out := &Group{Menu: menu}

	// ObjectIdx can be specified in the MENUID. Only seen for .mni files
	ints := listOfInts(idStr)
	out.ID = ints[0]
	if len(ints) > 1 {
		out.ObjectIdx = ints[1]
	}

	out.Locator = fmt.Sprintf("%v:%v", menu.Name, out.ID)

	return out
}

func newRecord(group *Group, idStr string) *Record {
	out := &Record{Group: group}

	out.ID, _ = strconv.Atoi(idStr) // FIXME: we're ignoring conversion errors here
	out.ObjectIdx = group.ObjectIdx // FIXME: we shouldn't *copy* this

	out.Locator = fmt.Sprintf("%v.%v", group.Locator, out.ID)

	return out
}

func (g *Group) setMenuType(s string) {
	v, _ := strconv.Atoi(s) // FIXME: conversion errors
	g.Type = MenuType(v)
}

func (r *Record) setSubMenuType(s string) {
	// FIXME: Type overrides shouldn't be necessary!
	if override, ok := TypeOverrides[r.Locator]; ok {
		r.Type = override
		return
	}

	// FIXME: what are the other types here? Related to list boxes?
	ints := listOfInts(s)
	r.Type = SubMenuType(ints[0])
}

func (p *Properties) setProperty(k, v string) error {
	ints := listOfInts(v)
	vInt := ints[0]
	asBool := (vInt != 0)

	switch strings.ToUpper(k) {
	case "ACCELERATOR":
		p.Accelerator = vInt
	case "ACTIVE":
		p.Active = asBool
	case "DESC":
		p.Desc = v // Usually int, occasionally string
	case "DRAW TYPE":
		p.DrawType = vInt
	case "FONTTYPE":
		p.FontType = vInt
	case "MOVEABLE":
		p.Moveable = asBool
	case "SOUNDTYPE":
		p.SoundType = vInt
	case "SPRITEID":
		p.SpriteId = ints
	case "X-CORD":
		p.X = vInt
	case "Y-CORD":
		p.Y = vInt
	case "SHARE":
		p.Share = vInt

	default:
		return fmt.Errorf("Unknown property for %v: %v=%v", p.Locator, k, v)
	}

	return nil
}

type Replacer interface {
	ReplaceText(int, *string)
	ReplaceHelp(int, *string)
}

func (r *Record) Internationalize(replacer Replacer) {
	if override, ok := TextOverrides[r.Locator]; ok {
		r.Text = override
		return
	}

	if override, ok := DescOverrides[r.Locator]; ok {
		r.Desc = strconv.Itoa(override)
	}

	id, err := strconv.Atoi(r.Desc)
	if err == nil {
		replacer.ReplaceText(id, &r.Text)
		replacer.ReplaceHelp(id, &r.Help)
	} else {
		r.Text = r.Desc // Sometimes it's a string like "EQUIPMENT"
	}
}

func (m *Menu) Internationalize(replacer Replacer) {
	for _, group := range m.Groups {
		for _, record := range group.Records {
			record.Internationalize(replacer)
		}
	}
}

func (g *Group) Props() *Properties {
	return &g.Properties
}

func (r *Record) Props() *Properties {
	return &r.Properties
}

func (p *Properties) BaseSpriteID() int {
	base := p.Share

	// SpriteId takes precedence if present
	if len(p.SpriteId) > 0 && p.SpriteId[0] >= 0 {
		base = p.SpriteId[0]
	}

	return base
}
