// To run this script drop the https://github.com/Leanny/splat3 repo into scripts/dicts/splat3

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";
import type {
	BaseWeaponStats,
	MainWeaponParams,
	ParamsJson,
	SubWeaponParams,
	WeaponKit,
} from "~/features/build-analyzer/analyzer-types";
import type {
	MainWeaponId,
	SpecialWeaponId,
	SubWeaponId,
} from "~/modules/in-game-lists/types";
import {
	SQUID_BEAKON_ID,
	subWeaponIds,
	weaponIdToBaseWeaponId,
} from "~/modules/in-game-lists/weapon-ids";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import {
	LANG_JSONS_TO_CREATE,
	loadLangDicts,
	loadSplPlayerParams,
	loadWeaponInfoMain,
	loadWeaponInfoSpecial,
	loadWeaponInfoSub,
	translationJsonFolderName,
	weaponParamsDir,
} from "./utils";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const playersParams = loadSplPlayerParams();
const weapons = loadWeaponInfoMain();
const subWeapons = loadWeaponInfoSub();
const specialWeapons = loadWeaponInfoSpecial();

const CURRENT_SEASON = 9;

type MainWeapon = (typeof weapons)[number];
type SubWeapon = (typeof subWeapons)[number];
type SpecialWeapon = (typeof specialWeapons)[number];
type TranslationArray = Array<{ language: string; key: string; value: string }>;

const KIT_PROPERTIES = [
	"SpecialPoint",
	"subWeaponId",
	"specialWeaponId",
] as const;

async function main() {
	const allMainWeaponParams: Record<number, MainWeaponParams> = {};
	const subWeaponsResult: Record<number, SubWeaponParams> = {};
	const specialWeaponsResult: any = {};
	const translations: TranslationArray = [];

	const langDicts = await loadLangDicts();
	const hasLangDicts = langDicts.length > 0;

	for (const weapon of weapons) {
		if (mainWeaponShouldBeSkipped(weapon)) continue;

		const rawParams = loadWeaponParamsObject(weapon);
		const params = combineSwingsIfSame(
			parametersToMainWeaponResult(weapon, rawParams),
		);

		if (hasLangDicts) {
			translationsToArray({
				arr: translations,
				internalName: weapon.__RowId,
				weaponId: weapon.Id,
				type: "Main",
				translations: langDicts,
			});
		}

		allMainWeaponParams[weapon.Id] = params;
	}

	const { baseWeaponStats, weaponKits } =
		splitIntoBaseStatsAndKits(allMainWeaponParams);

	for (const subWeapon of subWeapons) {
		if (subWeaponShouldBeSkipped(subWeapon)) continue;

		const rawParams = loadWeaponParamsObject(subWeapon);
		const params = parametersToSubWeaponResult(subWeapon, rawParams);

		if (hasLangDicts) {
			translationsToArray({
				arr: translations,
				internalName: subWeapon.__RowId,
				weaponId: subWeapon.Id,
				type: "Sub",
				translations: langDicts,
			});
		}

		subWeaponsResult[subWeapon.Id] = params;
	}

	for (const specialWeapon of specialWeapons) {
		if (specialWeaponShouldBeSkipped(specialWeapon)) continue;

		const rawParams = loadWeaponParamsObject(specialWeapon);
		const params: Record<string, any> =
			parametersToSpecialWeaponResult(rawParams);

		// Super Chumps has two distinct splash damage values (near/far)
		// that should be labeled separately in the analyzer
		const SUPER_CHUMP_SPECIAL_ID = 16;
		if (
			specialWeapon.Id === SUPER_CHUMP_SPECIAL_ID &&
			params.DistanceDamage?.length === 2
		) {
			const sorted = [...params.DistanceDamage].sort(
				(a: any, b: any) => b.Damage - a.Damage,
			);
			params.SplashDamageMax = [sorted[0]];
			params.SplashDamageMin = [sorted[1]];
			params.DistanceDamage = undefined;
		}

		Object.assign(params, specialWeaponRangeParams(specialWeapon, rawParams));

		if (hasLangDicts) {
			translationsToArray({
				arr: translations,
				internalName: specialWeapon.__RowId,
				weaponId: specialWeapon.Id,
				type: "Special",
				translations: langDicts,
			});
		}

		specialWeaponsResult[specialWeapon.Id] = params;
	}

	const toFile: ParamsJson = {
		baseWeaponStats,
		weaponKits,
		subWeapons: subWeaponsResult,
		specialWeapons: specialWeaponsResult,
	};

	const weaponParamsTs = `export const weaponParams = ${JSON.stringify(toFile, null, "\t")} as const;\n`;

	fs.writeFileSync(
		path.join(
			__dirname,
			"..",
			"app",
			"features",
			"build-analyzer",
			"data",
			"weapon-params.ts",
		),
		weaponParamsTs,
	);

	if (hasLangDicts) {
		writeTranslationsJsons(translations);
	}
	logWeaponIds(weaponKits);
}

function splitIntoBaseStatsAndKits(
	allParams: Record<number, MainWeaponParams>,
): {
	baseWeaponStats: Record<number, BaseWeaponStats>;
	weaponKits: Record<number, WeaponKit>;
} {
	const weaponGroups: Record<
		number,
		Array<{ id: number; params: MainWeaponParams }>
	> = {};

	for (const [idStr, params] of Object.entries(allParams)) {
		const id = Number(idStr) as MainWeaponId;
		const baseId = weaponIdToBaseWeaponId(id);
		if (!weaponGroups[baseId]) weaponGroups[baseId] = [];
		weaponGroups[baseId].push({ id, params });
	}

	const baseWeaponStats: Record<number, BaseWeaponStats> = {};
	const weaponKits: Record<number, WeaponKit> = {};

	for (const [baseIdStr, variants] of Object.entries(weaponGroups)) {
		const baseId = Number(baseIdStr);
		const firstVariant = variants[0].params;

		const nonKitProps = Object.keys(firstVariant).filter(
			(k) => !KIT_PROPERTIES.includes(k as (typeof KIT_PROPERTIES)[number]),
		) as Array<keyof MainWeaponParams>;

		const sharedProps: Partial<MainWeaponParams> = {};
		for (const prop of nonKitProps) {
			const firstVal = JSON.stringify(firstVariant[prop]);
			const allSame = variants.every(
				(v) => JSON.stringify(v.params[prop]) === firstVal,
			);
			if (allSame && firstVariant[prop] !== undefined) {
				(sharedProps as any)[prop] = firstVariant[prop];
			}
		}

		baseWeaponStats[baseId] = sharedProps as BaseWeaponStats;

		for (const variant of variants) {
			const kit: WeaponKit = {
				SpecialPoint: variant.params.SpecialPoint,
				subWeaponId: variant.params.subWeaponId,
				specialWeaponId: variant.params.specialWeaponId,
			};

			for (const prop of nonKitProps) {
				if (!(prop in sharedProps) && variant.params[prop] !== undefined) {
					(kit as any)[prop] = variant.params[prop];
				}
			}

			weaponKits[variant.id] = kit;
		}
	}

	return { baseWeaponStats, weaponKits };
}

function getWeaponCategoryName(weaponId: number): string | undefined {
	for (const category of [
		{ name: "SHOOTERS", range: [0, 199] },
		{ name: "BLASTERS", range: [200, 299] },
		{ name: "SHOOTERS", range: [300, 499] },
		{ name: "ROLLERS", range: [1000, 1099] },
		{ name: "BRUSHES", range: [1100, 1199] },
		{ name: "CHARGERS", range: [2000, 2099] },
		{ name: "SLOSHERS", range: [3000, 3099] },
		{ name: "SPLATLINGS", range: [4000, 4099] },
		{ name: "DUALIES", range: [5000, 5099] },
		{ name: "BRELLAS", range: [6000, 6099] },
		{ name: "STRINGERS", range: [7000, 7099] },
		{ name: "SPLATANAS", range: [8000, 8099] },
	]) {
		if (weaponId >= category.range[0] && weaponId <= category.range[1]) {
			return category.name;
		}
	}
	return undefined;
}

function extractRangeParams(weapon: MainWeapon, params: any) {
	const category = getWeaponCategoryName(weapon.Id);
	const isBrella = category === "BRELLAS";
	const isSlosher = category === "SLOSHERS";

	if (!category) {
		return {};
	}

	const isCharger = params.MoveParam?.$type === "spl__BulletChargerMoveParam";

	if (isCharger) {
		return {
			DistanceFullCharge: params.MoveParam?.DistanceFullCharge,
			DistanceMaxCharge: params.MoveParam?.DistanceMaxCharge,
			DistanceMinCharge: params.MoveParam?.DistanceMinCharge,
		};
	}

	if (isSlosher) {
		const units = params.UnitGroupParam?.Unit;
		const unitForRange = units?.reduce(
			(best: any, unit: any) =>
				(unit?.SpawnSpeedGround ?? 0) > (best?.SpawnSpeedGround ?? 0)
					? unit
					: best,
			units?.[0],
		);
		const moveParam = unitForRange?.MoveParam;
		const isBloblobber = weapon.Id === 3030 || weapon.Id === 3031;
		return {
			Range_SpawnSpeed: unitForRange?.SpawnSpeedGround,
			Range_GoStraightStateEndMaxSpeed: moveParam?.GoStraightStateEndMaxSpeed,
			Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame,
			Range_FreeGravity: moveParam?.FreeGravity,
			Range_BrakeAirResist: moveParam?.BrakeAirResist,
			Range_BrakeGravity: moveParam?.BrakeGravity,
			Range_BrakeToFreeStateFrame: moveParam?.BrakeToFreeStateFrame,
			Range_ZRate: params.spl__SpawnBulletAdditionMovePlayerParam?.ZRate,
			Range_BounceAfterMaxSpeed: isBloblobber ? 0.6 : undefined,
			Range_BurstFrame: isBloblobber ? 3 : undefined,
		};
	}

	const isRoller = category === "ROLLERS";
	if (isRoller) {
		const unit = params.VerticalSwingUnitGroupParam?.Unit?.[0];
		const moveParam = unit?.UnitParam?.MoveParam;
		return {
			Range_SpawnSpeed: unit?.SpawnSpeedBase,
			Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame,
			Range_FreeGravity: moveParam?.FreeGravity,
			Range_FreeAirResist: moveParam?.FreeAirResist,
			Range_ZRate: params.spl__SpawnBulletAdditionMovePlayerParam?.ZRate,
		};
	}

	const isBrush = category === "BRUSHES";
	if (isBrush) {
		const unit = params.SwingUnitGroupParam?.Unit?.[0];
		const moveParam = unit?.UnitParam?.MoveParam;
		return {
			Range_SpawnSpeed: unit?.SpawnSpeedBase,
			Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame,
			Range_ZRate: params.spl__SpawnBulletAdditionMovePlayerParam?.ZRate,
		};
	}

	const isSplatana = category === "SPLATANAS";
	if (isSplatana) {
		const bulletParam = params.BulletSaberVerticalParam;
		const moveParam = bulletParam?.MoveParam;
		return {
			Range_SpawnSpeed: moveParam?.SpawnSpeed,
			Range_GoStraightStateEndMaxSpeed: moveParam?.GoStraightStateEndMaxSpeed,
			Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame,
			Range_FreeGravity: moveParam?.FreeGravity,
			Range_BrakeAirResist: moveParam?.BrakeAirResist,
			Range_BrakeGravity: moveParam?.BrakeGravity,
			Range_BrakeToFreeStateFrame: moveParam?.BrakeToFreeStateFrame,
			Range_BurstFrame: bulletParam?.BurstParam?.BurstFrame,
		};
	}

	const isStringer = category === "STRINGERS";
	if (isStringer) {
		const bulletParam = params.spl__BulletStringerParam;
		const moveParam = bulletParam?.MoveParam;
		const isReefLux =
			weapon.Id === 7020 || weapon.Id === 7021 || weapon.Id === 7022;
		const blastRadius = isReefLux
			? undefined
			: bulletParam?.DetonationParam?.BlastParam?.DistanceDamage?.[0]?.Distance;
		return {
			Range_SpawnSpeed: moveParam?.SpawnSpeedMax,
			Range_GoStraightStateEndMaxSpeed: moveParam?.GoStraightStateEndMaxSpeed,
			Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame,
			Range_FreeGravity: moveParam?.FreeGravity,
			Range_FreeAirResist: moveParam?.FreeAirResist,
			Range_BrakeAirResist: moveParam?.BrakeAirResist,
			Range_BrakeGravity: moveParam?.BrakeGravity,
			Range_BrakeToFreeStateFrame: moveParam?.BrakeToFreeStateFrame,
			BlastRadius: blastRadius,
		};
	}

	const isSplatling = category === "SPLATLINGS";
	if (isSplatling) {
		const isBallpoint = weapon.Id === 4030 || weapon.Id === 4031;
		const moveParam = isBallpoint ? params.VariableMoveParam : params.MoveParam;
		return {
			Range_SpawnSpeed: isBallpoint
				? moveParam?.SpawnSpeed
				: moveParam?.SpawnSpeedFirstLastAndSecond,
			Range_GoStraightStateEndMaxSpeed: moveParam?.GoStraightStateEndMaxSpeed,
			Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame,
		};
	}

	const isBlaster = params.BlastParam?.DistanceDamage !== undefined;
	const blastRadius = isBlaster
		? params.BlastParam?.DistanceDamage?.[1]?.Distance
		: undefined;

	const moveParam = isBrella
		? params.spl__BulletShelterShotgunParam?.GroupParams?.[0]?.MoveParam
		: params.MoveParam;

	return {
		Range_SpawnSpeed: moveParam?.SpawnSpeed,
		Range_GoStraightStateEndMaxSpeed: moveParam?.GoStraightStateEndMaxSpeed,
		Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame,
		Range_FreeGravity: moveParam?.FreeGravity,
		Range_ZRate: params.spl__SpawnBulletAdditionMovePlayerParam?.ZRate,
		BlastRadius: blastRadius,
	};
}

function parametersToMainWeaponResult(
	weapon: MainWeapon,
	params: any,
): MainWeaponParams {
	const isSplatling = params.WeaponParam?.$type === "spl__WeaponSpinnerParam";
	const isSlosher = params.WeaponParam?.$type === "spl__WeaponSlosherParam";

	const InkConsume =
		!isSplatling && !isSlosher ? params.WeaponParam?.InkConsume : undefined;
	const InkConsumeFullChargeSplatling = isSplatling
		? params.WeaponParam?.InkConsume
		: undefined;

	const InkConsumeSlosher = isSlosher
		? params.WeaponParam?.InkConsume
		: undefined;

	// for blasters these values are the same and represent damage caused by direct
	const DamageParam_ValueDirect =
		params.DamageParam?.ValueMax &&
		params.DamageParam?.ValueMax === params.DamageParam?.ValueMin
			? params.DamageParam?.ValueMax
			: undefined;

	const MoveSpeed_Charge = () => {
		if (weapon.Id === 4010) return 0.062;

		return params.WeaponParam?.MoveSpeed_Charge;
	};

	const DamageParam_ValueMax = () => {
		if (DamageParam_ValueDirect) return undefined;

		return (
			params.DamageParam?.ValueMax ??
			params.spl__BulletStringerParam?.DamageParam?.DirectHitDamageMax ??
			params.spl__BulletShelterShotgunParam?.DamageEffectiveTotalMax
		);
	};

	const InkConsume_WeaponShelterShotgunParam = () => {
		return params.spl__WeaponShelterShotgunParam?.InkConsume;
	};

	const BlastParam_DistanceDamage = () => {
		// REEF-LUX has distance damage listed in params
		// but actually doesn't deal it in game
		if (weapon.Id === 7020 || weapon.Id === 7021 || weapon.Id === 7022)
			return undefined;

		return (
			params.BlastParam?.DistanceDamage ??
			params.BlastParam?.BlastParam?.DistanceDamage ??
			params.spl__BulletStringerParam?.DetonationParam?.BlastParam
				?.DistanceDamage
		);
	};

	const slosherDirectDamage = () => {
		const DamageParam_ValueDirectMax =
			params.UnitGroupParam?.Unit?.[0]?.DamageParam?.ValueMax;
		const DamageParam_ValueDirectMin =
			params.UnitGroupParam?.Unit?.[0]?.DamageParam?.ValueMin;

		if (
			DamageParam_ValueDirectMax &&
			DamageParam_ValueDirectMax === DamageParam_ValueDirectMin
		) {
			return {
				DamageParam_ValueDirect: DamageParam_ValueDirectMax,
			};
		}

		return {
			DamageParam_ValueDirectMax,
			DamageParam_ValueDirectMin,
		};
	};

	const slosherDirectDamageSecondary = () => {
		const isDreadWringer =
			weapon.Id === 3050 || weapon.Id === 3051 || weapon.Id === 3052;
		if (!isDreadWringer) return;

		const DamageParam_Secondary_ValueDirectMax =
			params.UnitGroupParam?.Unit?.[1]?.DamageParam?.ValueMax;
		const DamageParam_Secondary_ValueDirectMin =
			params.UnitGroupParam?.Unit?.[1]?.DamageParam?.ValueMin;

		return {
			DamageParam_Secondary_ValueDirectMax,
			DamageParam_Secondary_ValueDirectMin,
		};
	};

	const KeepChargeFullFrame =
		params.WeaponKeepChargeParam?.KeepChargeFullFrame ??
		params.spl__WeaponStringerParam?.ChargeKeepParam?.KeepChargeFullFrame;

	const isSloshingMachine = weapon.Id === 3020 || weapon.Id === 3021;

	const DamageParam_SplatanaHorizontalDirect =
		params.BulletSaberHorizontalParam?.DamageParam?.HitDamage +
		params.BulletSaberSlashHorizontalParam?.DamageParam?.DamageValue;

	const resolveMin = (
		valueOne: number | null | undefined,
		valueTwo: number | null | undefined,
	): number | undefined => {
		if (typeof valueOne !== "number" && typeof valueTwo !== "number")
			return undefined;

		if (typeof valueOne !== "number") return valueTwo as number;
		if (typeof valueTwo !== "number") return valueOne;

		return Math.min(valueOne, valueTwo);
	};

	const resolveMax = (
		valueOne: number | null | undefined,
		valueTwo: number | null | undefined,
	): number | undefined => {
		if (typeof valueOne !== "number" && typeof valueTwo !== "number")
			return undefined;

		if (typeof valueOne !== "number") return valueTwo as number;
		if (typeof valueTwo !== "number") return valueOne;

		return Math.max(valueOne, valueTwo);
	};

	const rangeParams = extractRangeParams(weapon, params);

	return {
		SpecialPoint: weapon.SpecialPoint,
		subWeaponId: resolveSubWeaponId(weapon),
		specialWeaponId: resolveSpecialWeaponId(weapon),
		overwrites: resolveOverwrites(params),
		...rangeParams,
		TripleShotSpanFrame: params.WeaponParam?.TripleShotSpanFrame,
		WeaponSpeedType:
			params.MainWeaponSetting?.WeaponSpeedType === "Mid"
				? undefined
				: params.MainWeaponSetting?.WeaponSpeedType,
		MoveSpeed:
			params.WeaponParam?.MoveSpeed ??
			params.spl__WeaponShelterShotgunParam?.MoveSpeed,
		MoveSpeed_Charge: MoveSpeed_Charge(),
		MoveSpeedFullCharge:
			params.WeaponParam?.MoveSpeedFullCharge ??
			params.spl__WeaponStringerParam?.ChargeParam?.MoveSpeedFullCharge,
		MoveSpeedVariable: params.VariableShotParam?.MoveSpeed,
		DamageParam_ValueMax: DamageParam_ValueMax(),
		DamageParam_ValueMin: !DamageParam_ValueDirect
			? (params.DamageParam?.ValueMin ??
				params.spl__BulletStringerParam?.DamageParam?.DirectHitDamageMin)
			: undefined,
		DamageParam_ValueDirect,
		...slosherDirectDamage(),
		...slosherDirectDamageSecondary(),
		// Glooga turret mode damage
		DamageLapOverParam_ValueMax: params.DamageLapOverParam?.ValueMax,
		DamageLapOverParam_ValueMin: params.DamageLapOverParam?.ValueMin,
		BlastParam_SplashDamage: isSloshingMachine
			? params.UnitGroupParam?.Unit?.[1]?.DamageParam?.ValueMax
			: undefined,
		BlastParam_DistanceDamage: BlastParam_DistanceDamage(),
		DamageParam_ValueFullCharge: params.DamageParam?.ValueFullCharge,
		DamageParam_ValueFullChargeMax:
			params.DamageParam?.ValueFullChargeMax !== DamageParam_ValueMax()
				? params.DamageParam?.ValueFullChargeMax
				: undefined,
		DamageParam_ValueMaxCharge: params.DamageParam?.ValueMaxCharge,
		DamageParam_ValueMinCharge: params.DamageParam?.ValueMinCharge,
		DamageParam_SplatanaVerticalDirect:
			params.BulletSaberSlashVerticalParam?.DamageParam?.DamageValue,
		DamageParam_SplatanaVertical:
			params.BulletSaberVerticalParam?.DamageParam?.HitDamage,
		DamageParam_SplatanaHorizontalDirect: Number.isNaN(
			DamageParam_SplatanaHorizontalDirect,
		)
			? undefined
			: DamageParam_SplatanaHorizontalDirect,
		DamageParam_SplatanaHorizontal:
			params.BulletSaberHorizontalParam?.DamageParam?.HitDamage,
		BodyParam_Damage: params.BodyParam?.Damage,
		Variable_Damage_ValueMax: params.VariableDamageParam?.ValueMax,
		Variable_Damage_ValueMin: params.VariableDamageParam?.ValueMin,
		SwingUnitGroupParam_DamageParam_DamageMinValue: resolveMin(
			params.SwingUnitGroupParam?.DamageParam?.Inside?.DamageMinValue,
			params.SwingUnitGroupParam?.DamageParam?.Outside?.DamageMinValue,
		),
		SwingUnitGroupParam_DamageParam_DamageMaxValue: resolveMax(
			params.SwingUnitGroupParam?.DamageParam?.Inside?.DamageMaxValue,
			params.SwingUnitGroupParam?.DamageParam?.Outside?.DamageMaxValue,
		),
		// roller splash damages
		VerticalSwingUnitGroupParam_DamageParam_DamageMinValue: resolveMin(
			params.VerticalSwingUnitGroupParam?.DamageParam?.Inside?.DamageMinValue,
			params.VerticalSwingUnitGroupParam?.DamageParam?.Outside?.DamageMinValue,
		),
		VerticalSwingUnitGroupParam_DamageParam_DamageMaxValue: resolveMax(
			params.VerticalSwingUnitGroupParam?.DamageParam?.Inside?.DamageMaxValue,
			params.VerticalSwingUnitGroupParam?.DamageParam?.Outside?.DamageMaxValue,
		),
		WideSwingUnitGroupParam_DamageParam_DamageMinValue: resolveMin(
			params.WideSwingUnitGroupParam?.DamageParam?.Inside?.DamageMinValue,
			params.WideSwingUnitGroupParam?.DamageParam?.Outside?.DamageMinValue,
		),
		WideSwingUnitGroupParam_DamageParam_DamageMaxValue: resolveMax(
			params.WideSwingUnitGroupParam?.DamageParam?.Inside?.DamageMaxValue,
			params.WideSwingUnitGroupParam?.DamageParam?.Outside?.DamageMaxValue,
		),
		CanopyHP:
			params.spl__BulletShelterCanopyParam?.CanopyHP ??
			(weapon.Id === 6000 || weapon.Id === 6001 || weapon.Id === 6005
				? 5000
				: undefined),
		ChargeFrameFullCharge:
			params.WeaponParam?.ChargeFrameFullCharge ??
			params.spl__WeaponStringerParam?.ChargeParam?.ChargeFrameFullCharge,
		KeepChargeFullFrame:
			KeepChargeFullFrame !== 1 ? KeepChargeFullFrame : undefined,
		Jump_DegSwerve: params.WeaponParam?.Jump_DegSwerve,
		Stand_DegSwerve: params.WeaponParam?.Stand_DegSwerve,
		Variable_Jump_DegSwerve:
			params.VariableWeaponParam?.Jump_DegSwerve ??
			params.VariableShotParam?.Jump_DegSwerve,
		Variable_Stand_DegSwerve:
			params.VariableWeaponParam?.Stand_DegSwerve ??
			params.VariableShotParam?.Stand_DegSwerve,
		InkRecoverStop: params.WeaponParam?.InkRecoverStop,
		InkConsume,
		InkConsumeSlosher,
		InkConsumeFullCharge: params.WeaponParam?.InkConsumeFullCharge,
		InkConsumeMinCharge: params.WeaponParam?.InkConsumeMinCharge,
		InkConsumeFullChargeSplatling,
		InkConsume_WeaponSwingParam: params.WeaponSwingParam?.InkConsume,
		InkConsume_WeaponVerticalSwingParam:
			params.WeaponVerticalSwingParam?.InkConsume,
		InkConsume_WeaponWideSwingParam: params.WeaponWideSwingParam?.InkConsume,
		InkConsumeUmbrella_WeaponShelterCanopyParam:
			params.spl__WeaponShelterCanopyParam?.InkConsumeUmbrella !== 0
				? params.spl__WeaponShelterCanopyParam?.InkConsumeUmbrella
				: undefined,
		InkConsume_WeaponShelterShotgunParam:
			InkConsume_WeaponShelterShotgunParam(),
		InkConsume_SideStepParam: params.SideStepParam?.InkConsume,
		InkConsume_SwingParam: params.spl__WeaponSaberParam?.SwingParam?.InkConsume,
		InkConsumeFullCharge_ChargeParam:
			params.spl__WeaponSaberParam?.ChargeParam?.InkConsumeFullCharge ??
			params.spl__WeaponStringerParam?.ChargeParam?.InkConsumeFullCharge,
	};
}

function combineSwingsIfSame(params: MainWeaponParams): MainWeaponParams {
	if (
		!params.InkConsume_WeaponVerticalSwingParam ||
		params.InkConsume_WeaponVerticalSwingParam !==
			params.InkConsume_WeaponWideSwingParam
	) {
		return params;
	}

	return {
		...params,
		InkConsume_WeaponSwingParam: params.InkConsume_WeaponVerticalSwingParam,
		InkConsume_WeaponVerticalSwingParam: undefined,
		InkConsume_WeaponWideSwingParam: undefined,
	};
}

function parametersToSubWeaponResult(
	subWeapon: SubWeapon,
	params: any,
): SubWeaponParams {
	const SubInkSaveLv = params.SubWeaponSetting?.SubInkSaveLv ?? 2;

	return {
		overwrites: resolveSubWeaponOverwrites(subWeapon, params),
		SubInkSaveLv,
		InkConsume: params.WeaponParam.InkConsume ?? 0.7,
		InkRecoverStop: params.WeaponParam.InkRecoverStop,
		DistanceDamage: params.BlastParamMaxCharge?.DistanceDamage
			? // curling bomb difference charge to same key
				[
					params.BlastParamMaxCharge.DistanceDamage,
					params.BlastParamMinCharge?.DistanceDamage,
				]
			: params.BlastParam?.DistanceDamage,
		DirectDamage:
			params.MoveParam?.DirectDamage ?? params.MoveParam?.DamageDirectHit,
		DistanceDamage_BlastParamArray: params.MoveParam?.BlastParamArray?.map(
			(b: any) => b.DistanceDamage,
		),
		DistanceDamage_BlastParamChase: params.BlastParamChase?.DistanceDamage,
		DistanceDamage_SplashBlastParam:
			params.BlastParamChase?.SplashBlastParam?.DistanceDamage,
	};
}

// Thrown targeting specials (Booyah Bomb, Super Chump, Triple Inkstrike, Ultra Stamp) are
// player-aimed and have no throw-distance param in the game data, so their throw (fly) range is
// approximated with a constant. Kept as named per-special constants so each can be tuned later;
// for now they all reuse Booyah Bomb's value.
const BOOYAH_BOMB_FLY_DISTANCE = 27;
const SUPER_CHUMP_FLY_DISTANCE = 30;
const TRIPLE_INKSTRIKE_FLY_DISTANCE = 28;
const ULTRA_STAMP_FLY_DISTANCE = 24;

// Range circle data for the map planner. Projectile / thrown specials store the distance the shot
// travels plus its blast; a few store a single effect radius. Specials whose reach is global
// (Tenta Missiles, Killer Wail), placed or utility (Big Bubbler, Tacticooler, Ink Storm, Ink Vac),
// or otherwise has no meaningful circle (Kraken Royale, Splattercolor Screen) are left out.
// See app/features/comp-analyzer/core/special-weapon-range.ts.
function specialWeaponRangeParams(
	specialWeapon: SpecialWeapon,
	rawParams: any,
): Record<string, number | undefined> {
	const outerBlastDistance = (blastParam: any): number | undefined =>
		blastParam?.DistanceDamage?.at(-1)?.Distance;

	const trajectoryFromMoveParam = (moveParam: any) => ({
		Range_SpawnSpeed: moveParam?.SpawnSpeed,
		Range_GoStraightStateEndMaxSpeed: moveParam?.GoStraightStateEndMaxSpeed,
		Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame,
		Range_FreeGravity: moveParam?.FreeGravity,
		Range_FreeAirResist: moveParam?.FreeAirResist,
		Range_BrakeAirResist: moveParam?.BrakeAirResist,
		Range_BrakeGravity: moveParam?.BrakeGravity,
		Range_BrakeToFreeStateFrame: moveParam?.BrakeToFreeStateFrame,
	});

	switch (specialWeapon.__RowId) {
		case "SpUltraShot": // Trizooka
			return {
				...trajectoryFromMoveParam(rawParams.MoveParam),
				Range_BlastRadius: outerBlastDistance(rawParams.BlastParam),
			};
		case "SpJetpack": // Inkjet (shot flies straight for a fixed distance)
			return {
				Range_Distance: rawParams.MoveParam?.Distance,
				Range_BlastRadius: outerBlastDistance(rawParams.BlastParam),
			};
		case "SpChariot": // Crab Tank (rapid gun reach)
			return {
				...trajectoryFromMoveParam(rawParams.ShooterMoveParam),
				Range_BlastRadius: outerBlastDistance(
					rawParams.CannonParam?.BlastParam,
				),
			};
		case "SpNiceBall": // Booyah Bomb (thrown, then bursts on impact)
			return {
				Range_Distance: BOOYAH_BOMB_FLY_DISTANCE,
				Range_BlastRadius: rawParams.BlastParam?.DamageRadiusEnd,
			};
		case "SpFirework": // Super Chump (thrown, chumps burst on impact)
			return {
				Range_Distance: SUPER_CHUMP_FLY_DISTANCE,
				Range_BlastRadius: outerBlastDistance(rawParams.IceParam?.BlastParam),
			};
		case "SpTripleTornado": // Triple Inkstrike (thrown beacons, strikes burst)
			return {
				Range_Distance: TRIPLE_INKSTRIKE_FLY_DISTANCE,
				Range_BlastRadius: rawParams.BlastParam?.DamageRadiusEnd,
			};
		case "SpUltraStamp": // Ultra Stamp (thrown stamp bursts on impact)
			return {
				Range_Distance: ULTRA_STAMP_FLY_DISTANCE,
				Range_BlastRadius: outerBlastDistance(rawParams.ThrowBlastParam),
			};
		case "SpSuperHook": // Zipcaster (grapple reach)
			return { Range_Radius: rawParams.WeaponParam?.MaxLengthHook };
		case "SpShockSonar": // Wave Breaker (tracking wave max radius, base Special Power Up)
			return {
				Range_Radius:
					rawParams.spl__BulletSpShockSonarParam?.WaveParam?.MaxRadius?.Low,
			};
		case "SpSkewer": // Reefslider (lethal blast)
			return {
				Range_Radius: rawParams.BulletBlastParam?.DistanceDamage?.[0]?.Distance,
			};
		case "SpPogo": // Triple Splashdown (lethal blast, per splashdown)
			return {
				Range_Radius: rawParams.BlastParamNormal?.DistanceDamage?.[0]?.Distance,
			};
		default:
			return {};
	}
}

// some specials lack damage values in the params
// so they are instead hardcoded here as a workaround
function parametersToSpecialWeaponResult(params: any) {
	const result: any = {};

	for (const parentValue of Object.values(params)) {
		for (const entries of Object.entries(parentValue as any)) {
			const [key, value]: any = entries;
			if (
				key === "SubSpecialSpecUpList" ||
				value.High !== value.Mid ||
				value.Mid !== value.Low
			) {
				result[key] = value;
			}

			for (const innerEntries of Object.entries(value)) {
				const [innerMostKey, innerMostValue]: any = innerEntries;
				if (typeof innerMostValue !== "object") continue;
				if (
					innerMostKey === "SubSpecialSpecUpList" ||
					innerMostValue.High !== innerMostValue.Mid ||
					innerMostValue.Mid !== innerMostValue.Low
				) {
					result[innerMostKey] = innerMostValue;
				}
			}
		}
	}

	// for Ultra Splashdown
	if (params.BlastParamDokanWarp) {
		result.SubSpecialSpecUpList =
			params.BlastParamDokanWarp.SubSpecialSpecUpList;
	}

	const resultUnwrapped = unwrapSubSpecialSpecUpList(result);

	const specialDurationFrameKeyAlises = [
		"LaserFrame",
		"RainyFrame",
		"SpecialTotalFrame",
	];

	for (const key of specialDurationFrameKeyAlises) {
		if (!resultUnwrapped[key]) continue;

		resultUnwrapped.SpecialDurationFrame = resultUnwrapped[key];
		resultUnwrapped[key] = undefined;
	}

	if (resultUnwrapped.SplashAroundPaintRadius) {
		// Inkjet
		if (params.BlastParam?.PaintRadius) {
			const regularPaintRadius = params.BlastParam.PaintRadius;

			resultUnwrapped.PaintRadius = {
				High: resultUnwrapped.SplashAroundPaintRadius.High + regularPaintRadius,
				Mid: resultUnwrapped.SplashAroundPaintRadius.Mid + regularPaintRadius,
				Low: resultUnwrapped.SplashAroundPaintRadius.Low + regularPaintRadius,
			};
			// Reefslider
		} else {
			resultUnwrapped.PaintRadius = {
				High:
					resultUnwrapped.SplashAroundPaintRadius.High +
					resultUnwrapped.PaintRadius.High,
				Mid:
					resultUnwrapped.SplashAroundPaintRadius.Mid +
					resultUnwrapped.PaintRadius.Mid,
				Low:
					resultUnwrapped.SplashAroundPaintRadius.Low +
					resultUnwrapped.PaintRadius.Low,
			};
		}

		resultUnwrapped.SplashAroundPaintRadius = undefined;
	}

	// Bubble Patch 7.2.0 change hack
	if (resultUnwrapped.MaxFieldHP) {
		resultUnwrapped.MaxFieldHP = {
			High: 28800,
			Low: 24000,
			Mid: 26400,
		};
	}

	const isUltraStamp = () => !!params.SwingBigBlastParam;
	const isCrabTank = () => !!params.CannonParam;
	const isKraken = () => !!params.BodyParam?.DamageJumpValue;
	const isInkjet = () => !!params.JetParam;
	const isScreen = () => !!params.WallParam;
	const isInkStorm = () => !!params.CloudParam;
	const isInkstrike = () => !!params.MotherParam;
	const isBooyahBomb = () =>
		params.BlastParam?.$type === "spl__BulletSpNiceBallBlastParam";

	const SwingDamage = () => {
		if (!isUltraStamp()) return;

		return [
			{ Damage: 1000, Distance: 0 },
			...params.SwingBigBlastParam.DistanceDamage,
		];
	};

	const ThrowDamage = () => {
		if (!isUltraStamp()) return;

		return params.ThrowBlastParam.DistanceDamage;
	};

	const Cannon = () => {
		if (!isCrabTank()) return;

		invariant(
			params.CannonParam.BlastParam.DistanceDamage,
			"Could not find cannon distance damage",
		);

		return [
			{
				Damage: 500,
				Distance: 1,
			},
			...params.CannonParam.BlastParam.DistanceDamage,
		];
	};

	const KrakenDirectDamage = () => {
		if (!isKraken()) return;

		return 1200;
	};

	const InkjetDirectDamage = () => {
		if (!isInkjet()) return;

		return 1200;
	};

	const ScreenDirectDamage = () => {
		if (!isScreen()) return;

		return 400;
	};

	const BooyahBombTickDamage = () => {
		if (!isBooyahBomb()) return;

		return 33;
	};

	const InkStormTickDamage = () => {
		if (!isInkStorm()) return;

		return 4;
	};

	const InkstrikeTickDamage = () => {
		if (!isInkstrike()) return;

		return 75;
	};

	return {
		ArmorHP: params.WeaponSpChariotParam?.ArmorHP,
		overwrites: resultUnwrapped,
		DistanceDamage:
			params.BlastParam?.DistanceDamage ??
			params.HookBlastParam?.DistanceDamage ??
			params.spl__BulletBlastParam?.DistanceDamage ??
			params.BulletBlastParam?.DistanceDamage ??
			params.IceParam?.BlastParam?.DistanceDamage ??
			params.BlastParamNormal?.DistanceDamage,
		DirectDamage:
			params.DamageParam?.DirectHitDamage ??
			params.spl__BulletSpShockSonarParam?.GeneratorParam?.HitDamage ??
			KrakenDirectDamage() ??
			InkjetDirectDamage() ??
			ScreenDirectDamage(),
		WaveDamage: params.spl__BulletSpShockSonarParam?.WaveParam?.Damage,
		ExhaleBlastParamMinChargeDistanceDamage:
			params.ExhaleBlastParamMinCharge?.DistanceDamage,
		ExhaleBlastParamMaxChargeDistanceDamage:
			params.ExhaleBlastParamMaxCharge?.DistanceDamage,
		SwingDamage: SwingDamage(),
		ThrowDamage: ThrowDamage(),
		ThrowDirectDamage: params.ThrowMoveParam?.DirectDamageValue,
		BulletDamageMin: params.ShooterDamageParam?.ValueMin,
		BulletDamageMax: params.ShooterDamageParam?.ValueMax,
		CannonDamage: Cannon(),
		BumpDamage: isCrabTank() ? 400 : undefined,
		JumpDamage: params.BodyParam?.DamageJumpValue,
		TickDamage:
			BooyahBombTickDamage() ??
			InkStormTickDamage() ??
			InkstrikeTickDamage() ??
			params.spl__BulletSpMicroLaserBitParam?.LaserParam?.LaserDamage,
	};
}

function unwrapSubSpecialSpecUpList(result: any) {
	return Object.fromEntries(
		Object.entries(result).flatMap((entries) => {
			const [key, value]: any = entries;
			if (Array.isArray(value)) {
				return value.map((v: any) => {
					if (
						!v.SpecUpType ||
						(v.Value.Low === v.Value.Mid && v.Value.Mid === v.Value.High)
					) {
						return [];
					}

					return [v.SpecUpType, v.Value];
				});
			}

			return [[key, value]];
		}),
	);
}

function resolveSubWeaponId(weapon: MainWeapon) {
	const codeName = weapon.SubWeapon.replace("Work/Gyml/", "").replace(
		".spl__WeaponInfoSub.gyml",
		"",
	);

	const subWeaponObj = subWeapons.find((wpn) => codeName === wpn.__RowId);
	invariant(subWeaponObj, `Could not find sub weapon for '${weapon.__RowId}'`);
	invariant(
		subWeaponIds.includes(subWeaponObj.Id as any),
		"Invalid sub weapon id",
	);

	return subWeaponObj.Id as SubWeaponId;
}

function resolveSpecialWeaponId(weapon: MainWeapon) {
	const codeName = weapon.SpecialWeapon.replace("Work/Gyml/", "").replace(
		".spl__WeaponInfoSpecial.gyml",
		"",
	);

	const specialWeaponObj = specialWeapons.find(
		(wpn) => codeName === wpn.__RowId,
	);
	invariant(
		specialWeaponObj,
		`Could not find special weapon for '${codeName}'`,
	);

	return specialWeaponObj.Id as SpecialWeaponId;
}

const overwriteSchema = z.object({
	High: z.number().optional(),
	Mid: z.number().optional(),
	Low: z.number().optional(),
});

function resolveOverwrites(params: any) {
	const result: MainWeaponParams["overwrites"] = {};

	for (const [key, value] of Object.entries(params)) {
		const parsed = overwriteSchema.safeParse(value);

		resolveOverwritesWithArbitraryKeys(result, value);

		// each object has a $type property which we ignore
		if (
			key.includes("PlayerGearSkillParam") &&
			parsed.success &&
			Object.keys(parsed).length > 1
		) {
			const abilityKey = key.split("_").at(-1);
			invariant(abilityKey, `Could not find ability key for '${key}'`);

			if (!parsed.data.High && !parsed.data.Mid && !parsed.data.Low) {
				continue;
			}

			result[abilityKey] = {
				High: parsed.data.High,
				Mid: parsed.data.Mid,
				Low: parsed.data.Low,
			};
		}
	}

	if (Object.keys(result).length === 0) return;

	return result;
}

function resolveOverwritesWithArbitraryKeys(
	result: NonNullable<MainWeaponParams["overwrites"]>,
	paramsObj: unknown,
) {
	for (const [key, value] of Object.entries(
		paramsObj as Record<string, number>,
	)) {
		if (!key.startsWith("Overwrite_")) continue;
		if (value === -1) continue;

		let abilityKey = key.replace("Overwrite_", "");

		for (const type of ["High", "Mid", "Low"] as const) {
			const suffix = `_${type}`;
			if (!abilityKey.endsWith(suffix)) continue;

			abilityKey = abilityKey.replace(suffix, "");

			if (!result[abilityKey]) result[abilityKey] = {};

			result[abilityKey]![type] = value;
		}
	}
}

function resolveSubWeaponOverwrites(subWeapon: SubWeapon, params: any) {
	const result: SubWeaponParams["overwrites"] = {
		SpawnSpeedZSpecUp: params.MoveParam?.SpawnSpeedZSpecUp,
		PeriodFirst: params.MoveParam?.PeriodFirst,
		PeriodSecond: params.MoveParam?.PeriodSecond,
		MarkingFrameSubSpec:
			params.MoveParam?.MarkingFrameSubSpec ??
			params.MoveParam?.MarkingFrame ??
			params.AreaParam?.MarkingFrameSubSpec,
		SensorRadius: params.MoveParam?.SensorRadius,
		ExplosionRadius: params.AreaParam?.Distance,
		MaxHP: params.MoveParam?.MaxHP,
		SubSpecUpParam: (subWeapon.Id === SQUID_BEAKON_ID
			? {
					Low: 0.0,
					...playersParams.GameParameters.spl__PlayerBeaconSubSpecUpParam
						.SubSpecUpParam,
				}
			: undefined) as any,
	};

	return Object.fromEntries(
		Object.entries(result).filter(
			([_key, value]) =>
				value && (value.High !== value.Mid || value.Low !== value.High),
		),
	);
}

const WEAPON_TYPES_TO_IGNORE = [
	"Mission",
	"Coop",
	"Hero",
	"Rival",
	"SalmonBuddy",
	"Sdodr",
];

const INTERNAL_WEAPON_NAMES_TO_IGNORE: readonly string[] = ["Free"] as const;
function mainWeaponShouldBeSkipped(mainWeapon: MainWeapon) {
	if (
		WEAPON_TYPES_TO_IGNORE.includes(mainWeapon.Type) ||
		INTERNAL_WEAPON_NAMES_TO_IGNORE.includes(mainWeapon.__RowId) ||
		mainWeapon.Season > CURRENT_SEASON
	) {
		return true;
	}

	return false;
}

function subWeaponShouldBeSkipped(subWeapon: SubWeapon) {
	if (subWeapon.Id === 10000) return true;
	if (WEAPON_TYPES_TO_IGNORE.some((val) => subWeapon.__RowId.includes(val))) {
		return true;
	}

	return false;
}

function specialWeaponShouldBeSkipped(specialWeapon: SpecialWeapon) {
	if (
		WEAPON_TYPES_TO_IGNORE.some((val) => specialWeapon.Type.includes(val)) ||
		WEAPON_TYPES_TO_IGNORE.some((val) => specialWeapon.__RowId.includes(val))
	) {
		return true;
	}
	if (specialWeapon.__RowId === "SpGachihoko") return true;
	if (specialWeapon.__RowId === "SpGachihokoForEventMatch") return true;

	return false;
}

function loadWeaponParamsObject(
	weapon: MainWeapon | SubWeapon | SpecialWeapon,
) {
	return JSON.parse(
		fs.readFileSync(
			path.join(weaponParamsDir(), weaponRowIdToFileName(weapon)),
			"utf8",
		),
	).GameParameters;
}

function weaponRowIdToFileName(weapon: MainWeapon | SubWeapon | SpecialWeapon) {
	const [category, codeName] = weapon.__RowId.split("_");
	invariant(category);

	return `Weapon${category}${codeName ?? ""}.game__GameParameterTable.json`;
}

function translationsToArray({
	arr,
	internalName,
	weaponId,
	type,
	translations,
}: {
	arr: TranslationArray;
	internalName: string;
	weaponId: number;
	type: "Main" | "Sub" | "Special";
	translations: [
		langCode: string,
		translations: Record<string, Record<string, string>>,
	][];
}) {
	for (const langCode of LANG_JSONS_TO_CREATE) {
		const translationOfLanguage = translations.find((t) => t[0] === langCode);
		invariant(
			translationOfLanguage,
			`Could not find translation for '${langCode}'`,
		);

		const value =
			translationOfLanguage[1][`CommonMsg/Weapon/WeaponName_${type}`]?.[
				internalName
			];
		invariant(value, `Could not find translation for '${internalName}'`);

		arr.push({
			key: `${type.toUpperCase()}_${weaponId}`,
			language: langCode,
			value,
		});
	}
}

function writeTranslationsJsons(arr: TranslationArray) {
	for (const langCode of LANG_JSONS_TO_CREATE) {
		fs.writeFileSync(
			path.join(
				__dirname,
				"..",
				"locales",
				translationJsonFolderName(langCode),
				"weapons.json",
			),
			`${JSON.stringify(
				Object.fromEntries(
					arr
						.filter((val) => val.language === langCode)
						.map(({ key, value }) => [key, value]),
				),
				null,
				"\t",
			)}\n`,
		);
	}
}

function logWeaponIds(weapons: Record<number, WeaponKit>) {
	logger.info(JSON.stringify(Object.keys(weapons).map(Number)));
}

void main();
