package gen7

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"strings"
	"time"
	"unicode/utf16"

	p "github.com/ajswis/go-pkparse-server/pokemon"
)

type consoleRegion map[uint8]string

// Parse accepts a raw pokemon (byte slice) and either returns a Pokemon struct
// with all applicable fields populated or an error depicting the reason for
// failure.
func Parse(rawPokemon p.RawPokemon) (*p.Pokemon, error) {
	if err := validateRawPokemon(rawPokemon); err != nil {
		return nil, err
	}
	consoleRegions := consoleRegion{0x00: "J", 0x01: "U", 0x02: "E", 0x03: "?", 0x04: "C", 0x05: "K", 0x06: "T"}

	var pkmn p.Pokemon
	pkmn.RawPokemon = &rawPokemon

	// Block A
	pkmn.EncryptionConstant = binary.LittleEndian.Uint32(rawPokemon[0x00:0x08])
	pkmn.PokedexNumber = toUint16(rawPokemon[0x08:0x0a])
	pkmn.HeldItemID = toUint16(rawPokemon[0x0a:0x0c])
	pkmn.TrainerID = toUint16(rawPokemon[0x0c:0x0e])
	pkmn.SecretID = toUint16(rawPokemon[0x0e:0x10])
	pkmn.FullTrainerID = binary.LittleEndian.Uint32(rawPokemon[0x0c:0x10])
	pkmn.Experience = binary.LittleEndian.Uint32(rawPokemon[0x10:0x15])
	pkmn.AbilityID = rawPokemon[0x14]
	pkmn.AbilityNum = rawPokemon[0x14]
	pkmn.PID = binary.LittleEndian.Uint32(rawPokemon[0x18:0x1c])
	pkmn.NatureID = rawPokemon[0x1c]

	pkmnGenderByte := rawPokemon[0x1d]
	pkmn.FatefulEncounter = (pkmnGenderByte & 0x01) == 1
	if (pkmnGenderByte & 0x02) == 0x02 {
		pkmn.Gender = "F"
	} else if (pkmnGenderByte & 0x04) == 0x04 {
		pkmn.Gender = ""
	} else {
		pkmn.Gender = "M"
	}
	pkmn.FormID = pkmnGenderByte >> 3

	pkmn.HPEV = rawPokemon[0x1e]
	pkmn.AtkEV = rawPokemon[0x1f]
	pkmn.DefEV = rawPokemon[0x20]
	pkmn.SpeEV = rawPokemon[0x21]
	pkmn.SpAtkEV = rawPokemon[0x22]
	pkmn.SpDefEV = rawPokemon[0x23]

	pokerusByte := rawPokemon[0x2b]
	pkmn.PokerusDuration = pokerusByte & 15
	pkmn.PokerusStrain = pokerusByte >> 4

	// Block B
	pkmn.RawNickname = rawPokemon[0x40:0x58]
	pkmn.Nickname = binaryToUTF16leString(pkmn.RawNickname)

	pkmn.Move1ID = toUint16(rawPokemon[0x5a:0x5c])
	pkmn.Move2ID = toUint16(rawPokemon[0x5c:0x5e])
	pkmn.Move3ID = toUint16(rawPokemon[0x5e:0x60])
	pkmn.Move4ID = toUint16(rawPokemon[0x60:0x62])
	pkmn.Move1PP = rawPokemon[0x62]
	pkmn.Move2PP = rawPokemon[0x63]
	pkmn.Move3PP = rawPokemon[0x64]
	pkmn.Move4PP = rawPokemon[0x65]
	pkmn.Move1PPUsed = rawPokemon[0x66]
	pkmn.Move2PPUsed = rawPokemon[0x67]
	pkmn.Move3PPUsed = rawPokemon[0x68]
	pkmn.Move4PPUsed = rawPokemon[0x69]
	pkmn.EggMove1ID = toUint16(rawPokemon[0x6a:0x6c])
	pkmn.EggMove2ID = toUint16(rawPokemon[0x6c:0x6e])
	pkmn.EggMove3ID = toUint16(rawPokemon[0x6e:0x70])
	pkmn.EggMove4ID = toUint16(rawPokemon[0x70:0x72])

	ivBytes := binary.LittleEndian.Uint32(rawPokemon[0x74:0x78])
	pkmn.HPIV = ivBytes & 0x1f
	pkmn.AtkIV = ivBytes >> 5 & 0x1f
	pkmn.DefIV = ivBytes >> 10 & 0x1f
	pkmn.SpeIV = ivBytes >> 15 & 0x1f
	pkmn.SpAtkIV = ivBytes >> 20 & 0x1f
	pkmn.SpDefIV = ivBytes >> 25 & 0x1f
	pkmn.IsEgg = ((ivBytes >> 30) % 2) == 1
	pkmn.IsNicknamed = ((ivBytes >> 31) % 2) == 1

	// Block C
	pkmn.RawNotOTName = rawPokemon[0x78:0x90]
	pkmn.NotOTName = binaryToUTF16leString(pkmn.RawNotOTName)
	if rawPokemon[0x92] == 1 {
		pkmn.NotOTGender = "F"
	} else {
		pkmn.NotOTGender = "M"
	}

	pkmn.CurrentHandlerIsOT = rawPokemon[0x93] == 1
	pkmn.GeoLocation1RegionID = rawPokemon[0x94]
	pkmn.GeoLocation1CountryID = rawPokemon[0x95]
	pkmn.GeoLocation2RegionID = rawPokemon[0x96]
	pkmn.GeoLocation2CountryID = rawPokemon[0x97]
	pkmn.GeoLocation3RegionID = rawPokemon[0x98]
	pkmn.GeoLocation3CountryID = rawPokemon[0x99]
	pkmn.GeoLocation4RegionID = rawPokemon[0x9a]
	pkmn.GeoLocation4CountryID = rawPokemon[0x9b]
	pkmn.GeoLocation5RegionID = rawPokemon[0x9c]
	pkmn.GeoLocation5CountryID = rawPokemon[0x9d]

	pkmn.NotOTFriendship = rawPokemon[0xa2]
	pkmn.NotOTAffection = rawPokemon[0xa3]
	pkmn.NotOTMemoryIntensity = rawPokemon[0xa4]
	pkmn.NotOTMemoryLine = rawPokemon[0xa5]
	pkmn.NotOTMemoryFeeling = rawPokemon[0xa6]
	pkmn.NotOTMemoryTextVar = toUint16(rawPokemon[0xa8:0xaa])

	pkmn.Fullness = rawPokemon[0xae]
	pkmn.Enjoyment = rawPokemon[0xaf]

	// Block D
	pkmn.RawOTName = rawPokemon[0xb0:0xc8]
	pkmn.OTName = binaryToUTF16leString(pkmn.RawOTName)
	pkmn.OTFriendship = rawPokemon[0xca]
	pkmn.OTAffection = rawPokemon[0xcb]
	pkmn.OTMemoryIntensity = rawPokemon[0xcc]
	pkmn.OTMemoryLine = rawPokemon[0xcd]
	pkmn.OTMemoryTextVar = toUint16(rawPokemon[0xce:0xd0])
	pkmn.OTMemoryFeeling = rawPokemon[0xd0]

	pkmn.DateEggReceived = uint16ToDatetime(binary.LittleEndian.Uint32(rawPokemon[0xd0:0xd4]))
	pkmn.DateMet = uint16ToDatetime(binary.LittleEndian.Uint32(rawPokemon[0xd4:0xd8]))
	pkmn.EggLocationID = toUint16(rawPokemon[0xd8:0xda])
	pkmn.MetLocationID = toUint16(rawPokemon[0xda:0xdc])
	pkmn.PokeballID = rawPokemon[0xdc]
	pkmn.LevelMet = rawPokemon[0xdd] & 0x7f

	if otGenderBit := rawPokemon[0xdd] >> 7; otGenderBit == 1 {
		pkmn.OTGender = "F"
	} else {
		pkmn.OTGender = "M"
	}

	pkmn.Gen4EncounterTypeID = rawPokemon[0xde]
	pkmn.OTGameID = rawPokemon[0xdf]
	pkmn.CountryID = rawPokemon[0xe0]
	pkmn.RegionID = rawPokemon[0xe1]
	pkmn.RawConsoleRegion = rawPokemon[0xe2]
	pkmn.ConsoleRegion = consoleRegions[pkmn.RawConsoleRegion]
	pkmn.OTLanguageID = rawPokemon[0xe3]

	return &pkmn, nil
}

func binaryToUTF16leString(b []byte) string {
	r := bytes.NewReader(b)
	u16 := make([]uint16, len(b)/2)
	binary.Read(r, binary.LittleEndian, &u16)
	runes := utf16.Decode(u16)
	s := strings.Split(string(runes), "\u0000")[0]

	return s
}

func calculateChecksum(b []byte) uint16 {
	var sum uint16
	for i := 8; i < 232; i += 2 {
		sum += toUint16(b[i : i+2])
	}
	return sum & 0xffff
}

func toUint16(b []byte) uint16 {
	return binary.LittleEndian.Uint16(b)
}

func validateRawPokemon(rawPokemon p.RawPokemon) error {
	if l := len(rawPokemon); l != 232 && l != 260 {
		return fmt.Errorf("Invalid length for generation 7 pokemon: %d bytes", l)
	}

	checksumValue := toUint16(rawPokemon[0x06:0x08])
	if calculatedChecksum := calculateChecksum(rawPokemon); calculatedChecksum != checksumValue {
		return errors.New("Invalid checksum for generation 7 pokemon")
	}

	if toUint16(rawPokemon[0x04:0x06]) == 1 || rawPokemon[0x58] == 1 || rawPokemon[0x90] == 1 || rawPokemon[0xc8] == 1 {
		return errors.New("Invalid bytes supplied for generation 7 pokemon")
	}

	return nil
}

func uint16ToDatetime(i uint32) int64 {
	if i == 0 {
		return 0
	}

	year := int((i & 0xff) + 2000)
	month := time.Month(int((i >> 8) & 0xff))
	day := int((i >> 16) & 0xff)
	date := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)

	// *1000 to return milliseconds
	return date.Unix() * 1000
}
