import {toID, BasicEffect} from './dex-data';

interface SpeciesAbility {
	0: string;
	1?: string;
	H?: string;
	S?: string;
}

type SpeciesTag = "Mythical" | "Restricted Legendary" | "Sub-Legendary";

export interface SpeciesData extends Partial<Species> {
	name: string;
	/** National Dex number */
	num: number;

	types: string[];
	abilities: SpeciesAbility;
	baseStats: StatsTable;
	eggGroups: string[];
	weightkg: number;
}

export type ModdedSpeciesData = SpeciesData | Partial<Omit<SpeciesData, 'name'>> & {inherit: true};

export interface SpeciesFormatsData {
	comboMoves?: readonly string[];
	doublesTier?: TierTypes.Doubles | TierTypes.Other;
	essentialMove?: string;
	exclusiveMoves?: readonly string[];
	gmaxUnreleased?: boolean;
	isNonstandard?: Nonstandard | null;
	randomBattleMoves?: readonly string[];
	randomBattleLevel?: number;
	randomDoubleBattleMoves?: readonly string[];
	randomDoubleBattleLevel?: number;
	randomBattleNoDynamaxMoves?: readonly string[];
	tier?: TierTypes.Singles | TierTypes.Other;
}

export type ModdedSpeciesFormatsData = SpeciesFormatsData & {inherit?: true};

export interface LearnsetData {
	learnset?: {[moveid: string]: MoveSource[]};
	eventData?: EventInfo[];
	eventOnly?: boolean;
	encounters?: EventInfo[];
	exists?: boolean;
}

export type ModdedLearnsetData = LearnsetData & {inherit?: true};

export class Species extends BasicEffect implements Readonly<BasicEffect & SpeciesFormatsData> {
	declare readonly effectType: 'Pokemon';
	/**
	 * Species ID. Identical to ID. Note that this is the full ID, e.g.
	 * 'basculinbluestriped'. To get the base species ID, you need to
	 * manually read toID(species.baseSpecies).
	 */
	declare readonly id: ID;
	/**
	 * Name. Note that this is the full name with forme,
	 * e.g. 'Basculin-Blue-Striped'. To get the name without forme, see
	 * `species.baseSpecies`.
	 */
	declare readonly name: string;
	/**
	 * Base species. Species, but without the forme name.
	 *
	 * DO NOT ASSUME A POKEMON CAN TRANSFORM FROM `baseSpecies` TO
	 * `species`. USE `changesFrom` FOR THAT.
	 */
	readonly baseSpecies: string;
	/**
	 * Forme name. If the forme exists,
	 * `species.name === species.baseSpecies + '-' + species.forme`
	 *
	 * The games make a distinction between Forme (foorumu) (legendary Pokémon)
	 * and Form (sugata) (non-legendary Pokémon). PS does not use the same
	 * distinction – they're all "Forme" to PS, reflecting current community
	 * use of the term.
	 *
	 * This property only tracks non-cosmetic formes, and will be `''` for
	 * cosmetic formes.
	 */
	readonly forme: string;
	/**
	 * Base forme name (e.g. 'Altered' for Giratina).
	 */
	readonly baseForme: string;
	/**
	 * Other forms. List of names of cosmetic forms. These should have
	 * `aliases.js` aliases to this entry, but not have their own
	 * entry in `pokedex.js`.
	 */
	readonly cosmeticFormes?: string[];
	/**
	 * Other formes. List of names of formes, appears only on the base
	 * forme. Unlike forms, these have their own entry in `pokedex.js`.
	 */
	readonly otherFormes?: string[];
	/**
	 * List of forme speciesNames in the order they appear in the game data -
	 * the union of baseSpecies, otherFormes and cosmeticFormes. Appears only on
	 * the base species forme.
	 *
	 * A species's alternate formeindex may change from generation to generation -
	 * the forme with index N in Gen A is not guaranteed to be the same forme as the
	 * forme with index in Gen B.
	 *
	 * Gigantamaxes are not considered formes by the game (see data/FORMES.md - PS
	 * labels them as such for convenience) - Gigantamax "formes" are instead included at
	 * the end of the formeOrder list so as not to interfere with the correct index numbers.
	 */
	readonly formeOrder?: string[];
	/**
	 * Sprite ID. Basically the same as ID, but with a dash between
	 * species and forme.
	 */
	readonly spriteid: string;
	/** Abilities. */
	readonly abilities: SpeciesAbility;
	/** Types. */
	readonly types: string[];
	/** Added type (added by Trick-Or-Treat or Forest's Curse, but only listed in species by OMs). */
	readonly addedType?: string;
	/** Pre-evolution. '' if nothing evolves into this Pokemon. */
	readonly prevo: string;
	/** Evolutions. Array because many Pokemon have multiple evolutions. */
	readonly evos: string[];
	readonly evoType?: 'trade' | 'useItem' | 'levelMove' | 'levelExtra' | 'levelFriendship' | 'levelHold' | 'other';
	/** Evolution condition. falsy if doesn't evolve. */
	declare readonly evoCondition?: string;
	/** Evolution item. falsy if doesn't evolve. */
	declare readonly evoItem?: string;
	/** Evolution move. falsy if doesn't evolve. */
	readonly evoMove?: string;
	/** Evolution level. falsy if doesn't evolve. */
	readonly evoLevel?: number;
	/** Is NFE? True if this Pokemon can evolve (Mega evolution doesn't count). */
	readonly nfe: boolean;
	/** Egg groups. */
	readonly eggGroups: string[];
	/** True if this species can hatch from an Egg. */
	readonly canHatch: boolean;
	/**
	 * Gender. M = always male, F = always female, N = always
	 * genderless, '' = sometimes male sometimes female.
	 */
	readonly gender: GenderName;
	/** Gender ratio. Should add up to 1 unless genderless. */
	readonly genderRatio: {M: number, F: number};
	/** Base stats. */
	readonly baseStats: StatsTable;
	/** Max HP. Overrides usual HP calculations (for Shedinja). */
	readonly maxHP?: number;
	/** A Pokemon's Base Stat Total */
	readonly bst: number;
	/** Weight (in kg). Not valid for OMs; use weighthg / 10 instead. */
	readonly weightkg: number;
	/** Weight (in integer multiples of 0.1kg). */
	readonly weighthg: number;
	/** Height (in m). */
	readonly heightm: number;
	/** Color. */
	readonly color: string;
	/**
	 * Tags, boolean data. Currently just legendary/mythical status.
	 */
	readonly tags: SpeciesTag[];
	/** Does this Pokemon have an unreleased hidden ability? */
	readonly unreleasedHidden: boolean | 'Past';
	/**
	 * Is it only possible to get the hidden ability on a male pokemon?
	 * This is mainly relevant to Gen 5.
	 */
	readonly maleOnlyHidden: boolean;
	/** True if a pokemon is mega. */
	readonly isMega?: boolean;
	/** True if a pokemon is primal. */
	declare readonly isPrimal?: boolean;
	/** Name of its Gigantamax move, if a pokemon is capable of gigantamaxing. */
	readonly canGigantamax?: string;
	/** If this Pokemon can gigantamax, is its gigantamax released? */
	readonly gmaxUnreleased?: boolean;
	/** True if a Pokemon species is incapable of dynamaxing */
	readonly cannotDynamax?: boolean;
	/** What it transforms from, if a pokemon is a forme that is only accessible in battle. */
	readonly battleOnly?: string | string[];
	/** Required item. Do not use this directly; see requiredItems. */
	readonly requiredItem?: string;
	/** Required move. Move required to use this forme in-battle. */
	declare readonly requiredMove?: string;
	/** Required ability. Ability required to use this forme in-battle. */
	declare readonly requiredAbility?: string;
	/**
	 * Required items. Items required to be in this forme, e.g. a mega
	 * stone, or Griseous Orb. Array because Arceus formes can hold
	 * either a Plate or a Z-Crystal.
	 */
	readonly requiredItems?: string[];

	/**
	 * Formes that can transform into this Pokemon, to inherit learnsets
	 * from. (Like `prevo`, but for transformations that aren't
	 * technically evolution. Includes in-battle transformations like
	 * Zen Mode and out-of-battle transformations like Rotom.)
	 *
	 * Not filled out for megas/primals - fall back to baseSpecies
	 * for in-battle formes.
	 */
	readonly changesFrom?: string;

	/**
	 * Singles Tier. The Pokemon's location in the Smogon tier system.
	 */
	readonly tier: TierTypes.Singles | TierTypes.Other;
	/**
	 * Doubles Tier. The Pokemon's location in the Smogon doubles tier system.
	 */
	readonly doublesTier: TierTypes.Doubles | TierTypes.Other;
	declare readonly randomBattleMoves?: readonly ID[];
	declare readonly randomBattleLevel?: number;
	declare readonly randomDoubleBattleMoves?: readonly ID[];
	declare readonly randomDoubleBattleLevel?: number;
	declare readonly randomBattleNoDynamaxMoves?: readonly ID[];
	declare readonly exclusiveMoves?: readonly ID[];
	declare readonly comboMoves?: readonly ID[];
	declare readonly essentialMove?: ID;

	constructor(data: AnyObject) {
		super(data);
		// eslint-disable-next-line @typescript-eslint/no-this-alias
		data = this;

		this.fullname = `pokemon: ${data.name}`;
		this.effectType = 'Pokemon';
		this.baseSpecies = data.baseSpecies || this.name;
		this.forme = data.forme || '';
		this.baseForme = data.baseForme || '';
		this.cosmeticFormes = data.cosmeticFormes || undefined;
		this.otherFormes = data.otherFormes || undefined;
		this.formeOrder = data.formeOrder || undefined;
		this.spriteid = data.spriteid ||
			(toID(this.baseSpecies) + (this.baseSpecies !== this.name ? `-${toID(this.forme)}` : ''));
		this.abilities = data.abilities || {0: ""};
		this.types = data.types || ['???'];
		this.addedType = data.addedType || undefined;
		this.prevo = data.prevo || '';
		this.tier = data.tier || '';
		this.doublesTier = data.doublesTier || '';
		this.evos = data.evos || [];
		this.evoType = data.evoType || undefined;
		this.evoMove = data.evoMove || undefined;
		this.evoLevel = data.evoLevel || undefined;
		this.nfe = data.nfe || false;
		this.eggGroups = data.eggGroups || [];
		this.canHatch = data.canHatch || false;
		this.gender = data.gender || '';
		this.genderRatio = data.genderRatio || (this.gender === 'M' ? {M: 1, F: 0} :
			this.gender === 'F' ? {M: 0, F: 1} :
			this.gender === 'N' ? {M: 0, F: 0} :
			{M: 0.5, F: 0.5});
		this.requiredItem = data.requiredItem || undefined;
		this.requiredItems = this.requiredItems || (this.requiredItem ? [this.requiredItem] : undefined);
		this.baseStats = data.baseStats || {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0};
		this.bst = this.baseStats.hp + this.baseStats.atk + this.baseStats.def +
			this.baseStats.spa + this.baseStats.spd + this.baseStats.spe;
		this.weightkg = data.weightkg || 0;
		this.weighthg = this.weightkg * 10;
		this.heightm = data.heightm || 0;
		this.color = data.color || '';
		this.tags = data.tags || [];
		this.unreleasedHidden = data.unreleasedHidden || false;
		this.maleOnlyHidden = !!data.maleOnlyHidden;
		this.maxHP = data.maxHP || undefined;
		this.isMega = !!(this.forme && ['Mega', 'Mega-X', 'Mega-Y'].includes(this.forme)) || undefined;
		this.canGigantamax = data.canGigantamax || undefined;
		this.gmaxUnreleased = !!data.gmaxUnreleased;
		this.cannotDynamax = !!data.cannotDynamax;
		this.battleOnly = data.battleOnly || (this.isMega ? this.baseSpecies : undefined);
		this.changesFrom = data.changesFrom ||
			(this.battleOnly !== this.baseSpecies ? this.battleOnly : this.baseSpecies);
		if (Array.isArray(data.changesFrom)) this.changesFrom = data.changesFrom[0];

		if (!this.gen && this.num >= 1) {
			if (this.num >= 810 || ['Gmax', 'Galar', 'Galar-Zen', 'Hisui'].includes(this.forme)) {
				this.gen = 8;
			} else if (this.num >= 722 || this.forme.startsWith('Alola') || this.forme === 'Starter') {
				this.gen = 7;
			} else if (this.forme === 'Primal') {
				this.gen = 6;
				this.isPrimal = true;
				this.battleOnly = this.baseSpecies;
			} else if (this.num >= 650 || this.isMega) {
				this.gen = 6;
			} else if (this.num >= 494) {
				this.gen = 5;
			} else if (this.num >= 387) {
				this.gen = 4;
			} else if (this.num >= 252) {
				this.gen = 3;
			} else if (this.num >= 152) {
				this.gen = 2;
			} else {
				this.gen = 1;
			}
		}
	}
}

export class Learnset {
	readonly effectType: 'Learnset';
	/**
	 * Keeps track of exactly how a pokemon might learn a move, in the
	 * form moveid:sources[].
	 */
	readonly learnset?: {[moveid: string]: MoveSource[]};
	/** True if the only way to get this Pokemon is from events. */
	readonly eventOnly: boolean;
	/** List of event data for each event. */
	readonly eventData?: EventInfo[];
	readonly encounters?: EventInfo[];
	readonly exists: boolean;

	constructor(data: AnyObject) {
		this.exists = true;
		this.effectType = 'Learnset';
		this.learnset = data.learnset || undefined;
		this.eventOnly = !!data.eventOnly;
		this.eventData = data.eventData || undefined;
		this.encounters = data.encounters || undefined;
	}
}

export class DexSpecies {
	readonly dex: ModdedDex;
	readonly speciesCache = new Map<ID, Species>();
	readonly learnsetCache = new Map<ID, Learnset>();
	allCache: readonly Species[] | null = null;

	constructor(dex: ModdedDex) {
		this.dex = dex;
	}

	get(name?: string | Species): Species {
		if (name && typeof name !== 'string') return name;

		name = (name || '').trim();
		let id = toID(name);
		if (id === 'nidoran' && name.endsWith('♀')) {
			id = 'nidoranf' as ID;
		} else if (id === 'nidoran' && name.endsWith('♂')) {
			id = 'nidoranm' as ID;
		}

		return this.getByID(id);
	}
	getByID(id: ID): Species {
		let species: Mutable<Species> | undefined = this.speciesCache.get(id);
		if (species) return species;

		if (this.dex.data.Aliases.hasOwnProperty(id)) {
			if (this.dex.data.FormatsData.hasOwnProperty(id)) {
				// special event ID, like Rockruff-Dusk
				const baseId = toID(this.dex.data.Aliases[id]);
				species = new Species({
					...this.dex.data.Pokedex[baseId],
					...this.dex.data.FormatsData[id],
					name: id,
				});
				species.abilities = {0: species.abilities['S']!};
			} else {
				species = this.get(this.dex.data.Aliases[id]);
				if (species.cosmeticFormes) {
					for (const forme of species.cosmeticFormes) {
						if (toID(forme) === id) {
							species = new Species({
								...species,
								name: forme,
								forme: forme.slice(species.name.length + 1),
								baseForme: "",
								baseSpecies: species.name,
								otherFormes: null,
								cosmeticFormes: null,
							});
							break;
						}
					}
				}
			}
			this.speciesCache.set(id, species);
			return species;
		}

		if (!this.dex.data.Pokedex.hasOwnProperty(id)) {
			let aliasTo = '';
			const formeNames: {[k: string]: string[]} = {
				alola: ['a', 'alola', 'alolan'],
				galar: ['g', 'galar', 'galarian'],
				mega: ['m', 'mega'],
				primal: ['p', 'primal'],
			};
			for (const forme in formeNames) {
				let pokeName = '';
				for (const i of formeNames[forme]) {
					if (id.startsWith(i)) {
						pokeName = id.slice(i.length);
					} else if (id.endsWith(i)) {
						pokeName = id.slice(0, -i.length);
					}
				}
				if (this.dex.data.Aliases.hasOwnProperty(pokeName)) pokeName = toID(this.dex.data.Aliases[pokeName]);
				if (this.dex.data.Pokedex[pokeName + forme]) {
					aliasTo = pokeName + forme;
					break;
				}
			}
			if (aliasTo) {
				species = this.get(aliasTo);
				if (species.exists) {
					this.speciesCache.set(id, species);
					return species;
				}
			}
		}
		if (id && this.dex.data.Pokedex.hasOwnProperty(id)) {
			const pokedexData = this.dex.data.Pokedex[id];
			const baseSpeciesTags = pokedexData.baseSpecies && this.dex.data.Pokedex[toID(pokedexData.baseSpecies)].tags;
			species = new Species({
				tags: baseSpeciesTags,
				...pokedexData,
				...this.dex.data.FormatsData[id],
			});
			// Inherit any statuses from the base species (Arceus, Silvally).
			const baseSpeciesStatuses = this.dex.data.Conditions[toID(species.baseSpecies)];
			if (baseSpeciesStatuses !== undefined) {
				let key: keyof EffectData;
				for (key in baseSpeciesStatuses) {
					if (!(key in species)) (species as any)[key] = baseSpeciesStatuses[key];
				}
			}
			if (!species.tier && !species.doublesTier && species.baseSpecies !== species.name) {
				if (species.baseSpecies === 'Mimikyu') {
					species.tier = this.dex.data.FormatsData[toID(species.baseSpecies)].tier || 'Illegal';
					species.doublesTier = this.dex.data.FormatsData[toID(species.baseSpecies)].doublesTier || 'Illegal';
				} else if (species.id.endsWith('totem')) {
					species.tier = this.dex.data.FormatsData[species.id.slice(0, -5)].tier || 'Illegal';
					species.doublesTier = this.dex.data.FormatsData[species.id.slice(0, -5)].doublesTier || 'Illegal';
				} else if (species.battleOnly) {
					species.tier = this.dex.data.FormatsData[toID(species.battleOnly)].tier || 'Illegal';
					species.doublesTier = this.dex.data.FormatsData[toID(species.battleOnly)].doublesTier || 'Illegal';
				} else {
					const baseFormatsData = this.dex.data.FormatsData[toID(species.baseSpecies)];
					if (!baseFormatsData) {
						throw new Error(`${species.baseSpecies} has no formats-data entry`);
					}
					species.tier = baseFormatsData.tier || 'Illegal';
					species.doublesTier = baseFormatsData.doublesTier || 'Illegal';
				}
			}
			if (!species.tier) species.tier = 'Illegal';
			if (!species.doublesTier) species.doublesTier = species.tier as any;
			if (species.gen > this.dex.gen) {
				species.tier = 'Illegal';
				species.doublesTier = 'Illegal';
				species.isNonstandard = 'Future';
			}
			if (this.dex.currentMod === 'gen7letsgo' && !species.isNonstandard) {
				const isLetsGo = (
					(species.num <= 151 || ['Meltan', 'Melmetal'].includes(species.name)) &&
					(!species.forme || (['Alola', 'Mega', 'Mega-X', 'Mega-Y', 'Starter'].includes(species.forme) &&
					species.name !== 'Pikachu-Alola'))
				);
				if (!isLetsGo) species.isNonstandard = 'Past';
			}
			if (this.dex.currentMod === 'gen8bdsp' &&
				(!species.isNonstandard || ["Gigantamax", "CAP"].includes(species.isNonstandard))) {
				if (species.gen > 4 || (species.num < 1 && species.isNonstandard !== 'CAP') ||
					species.id === 'pichuspikyeared') {
					species.isNonstandard = 'Future';
					species.tier = species.doublesTier = 'Illegal';
				}
			}
			species.nfe = species.evos.some(evo => {
				const evoSpecies = this.get(evo);
				return !evoSpecies.isNonstandard || evoSpecies.isNonstandard === species?.isNonstandard;
			});
			species.canHatch = species.canHatch ||
				(!['Ditto', 'Undiscovered'].includes(species.eggGroups[0]) && !species.prevo && species.name !== 'Manaphy');
			if (this.dex.gen === 1) species.bst -= species.baseStats.spd;
			if (this.dex.gen < 5) delete species.abilities['H'];
			if (this.dex.gen === 3 && this.dex.abilities.get(species.abilities['1']).gen === 4) delete species.abilities['1'];
		} else {
			species = new Species({
				id, name: id,
				exists: false, tier: 'Illegal', doublesTier: 'Illegal', isNonstandard: 'Custom',
			});
		}
		if (species.exists) this.speciesCache.set(id, species);
		return species;
	}

	getLearnset(id: ID): Learnset['learnset'] {
		return this.getLearnsetData(id).learnset;
	}

	getLearnsetData(id: ID): Learnset {
		let learnsetData = this.learnsetCache.get(id);
		if (learnsetData) return learnsetData;
		if (!this.dex.data.Learnsets.hasOwnProperty(id)) {
			return new Learnset({exists: false});
		}
		learnsetData = new Learnset(this.dex.data.Learnsets[id]);
		this.learnsetCache.set(id, learnsetData);
		return learnsetData;
	}

	all(): readonly Species[] {
		if (this.allCache) return this.allCache;
		const species = [];
		for (const id in this.dex.data.Pokedex) {
			species.push(this.getByID(id as ID));
		}
		this.allCache = species;
		return this.allCache;
	}
}
