package maps

import (
	"bytes"
	"compress/gzip"
	"encoding/binary"
	"fmt"
	"image"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"
)

var (
	expectedMagic         = []byte("\x15\x00AMB_MAP\x00")
	expectedSetNameOffset = uint32(0x10)
	notImplemented        = fmt.Errorf("Not implemented")
)

const (
	MaxHeight = 7   // Z coordinate
	MaxLength = 100 // Y coordinate
	MaxWidth  = 130 // X coordinate

	CellSize = 13 // seems to be

	cellDataOffset = 0xc0
	cellCount      = MaxHeight * MaxLength * MaxWidth
)

type Header struct {
	Magic   [10]byte // "\x15\x00AMB_MAP\x00"
	SetName [8]byte  // Links to a filename in `/Sets/*.set`
	// Need to investigate the rest of the header too
	IsCampaignMap byte
}

func (h Header) Width() int {
	return MaxWidth
}

func (h Header) Length() int {
	return MaxLength
}

func (h Header) Height() int {
	return MaxHeight
}

func (h Header) MapSetName() string {
	idx := bytes.IndexByte(h.SetName[:], 0)
	if idx < 0 {
		idx = 8 // all 8 bytes are used
	}

	return string(h.SetName[0:idx:idx])
}

func (h Header) MapSetFilename() string {
	return h.MapSetName() + ".set"
}

type ObjRef struct {
	AreaByte          byte
	SpriteAndFlagByte byte
}

// The index into a set palette to retrieve the object
func (o ObjRef) Index() int {
	return int(o.AreaByte & 0x7f)
}

func (o ObjRef) Sprite() int {
	// The top bit seems to be a flag of some kind
	return int(o.SpriteAndFlagByte & 0x7f)
}

// The top bit seems to say whether we should draw or not.
func (o ObjRef) IsActive() bool {
	return (o.SpriteAndFlagByte & 0x80) == 0x80
} // PARIS is 78 x 60 x 7
//          4E   3C   7
/*
type Cell struct {
	DoorAndCanisterRelated    byte
//	DoorLockAndReactorRelated byte
//	Unknown2                  byte
	Surface                   ObjRef
	Left                      ObjRef
	Right                     ObjRef
	Center                    ObjRef
	Unknown11                 byte
	Unknown12                 byte
	Unknown13                 byte
	Unknown14                 byte
//	SquadRelated              byte
}*/

type Cell struct {
	Unknown  byte
	Surface  ObjRef
	Left     ObjRef
	Right    ObjRef
	Center   ObjRef
	Unknown2 [4]byte

	/*
	   	DoorAndCanisterRelated    byte
	   //	DoorLockAndReactorRelated byte
	   //	Unknown2                  byte
	   	Surface                   ObjRef
	   	Left                      ObjRef
	   	Right                     ObjRef
	   	Center                    ObjRef
	   	Unknown11                 byte
	   	Unknown12                 byte
	   	Unknown13                 byte
	   	Unknown14                 byte
	   	SquadRelated              byte*/
}

/*
func (c *Cell) At(n int) byte {
	return c.Unknown[n]
}
*/
/*
func (c *Cell) At(n int) byte {
	switch n {
	case 0:
		return c.DoorAndCanisterRelated
	case 1:
		return c.DoorLockAndReactorRelated

	case 2:
		return c.Unknown2
	case 3:
		return c.Surface.AreaByte
	case 4:
		return c.Surface.SpriteAndFlagByte
	case 5:
		return c.Left.AreaByte
	case 6:
		return c.Left.SpriteAndFlagByte
	case 7:
		return c.Right.AreaByte
	case 8:
		return c.Right.SpriteAndFlagByte
	case 9:
		return c.Center.AreaByte
	case 10:
		return c.Center.SpriteAndFlagByte
	case 11:
		return c.Unknown11
	case 12:
		return c.Unknown12

	case 13:
		return c.Unknown13
	case 14:
		return c.Unknown14
	case 15:
		return c.SquadRelated

	}

	return 0
}
*/
// Cells is always a fixed size; use At to get a cell according to x,y,z
type Cells []Cell

// 6 Possibilities for being laid out in memory. Most likely:
// XXYYZZ
// OR
// XYZXYZ

func (c Cells) At(x, y, z int) Cell {
	//	log.Printf("At (%v,%v,%v)=%v", x, y, z, x*y*z)
	return c[(z*MaxLength*MaxWidth)+
		(y*MaxWidth)+
		x]
}

func (h Header) Check() []error {
	var out []error
	//	if h.IsCampaignMap > 1 {
	//		out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
	//	}

	if bytes.Compare(expectedMagic, h.Magic[:]) != 0 {
		out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
	}

	return out
}

type GameMap struct {
	Header
	Cells
	// TODO: parse this into sections
	Text string
}

func (m *GameMap) Rect() image.Rectangle {
	return image.Rect(
		int(0),
		int(0),
		int(m.Width()-1),
		int(m.Length()-1),
	)
}

// A game map contains a .txt and a .map. If they're in the same directory,
// just pass the directory + basename to load both
func LoadGameMap(prefix string) (*GameMap, error) {
	for _, txtExt := range []string{".TXT", ".txt"} {
		for _, mapExt := range []string{".MAP", ".map"} {
			out, err := LoadGameMapByFiles(prefix+mapExt, prefix+txtExt)
			if err != nil && !os.IsNotExist(err) {
				return nil, err
			}

			if err == nil {
				return out, nil
			}
		}
	}

	return nil, fmt.Errorf("Couldn't find %s.{map,txt}, even ignoring case", prefix)
}

// A game map is composed of two files: .map and .txt
func LoadGameMapByFiles(mapFile, txtFile string) (*GameMap, error) {
	out, err := loadMapFile(mapFile)
	if err != nil {
		return nil, err
	}

	// TODO: load text and parse into sections
	txt, err := ioutil.ReadFile(txtFile)
	if err != nil {
		return nil, err
	}
	out.Text = string(txt)

	for _, err := range out.Check() {
		log.Printf("%s: %v", mapFile, err)
	}

	return out, nil
}

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

	out := make(map[string]*GameMap)

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

		// Only pay attention to .MAP files. Assume they will have a .txt too
		if !strings.EqualFold(extname, ".MAP") {
			continue
		}

		prefix := filename[0 : len(filename)-4]

		gameMap, err := LoadGameMap(prefix)
		if err != nil {
			return nil, fmt.Errorf("Error processing %v: %s", filename, err)
		}

		out[basename] = gameMap
	}

	return out, nil
}

func loadMapFile(filename string) (*GameMap, error) {
	var out GameMap

	mf, err := os.Open(filename)
	if err != nil {
		return nil, err
	}

	defer mf.Close()

	zr, err := gzip.NewReader(mf)
	if err != nil {
		return nil, err
	}

	defer zr.Close()

	if err := binary.Read(zr, binary.LittleEndian, &out.Header); err != nil {
		return nil, fmt.Errorf("Error parsing %s: %v", filename, err)
	}

	// no gzip.SeekReader, so discard unread header bytes for now
	discardSize := int64(cellDataOffset - binary.Size(&out.Header))
	if _, err := io.CopyN(ioutil.Discard, zr, discardSize); err != nil {
		return nil, err
	}

	out.Cells = make(Cells, cellCount)
	if err := binary.Read(zr, binary.LittleEndian, &out.Cells); err != nil {
		return nil, fmt.Errorf("Error parsing cells for %s: %v", filename, err)
	}

	return &out, nil
}
