import { system, world, Player, EntityDamageCause, Entity, MolangVariableMap } from "@minecraft/server";
import * as consts from "./constants.js";

export class Vector3OP {
    multiply(a, b) {
        return {
            x: (a.x ?? 0) * (b.x ?? 0),
            y: (a.y ?? 0) * (b.y ?? 0),
            z: (a.z ?? 0) * (b.z ?? 0),
        };
    }
    add(a, b) {
        return {
            x: (a.x ?? 0) + (b.x ?? 0),
            y: (a.y ?? 0) + (b.y ?? 0),
            z: (a.z ?? 0) + (b.z ?? 0),
        };
    }
    sub(a, b) {
        return {
            x: (a.x ?? 0) - (b.x ?? 0),
            y: (a.y ?? 0) - (b.y ?? 0),
            z: (a.z ?? 0) - (b.z ?? 0),
        };
    }
    distance(a, b) {
        const dx = (a.x ?? 0) - (b.x ?? 0);
        const dy = (a.y ?? 0) - (b.y ?? 0);
        const dz = (a.z ?? 0) - (b.z ?? 0);
        return Math.sqrt(dx * dx + dy * dy + dz * dz);
    }
}
export const VOP = new Vector3OP();

/**
 * Applies knockback to the given player in the direction of their movement.
 * @param {Player} player - The player to apply knockback to.
 * @param {number} hStrength - The horizontal strength of the knockback. Default is 2.
 * @param {number} vStrength - The vertical strength of the knockback. Default is 0.2.
 */
export function addDirectionalKnockback(player, hStrength = 2, vStrength = 0.2) {

    const movementVector = player.inputInfo.getMovementVector();

    // Get player's rotation as a Vector2 (yaw and pitch)
    const rotation = player.getRotation(); // Vector2 with { x: pitch, y: yaw }
    const yaw = (rotation.y + 90) * (Math.PI / 180); // Convert yaw to radians (+90 to align with Minecraft axes)

    // Calculate the global knockback direction using movementVector and yaw
    const knockbackX = movementVector.y * Math.cos(yaw) + movementVector.x * Math.sin(yaw);
    const knockbackZ = movementVector.y * Math.sin(yaw) - movementVector.x * Math.cos(yaw);

    // // Scale horizontal and vertical strengths based on pitch
    // const pitch = rotation.x * (Math.PI / 180); // Convert pitch to radians
    // const hStrength = strength * Math.cos(pitch); // Horizontal strength reduces as pitch approaches ±90° (straight up/down)
    // const vStrength = strength * (-Math.sin(pitch)); // Vertical strength increases as pitch approaches ±90°

    // Apply the knockback using the transformed global coordinates
    player.applyKnockback(
        knockbackX,
        knockbackZ,
        hStrength,
        vStrength
    );

    // // Debugging
    // world.sendMessage(`Global Knockback Vector: (${knockbackX.toFixed(2)}, ${knockbackZ.toFixed(2)})`);
    // world.sendMessage(`Vector: (${movementVector.x}, ${movementVector.y})`);
}
export function addBackwardsKnockback(player, hStrength = 2, vStrength = 0.2) {

    // Get player's rotation as a Vector2 (yaw and pitch)
    const rotation = player.getRotation(); // Vector2 with { x: pitch, y: yaw }
    const yaw = rotation.y * (Math.PI / 180); // Convert yaw to radians

    // Calculate the knockback direction, but in the opposite direction
    const knockbackX = Math.sin(yaw) * hStrength;
    const knockbackZ = -Math.cos(yaw) * hStrength;

    // Apply the knockback using the transformed global coordinates
    player.applyKnockback(
        knockbackX,
        knockbackZ,
        hStrength,
        vStrength
    );
}
/**
 * Calculates a unit vector pointing away from the origin toward the target.
 *
 * @param {Object} target - The entity or point to push.
 * @param {Object} origin - The location pushing the target away.
 * @param {boolean} [ignoreY=false] - Whether to ignore vertical difference.
 * @returns {Object} A normalized direction vector to apply as force.
 */
export function getPointAwayDirection(target, origin, ignoreY = false) {
    const delta = VOP.sub(target, origin); // direction from origin to target (push away)

    if (ignoreY) {
        delta.y = 0;
    }

    const length = Math.sqrt(delta.x ** 2 + delta.y ** 2 + delta.z ** 2);
    if (length === 0) return { x: 0, y: 0, z: 0 }; // no direction if positions are the same

    return {
        x: delta.x / length,
        y: delta.y / length,
        z: delta.z / length,
    };
}


/**
 * Returns valid entities in range, executed by the given entity while excluding certain types.
 * @param {Entity} executerEntity - The entity that executes the scan.
 * @param {Object} [options] - Optional parameters.
 * @param {Object} [options.startLocation] - The location to start the scan from, defaults to 'executerEntity.location'.
 * @param {number} [options.maxDistance=1] - The maximum distance to scan.
 * @param {number} [options.minDistance] - The minimum distance to scan.
 * @param {boolean} [options.includeSelf] - Whether to include the executerEntity in the scan. Defaults to FALSE.
 * @param {boolean} [options.includeTamed] - Whether to include tamed entities in the scan. Defaults to FALSE.
 * @param {string[]} [options.additionalExcludedTypes] - Additional entity types to exclude.
 * @param {string[]} [options.additionalExcludedFamilies] - Additional entity families to exclude.
 * @param {Entity[]} [options.excludedEntities] - Additional entities to exclude.
 * @param {boolean} [options.excludePetsOfExcludedEntities] - Whether to exclude pets of excluded entities.
 * @param {boolean} [options.includeUndeadMinions=false] - Include undead minions.
 * @param {boolean} [options.includeTsunami=false] - Include tsunami entity.
 * @returns {Entity[]} - Array of entities found.
 */
export function getValidTargetsInRange(executerEntity, options = {}) {
    try {
        if (!executerEntity.isValid()) return [];

        const {
            startLocation = executerEntity.location,
            maxDistance = 1,
            minDistance = 0,
            includeSelf = false,
            includeTamed = false,
            additionalExcludedTypes = [],
            additionalExcludedFamilies = [],
            excludedEntities = [],
            excludePetsOfExcludedEntities = true,
            includeUndeadMinions = false,
            includeTsunami = false
        } = options;

        // Combine excluded types and families
        const allExcludedTypes = [...consts.CMN_EXCLUDE_TYPES, ...additionalExcludedTypes];
        const allExcludedFamilies = [...consts.CMN_EXCLUDE_FAMILIES, ...additionalExcludedFamilies];

        // Create a set of excluded entities
        const excludedEntitySet = new Set(excludedEntities);
        if (!includeSelf) excludedEntitySet.add(executerEntity); // Exclude self if not allowed

        // PvP setting
        const pvpSetting =
            world.getDynamicProperty("oct_eg:pvpDamageScallingPercent") >= 1 &&
            world.gameRules.pvp === true;

        return executerEntity.dimension.getEntities({
            location: startLocation,
            maxDistance: maxDistance,
            minDistance: minDistance,
            excludeTypes: allExcludedTypes,
            excludeFamilies: allExcludedFamilies

        }).filter(e => {
            if (!e?.isValid()) return false;

            const tameable = e.getComponent("minecraft:tameable");
            const ownerProp = e.getDynamicProperty("oct_eg:owner_id");
            const owner = tameable ? tameable?.tamedToPlayer
                : ownerProp !== undefined
                    ? world.getEntity(ownerProp) : null;

            // Skip if invalid or explicitly excluded
            if (isExcluded(e, excludedEntitySet)) return false;
            // Exclude players or their pets when PvP is off
            if (isBlockedByPvP(e, owner, pvpSetting, includeUndeadMinions, includeTsunami)) return false;
            // Exclude executer's own pets if not allowed
            if (isOwnTamedPet(e, owner, executerEntity, includeTamed, includeUndeadMinions, includeTsunami)) return false;
            // Exclude pets of other excluded entities
            if (isPetOfExcludedOwner(owner, excludedEntitySet, excludePetsOfExcludedEntities, executerEntity)) return false;

            return true; // Valid target
        });

    } catch (e) {
        console.log(`Error in getValidTargetsInRange: ${e}`);
        return [];
    }
}

/**
 * Returns valid entities hit by multiple raycasts, with filters applied.
 * @param {Entity} executerEntity - The entity performing the raycasts.
 * @param {{ location: Object, direction: Object }[]} rays - Array of ray origin and direction pairs.
 * @param {Object} [options] - Optional filter and raycast parameters.
 * @param {number} [options.maxDistance=1] - Max distance for each ray.
 * @param {boolean} [options.includeSelf=false] - Whether to include the executerEntity.
 * @param {boolean} [options.includeTamed=false] - Whether to include executer's own tamed pets.
 * @param {string[]} [options.additionalExcludedTypes=[]] - Extra types to exclude.
 * @param {string[]} [options.additionalExcludedFamilies=[]] - Extra families to exclude.
 * @param {Entity[]} [options.excludedEntities=[]] - Entities to exclude.
 * @param {boolean} [options.excludePetsOfExcludedEntities=true] - Exclude pets of excluded entities.
 * @param {boolean} [options.includeLiquidBlocks=false] - Whether rays pass through liquids.
 * @param {boolean} [options.includePassableBlocks=false] - Whether rays pass through passable blocks.
 * @param {boolean} [options.includeUndeadMinions=false] - Include undead minions.
 * @param {boolean} [options.includeTsunami=false] - Include tsunami entity.
 * @returns {Entity[]} - Unique set of valid target entities.
 */
export function getValidTargetsFromRays(executerEntity, rays, options = {}) {
    try {
        if (!executerEntity?.isValid()) return [];

        const {
            maxDistance = 1,
            includeSelf = false,
            includeTamed = false,
            additionalExcludedTypes = [],
            additionalExcludedFamilies = [],
            excludedEntities = [],
            excludePetsOfExcludedEntities = true,
            includeLiquidBlocks = false,
            includePassableBlocks = false,
            includeUndeadMinions = false,
            includeTsunami = false
        } = options;

        // Combine excluded types and families
        const allExcludedTypes = [...consts.CMN_EXCLUDE_TYPES, ...additionalExcludedTypes];
        const allExcludedFamilies = [...consts.CMN_EXCLUDE_FAMILIES, ...additionalExcludedFamilies];

        // Create a set of excluded entities
        const excludedEntitySet = new Set(excludedEntities);
        if (!includeSelf) excludedEntitySet.add(executerEntity); // Exclude self if not allowed

        // PvP setting
        const pvpSetting =
            world.getDynamicProperty("oct_eg:pvpDamageScallingPercent") >= 1 &&
            world.gameRules.pvp === true;

        const hitEntities = new Set();

        for (const { location, direction } of rays) {
            const hits = executerEntity.dimension.getEntitiesFromRay(location, direction, {
                maxDistance: maxDistance,
                excludeTypes: allExcludedTypes,
                excludeFamilies: allExcludedFamilies,
                includeLiquidBlocks: includeLiquidBlocks,
                includePassableBlocks: includePassableBlocks
            });

            for (const hit of hits) {
                const e = hit.entity;
                if (!e?.isValid()) continue;

                const tameable = e.getComponent("minecraft:tameable");
                const ownerProp = e.getDynamicProperty("oct_eg:owner_id");
                const owner = tameable ? tameable?.tamedToPlayer
                    : ownerProp !== undefined
                        ? world.getEntity(ownerProp) : null;


                // Skip if invalid or explicitly excluded
                if (isExcluded(e, excludedEntitySet)) continue;
                // Exclude players or their pets when PvP is off
                if (isBlockedByPvP(e, owner, pvpSetting, includeUndeadMinions, includeTsunami)) continue;
                // Exclude executer's own pets if not allowed
                if (isOwnTamedPet(e, owner, executerEntity, includeTamed, includeUndeadMinions, includeTsunami)) continue;
                // Exclude pets of other excluded entities
                if (isPetOfExcludedOwner(owner, excludedEntitySet, excludePetsOfExcludedEntities, executerEntity)) continue;

                hitEntities.add(e); // Valid target
            }
        }

        //world.sendMessage(`Hit ${hitEntities.size} entities`);
        return Array.from(hitEntities);

    } catch (e) {
        //console.warn(`Error in getValidTargetsFromRays: ${e}`);
        return [];
    }
}

function isExcluded(entity, excludedEntitySet) {
    return excludedEntitySet.has(entity);
}
function isBlockedByPvP(entity, owner, pvpEnabled, includeUndeadMinions = false, includeTsunami = false) {
    // Special exception: allow undead minions if includeUndeadMinions is true
    if (entity.typeId === "oct_eg:undead_minion" && includeUndeadMinions) return false;

    // Special exception: allow tsunami if includeTsunami is true
    if (entity.typeId === "oct_eg:tsunami" && includeTsunami) return false;

    return !pvpEnabled && (entity instanceof Player || owner instanceof Player);
}
function isOwnTamedPet(entity, owner, executerEntity, includeTamed, includeUndeadMinions = false, includeTsunami = false) {
    // Only consider pets owned by the executer
    if (owner !== executerEntity) return false;

    // Special exception: allow undead minions if includeUndeadMinions is true
    if (entity.typeId === "oct_eg:undead_minion" && includeUndeadMinions) return false;

    // Special exception: allow tsunami if includeTsunami is true
    if (entity.typeId === "oct_eg:tsunami" && includeTsunami) return false;

    // Otherwise, exclude own pets if includeTamed is false
    return !includeTamed;
}
function isPetOfExcludedOwner(owner, excludedEntitySet, excludePets, executerEntity) {
    return (
        excludePets &&
        owner &&
        owner !== executerEntity && // Prevent excluding own pets again
        excludedEntitySet.has(owner)
    );
}


/**
 * Applies the given damage to the victim, modified for pvp player settings.
 *
 * @param {Entity} victim - The entity to apply the damage to.
 * @param {number} damage - The amount of damage to apply.
 * @param {Object} [options] - Optional parameters.
 * @param {EntityDamageCause} [options.cause] - The cause of the damage. Defaults to 'EntityDamageCause.entityAttack'.
 * @param {Entity} [options.damagingEntity] - The entity responsible for the damage.
 */
export function applyModifiedDamage(victim, damage, options = {}) {
    if (!victim.isValid()) return;
    const {
        damagingEntity = undefined,
        cause = EntityDamageCause.entityAttack
    } = options;

    try {
        const pvpDamageScaling = world.getDynamicProperty("oct_eg:pvpDamageScallingPercent") ?? 100;
        const modifiedDamage = victim instanceof Player
            ? damage * (pvpDamageScaling / 100)
            : damage;

        victim?.applyDamage(modifiedDamage,
            {
                cause: cause,
                damagingEntity: damagingEntity
            });

    } catch (e) { }
}


/**
 * Checks if an entity is a valid pull target.
 * @param {Entity} entity - The entity to check.
 * @returns {boolean} Whether the entity is a valid pull target.
 * A valid pull target is either a player in a non-creative, non-spectator gamemode, or an entity with a valid navigation component.
 */
export function isValidPullTarget(entity) {
    if (!entity?.isValid()) return false;

    // check for valid players
    if (entity instanceof Player) {
        const gamemode = entity.getGameMode();
        if (gamemode === "spectator" || gamemode === "creative") return false;
        return true;
    }

    for (const navType of consts.VALID_NAVIGATION_COMPONENTS) {
        if (entity.getComponent(navType)) return true;
    }

    return false;
}