package assetstore

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"code.ur.gs/lupine/ordoor/internal/data"
	"code.ur.gs/lupine/ordoor/internal/idx"
)

const (
	RootDir = "" // Used in the entryMap for entries pertaining to the root dir
)

type entryMap map[string]map[string]string

// type AssetStore is responsible for lazily loading game data when it is
// required. Applications shouldn't need to do anything except set one of these
// up, pointing at the game dir root, to access all assets.
//
// Assets should be loaded on-demand to keep memory costs as low as possible.
// Cross-platform differences such as filename case sensitivity are also dealt
// with here.
//
// We assume the directory is read-only. You can run Refresh() if you make a
// change.
type AssetStore struct {
	RootDir string

	// Case-insensitive file lookup.
	// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
	entries entryMap

	// These members are used to store things we've already loaded
	aniObj    *Object
	cursorObj *Object
	cursors   map[CursorName]*Cursor
	fonts     map[string]*Font
	generic   *data.Generic
	idx       *idx.Idx
	maps      map[string]*Map
	menus     map[string]*Menu
	objs      map[string]*Object
	sets      map[string]*Set
	sounds    map[string]*Sound
	strings   *data.I18n
}

// New returns a new AssetStore
func New(dir string) (*AssetStore, error) {
	store := &AssetStore{
		RootDir: dir,
	}

	// fill entryMap
	if err := store.Refresh(); err != nil {
		return nil, err
	}

	return store, nil
}

func (a *AssetStore) Refresh() error {
	rootEntries, err := processDir(a.RootDir)
	if err != nil {
		return fmt.Errorf("failed to process %v: %v", a.RootDir, err)
	}

	newEntryMap := make(entryMap, len(rootEntries))
	newEntryMap[RootDir] = rootEntries

	for lower, natural := range rootEntries {
		path := filepath.Join(a.RootDir, natural)
		fi, err := os.Stat(path)
		if err != nil {
			return fmt.Errorf("Failed to stat %v: %v", path, err)
		}

		if fi.IsDir() {
			entries, err := processDir(path)
			if err != nil {
				return fmt.Errorf("Failed to process %v: %v", path, err)
			}

			newEntryMap[lower] = entries
		}
	}

	// Refresh
	a.aniObj = nil
	a.cursorObj = nil
	a.cursors = make(map[CursorName]*Cursor)
	a.entries = newEntryMap
	a.fonts = make(map[string]*Font)
	a.idx = nil
	a.maps = make(map[string]*Map)
	a.menus = make(map[string]*Menu)
	a.objs = make(map[string]*Object)
	a.sets = make(map[string]*Set)
	a.sounds = make(map[string]*Sound)
	a.strings = nil

	return nil
}

func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
	var filename string
	if ext != "" {
		filename = canonical(name + "." + ext)
	} else {
		filename = canonical(name)
	}

	for _, dir := range dirs {
		dir = canonical(dir)
		if base, ok := a.entries[dir]; ok {
			if file, ok := base[filename]; ok {
				actualDir := a.entries[RootDir][dir]
				return filepath.Join(a.RootDir, actualDir, file), nil
			}
		}
	}

	return "", fmt.Errorf("file %q does not exist", filename)
}

func canonical(s string) string {
	return strings.ToLower(s)
}

func processDir(dir string) (map[string]string, error) {
	entries, err := ioutil.ReadDir(dir)
	if err != nil {
		return nil, err
	}

	out := make(map[string]string, len(entries))
	for _, entry := range entries {
		if entry.Name() == "." || entry.Name() == ".." {
			continue
		}

		out[canonical(entry.Name())] = entry.Name()
	}

	return out, nil
}
