import { ItemStack, world, system, TimeOfDay } from "@minecraft/server";
import * as vector3 from "./vector3";

let scoreboard = world.scoreboard;
export const overworld = world.getDimension('overworld');
export const nether = world.getDimension('nether');
export const the_end = world.getDimension('the_end');
export const dimensions = [overworld, nether, the_end];

export let player0 = undefined;

export function randomInt(min, max) {
	return Math.floor(Math.random() * (max - min + 1))+min;
}
export function floorVector(vector)
{
	return { x: Math.floor(vector.x), y: Math.floor(vector.y+0.5), z: Math.floor(vector.z) };
}

export async function waitForTicks(ticks, func) {
    return await system.runTimeout(func, ticks);
}

export function getItemInHand(player) {
	const playerInventory = player.getComponent("minecraft:inventory").container;
	const itemInHand = playerInventory.getItem(player.selectedSlotIndex);
	return itemInHand;
}

export function getItemInOffHand(player) {
    const equippable = player.getComponent("minecraft:equippable");
    const itemInOffHand = equippable.getEquipment("Offhand");
    return itemInOffHand;
}

export function setItemInHand(player, item) {
	const playerInventory = player.getComponent("minecraft:inventory").container;
	playerInventory.setItem(player.selectedSlotIndex, item);
}

export function setItemInOffHand(player, item) {
    const equippable = player.getComponent("minecraft:equippable");
    equippable.setEquipment("Offhand", item);
}

export function getDurabilityInHand(player) {
	const itemInHand = getItemInHand(player);
	if (!itemInHand) return;
	const durabilityComponent = itemInHand.getComponent("minecraft:durability");
	if (!durabilityComponent) return;
	return durabilityComponent.damage
}

export function getDurabilityInOffHand(player) {
	const itemInOffHand = getItemInOffHand(player);
	if (!itemInOffHand) return;
	const durabilityComponent = itemInOffHand.getComponent("minecraft:durability");
	if (!durabilityComponent) return;
	return durabilityComponent.damage
}

export function playAnimation(entity, animation, options) {
	if (!entity) throw new Error("playAnimation: Invalid entity");
	if (!animation) throw new Error("playAnimation: Invalid animation");

	const args = [
		animation,
		options && options.nextState ? `${options.nextState}` : undefined,
		options && options.blendOutTime ? `${options.blendOutTime}` : undefined,
		options && options.stopExpression ? `"${options.stopExpression}"` : undefined,
		options && options.controller ? `${options.controller}` : undefined
	];

	let skippedOptional = false;
	let lastValidIndex = 0;

	for (let i = 1; i < args.length; i++) {
		if (args[i] === undefined) {
			skippedOptional = true;
		} else {
			if (skippedOptional) {
				throw new Error("playAnimation: encountered a value after a missing optional parameter.");
			}
			lastValidIndex = i;
		}
	}

	const trimmedArgs = args.slice(0, lastValidIndex + 1);
	entity.runCommandAsync(`playanimation @s ${trimmedArgs.join(" ")}`);
}

export async function waitForLoopEvent(ticks, callback) {
    return new Promise(resolve => {
        let id = 0;
        const event = async function () {
            var _a;
            if (await callback()) {
                resolve();
            }
            else {
                id = system.runTimeout(event, ticks);
            }
        };
        system.run(event);
    });
}

export async function onLoad() {
	if (player0)
		return;
	
	await waitForLoopEvent(1, () => {
		return world.getAllPlayers().length > 0;
	});
	player0 = await world.getAllPlayers()[0];
	if (!player0.hasTag("tcs_cwp_player0"))
	{
		player0.addTag("tcs_cwp_player0");
		
		let spawnPoint = world.getDefaultSpawnLocation();
		if (spawnPoint.y > 320)
			spawnPoint.y = player0.location.y;
		setWorldParam("tcs_cwp_default_spawn", spawnPoint);
	}
}


export async function runCommand(command) {
	return await overworld.runCommandAsync(command);
}

export async function runPlayer0Command(command) {
	return await player0.runCommandAsync(command);
}

export async function runPlayerCommand(entity, command) {
	return await entity.runCommandAsync(command);
}
export async function runDelayedPlayerCommand(delay, entity, command) {
	return await system.runTimeout(() => { runPlayerCommand(entity, command); }, delay);
}
export async function runDelayedEntityCommand(delay, entity, command) {
	return await system.runTimeout(() => { runEntityCommand(entity, command); }, delay);
}
export async function runMobCommand(entity, command) {
	return await entity.runCommandAsync(command);
}
export async function runEntityCommand(entity, command) {
	return await entity.runCommandAsync(command);
}

export function applyKnockback(entity, x, z, strength, verticalStrength) {
	try { entity.applyKnockback(x, z, strength, verticalStrength); } catch (error) { }
}

export function applyDamage(entity, damage, options) {
	try { entity.applyDamage(damage); } catch (error) { }
}

export async function getAllMobs()
{
	let entities = await dimensions.flatMap((dim) => (dim.getEntities()));
	entities = entities.filter( (entity) =>  entity.typeId !== "minecraft:player");
	return entities;
}
export async function getEntities(entityType)
{
	const entities = await dimensions.flatMap((dim) => (dim.getEntities({ type: entityType })));
	return entities;
}
export async function getAllEntitiesWithTags(entityTags)
{
	const entities = await dimensions.flatMap((dim) => (dim.getEntities({ tags: entityTags })));
	return entities;
}
export async function getEntitiesWithTags(entityType, entityTags)
{
	const entities = await dimensions.flatMap((dim) => (dim.getEntities({ type: entityType, tags: entityTags })));
	return entities;
}
export async function getEntityNearEntity(entityType, nearbyEntity)
{
	const entities = await nearbyEntity.dimension.getEntities( { type: entityType, location: nearbyEntity.location, closest: 1 } );
	return entities;
}
export async function getEntityInLocation(entityType, dim, centerLocation)
{
	const entities = await dim.getEntities( { type: entityType, location: centerLocation, closest: 1 } );
	return entities[0];
}
export async function getPlayerInLocation(dim, centerLocation)
{
	const players = await dim.getPlayers( { location: centerLocation, closest: 1 } );
	return players;
}
export async function getPlayersInRange(dim, centerLocation, minDist, maxDist)
{
	const players = await dim.getPlayers( { location: centerLocation, minDistance: minDist, maxDistance: maxDist } );
	return players;
}
export async function getPlayersWithTags(playerTags)
{
	const players = await dimensions.flatMap((dim) => (dim.getPlayers({ tags: playerTags })));
	return players;
}
export async function getEntitiesInRange(entityType, dim, centerLocation, minDist, maxDist)
{
	const entities = await dim.getEntities( { type: entityType, location: centerLocation, minDistance: minDist, maxDistance: maxDist } );
	return entities;
}

export function getMobsInRadius(dim, centerLocation, radius)
{
	const entities = dim.getEntities({ location: centerLocation, maxDistance: radius, excludeTypes: ["minecraft:player"] });
	return entities;
}

export async function getEntitiesInArea(entityType, dim, corner1, corner2, checkDimensions, tag)
{
	// checkDimensions - what dimensions to check - "x", "xz", "xyz", etc
	let entities = await dim.getEntities( { type: entityType } );
	if (tag !== undefined)
		entities = entities.filter( (entity) =>  entity.hasTag(tag));
	return entities.filter( (entity) =>  vector3.locationIsInArea(entity.location, corner1, corner2, checkDimensions));
}
export async function getPlayersInArea(dim, corner1, corner2, checkDimensions, tag)
{
	// checkDimensions - what dimensions to check - "x", "xz", "xyz", etc
	let players = await dim.getPlayers();
	if (tag !== undefined)
		players = players.filter( (player) =>  player.hasTag(tag));
	return players.filter( (player) =>  vector3.locationIsInArea(player.location, corner1, corner2, checkDimensions));
}

export async function getAllEntitiesInRange(dim, centerLocation, minDist, maxDist)
{
	const entities = await dim.getEntities( { location: centerLocation, minDistance: minDist, maxDistance: maxDist } );
	return entities;
}

const DEFAULT_MESSAGE_COOLDOWN = 3; // Cooldown time in seconds

// Add cooldown (in seconds) to stop K/D actionbar overriding message
export async function setMessageCooldown(cooldown = 1) {
	world.setDynamicProperty("tcs_cwp_message_cooldown", cooldown);
}

export async function showPlayersErrorMessage(entity, target, message)
{
	await setMessageCooldown(DEFAULT_MESSAGE_COOLDOWN);
	await actionbar(entity, target, message);
	await entity.runCommandAsync(`playsound tcs_cwp:error @a[r=5] ~ ~ ~ 0.8`);
}

let lastActionBarIndex = 0;
export async function actionBarTimed(entity, target, msg, duration) {
	lastActionBarIndex++;
	actionBarTimedRecursive(entity, target, msg, duration, lastActionBarIndex);
}
export async function actionBarTimedRecursive(entity, target, msg, remainingDuration, actionBarIndex) {
	if(actionBarIndex < lastActionBarIndex) return;
	actionbar(entity, target, msg);
	if(remainingDuration > 30) {
		system.runTimeout(() => {
			actionBarTimedRecursive(entity, target, msg, remainingDuration - 30, actionBarIndex)
		}, 30);
	}
}

export async function showPlayerErrorMessage(player, message)
{
	await setMessageCooldown(DEFAULT_MESSAGE_COOLDOWN);
	if (player !== undefined)
	{
		await runPlayerCommand(player, "title @s actionbar "+message);
		await player.playSound("tcs_cwp:error", { volume: 0.8 } );
	}
	else
	{
		await runCommand("title @a actionbar "+message);
		await runCommand("playsound tcs_cwp:error @a ~ ~ ~ 100 1 0.8");
	}
}

export async function showPlayerInfoMessage(player, message)
{
	await setMessageCooldown(DEFAULT_MESSAGE_COOLDOWN);
	if (player !== undefined)
	{
		await runPlayerCommand(player, "title @s actionbar "+message);
		await player.playSound("tcs_cwp:info", { volume: 0.8 } );
	}
	else
	{
		await runCommand("title @a actionbar "+message);
		await runCommand("playsound tcs_cwp:info @a ~ ~ ~ 100 1 0.8");
	}
}

export async function tellraw(entity, target, msg) {
	return await entity.runCommandAsync(`tellraw ${target} {"rawtext":[{"text":"§r${msg}"}]}`);
}

export async function title(entity, target, msg) {
	await setMessageCooldown(DEFAULT_MESSAGE_COOLDOWN);
	await runPlayerCommand(entity, `title ${target} times 20 60 20`);
	return await entity.runCommandAsync(`titleraw ${target} title {"rawtext":[{"text":"§r${msg}"}]}`);
}
export async function subtitle(entity, target, msg, addEmptyTitle=false) {
	await setMessageCooldown(DEFAULT_MESSAGE_COOLDOWN);
	await runPlayerCommand(entity, `title ${target} times 20 60 20`);
	await entity.runCommandAsync(`title ${target} subtitle ${msg}`);
	if (addEmptyTitle === true)
		await entity.runCommandAsync(`title ${target} title `);
}
export async function actionbar(entity, target, msg) {
	await setMessageCooldown(DEFAULT_MESSAGE_COOLDOWN);
	await runPlayerCommand(entity, `title ${target} times 20 60 20`);
	return await entity.runCommandAsync(`titleraw ${target} actionbar {"rawtext":[{"text":"§r${msg}"}]}`);
}
export async function titleFull(entity, target, titleMsg, subtitleMsg, actionbarMsg) {
	if (subtitleMsg === undefined)
		subtitleMsg = "§r";
	if (titleMsg === undefined)
		titleMsg = "§r";
	
	await setMessageCooldown(DEFAULT_MESSAGE_COOLDOWN);
	await runPlayerCommand(entity, `title ${target} times 20 60 20`);
	if (actionbarMsg !== undefined)
		await entity.runCommandAsync(`title ${target} actionbar ${actionbarMsg}`);
	await entity.runCommandAsync(`title ${target} title ${titleMsg}`);
	await entity.runCommandAsync(`title ${target} subtitle ${subtitleMsg}`);
}

export async function playSound(entity, target, playSound, volume, pitch, minVolume) {
	let soundVolume = volume ? volume : 100;
	let soundPitch = pitch ? pitch : 1;
	let soundMinVolume = minVolume ? minVolume : 1;
	return await entity.runCommandAsync(`playsound ${playSound} ${target} ~ ~ ~ ${soundVolume} ${soundPitch} ${soundMinVolume}`);
}

export function concat2LinesWithPad(line1, line2)
{
	let len1 = line1.length;
	let len2 = line2.length;
	let message = "";
	if (len1 > len2) 
		message = line1 + "\n" + line2.padStart((len2+((len1-len2)/2)), " ");
	else if (len2 > len1)
		message = line1.padStart((len1+((len2-len1)/2)), " ") + "\n" + line2;
	else 
		message = line1 + "\n" + line2;
	return message;
}
export function debug(message) {
	return world.sendMessage(message);
}

export function typeIdToEntityName(typeId) {
    let name = typeId.split(":")[1]; // Remove prefix:
    name = name.replace(/_/g, ' '); // Replace '_' with ' '
    name = name.split(' ')
        .map(word => word.charAt(0).toUpperCase() + word.slice(1))
        .join(' '); // Replace first letters with uppercase

    return name;
}

export async function setWorldParam(name, value)
{
	world.setDynamicProperty(name, value);
}
export async function loadWorldParam(name, defaultValue)
{
	let param = world.getDynamicProperty(name);
	if (param === undefined)
	{
		param = defaultValue;
		world.setDynamicProperty(name, param);
	}
	return param
}
export async function loadEntityParam(entity, name, defaultValue)
{
	let param = await entity.getDynamicProperty(name);
	if (param === undefined)
	{
		param = defaultValue;
		await entity.setDynamicProperty(name, param);
	}
	return param
}
export async function advanceWorldParam(name)
{
	let param = await world.getDynamicProperty(name);
	param = (param === undefined) ? 1 : param+1;
	await world.setDynamicProperty(name, param);
	return param
}
export async function advanceEntityParam(entity, name)
{
	let param = await entity.getDynamicProperty(name);
	param = (param === undefined) ? 1 : param+1;
	await entity.setDynamicProperty(name, param);
	return param
}

export async function verifyAir(x1, y1, z1, x2, y2, z2)
{
	for (let x=x1; x<=x2; x++)
	{
		for (let y=y1; y<=y2; y++)
		{
			for (let z=z1; z<=z2; z++)
			{
				let block = await overworld.getBlock({x: x, y: y, z: z});
				if (!isBlockAir(block))
					return false;
			}
		}
	}
	return true;
}
export async function verifyAir2(corner1, corner2)
{
	for (let x=corner1.x; x<=corner2.x; x++)
	{
		for (let y=corner1.y; y<=corner2.y; y++)
		{
			for (let z=corner1.z; z<=corner2.z; z++)
			{
				let block = await overworld.getBlock({x: x, y: y, z: z});
				if (!isBlockAir(block))
					return false;
			}
		}
	}
	return true;
}


const ignoreBlocks = [ 'air', 'light_block', 'white_carpet', 'orange_carpet', 'magenta_carpet',
		'light_blue_carpet', 'yellow_carpet', 'lime_carpet', 'pink_carpet', 'gray_carpet', 'light_gray_carpet',
		'cyan_carpet', 'purple_carpet', 'blue_carpet', 'brown_carpet','green_carpet', 'red_carpet', 'black_carpet',
		'grass', 'short_grass', 'tall_grass', 'blue_orchid', 'allium', 'azure_bluet', 'red_tulip', 'orange_tulip', 'white_tulip',
		'pink_tulip', 'oxeye_daisy', 'cornflower', 'lily_of_the_valley', 'wither_rose', 'fern', 'large_fern',"snow", "snow_layer" ];

export function isBlockAir(block)
{
	if (block === null || block === undefined || block.isAir == undefined || block.isAir) 
		return true;
	if (ignoreBlocks.includes(block.typeId.replace("minecraft:","")))
		return true;
	//if (ignoreBlocks.find((ignoreBlock) => { var _a; return (_a = block) === null || _a === void 0 ? void 0 : _a.permutation.matches(ignoreBlock.block, ignoreBlock.properties); }))
	//	return true;
	return false;
}

export function mapRange(value, inMin, inMax, outMin, outMax) {
    return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}

export function convertToRadians(angle) {
	return angle * Math.PI / 180;
}

export function getCurrentDayTime() {
    let timeInTicks = world.getTimeOfDay();
    if(timeInTicks >= 0 && timeInTicks < 3500) { return "Day";}
    else if (timeInTicks >= 3500 && timeInTicks < 9000) {return "Noon";}
    else if (timeInTicks >= 9000 && timeInTicks < 12500) {return "Sunset";}
    else if (timeInTicks >= 12500 && timeInTicks < 18000) {return "Night";}
    else if (timeInTicks >= 18000 && timeInTicks <= 24000) {return "Sunrise";}
}

export function getTimeIn24hFormat(){
	let timeInTicks = world.getTimeOfDay();
	let hours = Math.round(timeInTicks / 1000 + 6);
	if(hours > 24) 
	  hours = hours - 24;
	let minutes = Math.round((timeInTicks % 1000) / 16.6);
	let timeIn24h = "";
	if(hours >= 0 && hours < 10){
	  if(minutes >= 0 && minutes < 10 ){
		timeIn24h = `0${hours}:${minutes}0`;
	  } else {
		timeIn24h = `0${hours}:${minutes}`;
	  }
	} else {
	  if(minutes >= 0 && minutes < 10 ){
		timeIn24h = `${hours}:${minutes}0`;
	  } else {
		timeIn24h = `${hours}:${minutes}`;
	  }
	}
	return timeIn24h;
}

export function getTimeIn12hFormat() {
    let timeInTicks = world.getTimeOfDay();
    let hours = Math.round(timeInTicks / 1000 + 6);
    if (hours > 24) 
        hours = hours - 24;
    let minutes = Math.round((timeInTicks % 1000) / 16.6);

    let period = "AM";
    if (hours >= 12) {
        period = "PM";
        if (hours > 12) {
            hours = hours - 12;
        }
    } else if (hours === 0) {
        hours = 12;
    }

    let formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
    let timeIn12h = `${hours}:${formattedMinutes} ${period}`;
    
    return timeIn12h;
}

export function toSnakeCase(input) {
    return input
        .replace(/([a-z])([A-Z])/g, '$1_$2')  // Convert camelCase to snake_case
        .replace(/\s+/g, '_')                 // Replace all spaces with underscores
        .replace(/[^\w_]/g, '')               // Remove all non-word characters except underscores
        .toLowerCase();                       // Convert the string to lowercase
}

export function toTitleCase(str) {
    return str
        .toLowerCase()                                              // Convert the entire string to lowercase
        .replace(/_/g, ' ')                                         // Replace underscores with spaces
        .split(' ')                                                 // Split the string into an array of words
        .map(word => word.charAt(0).toUpperCase() + word.slice(1))  // Capitalize the first letter of each word
        .join(' ');                                                 // Join the words back into a single string with spaces
}

export function toRoman(number) {
	switch (number) {
		case 1: return "I";
		case 2: return "II";
		case 3: return "III";
		case 4: return "IV";
		case 5: return "V";
		default: return "";
	}
}

export function getProgressBar(percentage, len, color) {
	const fullBlockCount = Math.floor(percentage * len);

	let progressBar = color;
	for (let i = 0; i < fullBlockCount; i++) {
		progressBar += '█'; // Full Block
	}

	progressBar += "§8";

	for (let i = 0; i < len - fullBlockCount; i++) {
		progressBar += '█'; // Full block for the rest
	}

	return progressBar += "＀";
}

export function lerp (start, end, lerp) {
    return start + (end - start) * lerp;
};

export function directionToRotation(direction) {
	// Yaw: how much to rotate around Y so that the forward axis (+Z) aligns with (x, z)
	// math.atan2 returns radians in [-π, π], which translates to degrees in [-180, 180].
	//
	// atan2(X, Z) => yaw=0 along +Z, yaw=+90 along +X, etc.
	const yaw = (Math.atan2(direction.z, direction.x) * (180 / Math.PI) + 360 - 90) % 360;
  
	// Pitch: angle up/down. We measure the vertical angle from horizontal by looking at y vs. the magnitude in the XZ plane.
	// We put a negative sign if we want "y>0 => pitch<0" (i.e., 'up' is negative pitch).
	const pitch = -Math.atan2(direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * (180 / Math.PI);
  
	return vector3.copy({ x: pitch, y: yaw, z: 0 });
}

export function findNearestPlayer(entity) {
    const players = world.getDimension("overworld").getPlayers({ location: entity.location, closest: 1 });
    return players.length > 0 ? players[0] : null;
}