package assetstore

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

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
	maps map[string]*Map
	objs map[string]*Object
	sets map[string]*Set
}

// 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.entries = newEntryMap
	a.maps = make(map[string]*Map)
	a.objs = make(map[string]*Object)
	a.sets = make(map[string]*Set)

	return nil
}

func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
	filename := canonical(name + "." + ext)

	for _, dir := range dirs {
		dir = canonical(dir)
		log.Printf("Looking in canonical dir %s", dir)
		if base, ok := a.entries[dir]; ok {
			log.Printf("    base: %#+v", base)
			log.Printf("    filename: %v", filename)
			if file, ok := base[filename]; ok {
				actualDir := a.entries[RootDir][dir]
				return filepath.Join(a.RootDir, actualDir, file), nil
			}
		}
	}

	return "", os.ErrNotExist
}

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
}
