/*
This is an alpha version of the tank development by Arthur. Functions and structure
is not final, some areas can definitely be improved but we focused on making the
movement functional for now. I wrote all the functions and code in one file but I 
categorized each one in case you want to separate them in the future.
*/

// ============================================================================
// Core - Core tank system and movement logic
// ============================================================================

import { system, world } from "@minecraft/server";

const is1 = {
    collision: {
        width: 4
    },
    movement: {
        climbSpeed: 10.25,
        horizontalSpeed: 1.25,
        acceleration: 0.031
    },
    rotations: {
        hull: 0.95,
        turret: 3.3,
        mainGun: {
            rate: 3.6,
            up: -20,
            down: 3
        }
    }
};

const tanksList = {
    "wt:is_1": is1,
};

const tankQuery = { families: ["tank"] };

system.runInterval(() => {
    const entities = world.getDimension("overworld").getEntities(tankQuery);
    for (const tank of entities) {
        const rider = getRiderByType(tank, "minecraft:player");
        const controlInput = getPlayerInput(rider);
        const data = {
            pilot: rider,
            controlInput: controlInput
        };

        setMovement(tank, data);
        setBodyRotation(tank, data);
        setTurretRotation(tank, data);
    }
}, 0);

function getVehicleStats(tank) {
    const id = tank.typeId;
    if (tanksList[id]) return tanksList[id];
    console.warn(`[VehicleStats] Unknown vehicle type: ${id}`);
    return null;
}

function setMovement(tank, data) {
    const vehicleStats = getVehicleStats(tank);
    if (!vehicleStats) return;

    const currentSpeed = tank.getProperty("tank:speed");
    playMoveSound(tank, currentSpeed);

    if (!data.controlInput) {
        decelerate(tank);
        if (currentSpeed === 0) return;
    }

    const gravityMultiplier = 2;
    const airResistance = 0.25;

    let viewDirection = tank.getViewDirection();
    viewDirection.y = 0;
    viewDirection = normalizeVector(viewDirection);

    let direction, scale;
    const climbHeight = getHighestBlockInFront(tank, data);
    let verticalSpeed = getVerticalSpeed(tank, climbHeight, vehicleStats);
    let horizontalSpeed;

    if (data.controlInput) {
        const movementData = setMovementDirectionAndScale(data);
        direction = movementData.direction;
        scale = movementData.scale;
        horizontalSpeed = getHorizontalSpeed(tank, data, verticalSpeed, vehicleStats) * scale;
    } else {
        direction = Math.sign(currentSpeed);
        scale = 1;
        let ratio = 1;

        const maxVerticalSpeed = vehicleStats.movement.climbSpeed;
        if (maxVerticalSpeed > 0) {
            ratio = Math.min(1 - (verticalSpeed / maxVerticalSpeed), 1);
        }
        horizontalSpeed = currentSpeed * ratio;
    }

    verticalSpeed = verticalSpeed < 0 ? verticalSpeed * gravityMultiplier : verticalSpeed;
    horizontalSpeed = tank.isOnGround ? horizontalSpeed : horizontalSpeed * airResistance;

    const velocity = {
        x: viewDirection.x * horizontalSpeed,
        z: viewDirection.z * horizontalSpeed
    };

    tank.applyKnockback(velocity, verticalSpeed);
}

function playMoveSound(tank, speed) {
    const sound = "tank.move";
    const volume = clampNumber(Math.abs(speed), 0.4, 1);
    tank.dimension.playSound(sound, tank.location, { volume });
}

function decelerate(tank) {
    const rate = 0.04;
    const currentSpeed = tank.getProperty("tank:speed");

    if (currentSpeed > 0) {
        tank.setProperty("tank:speed", Math.max(currentSpeed - rate, 0));
    } else if (currentSpeed < 0) {
        tank.setProperty("tank:speed", Math.min(currentSpeed + rate, 0));
    }
}

function getHorizontalSpeed(tank, data, verticalSpeed, vehicleStats) {
    const moveDirection = data.movement ? data.movement.direction : Math.sign(getTankMovementDirection(tank));
    const speedPercentage = getCurrentSpeedPercentage(tank, moveDirection, vehicleStats);

    const maxVerticalSpeed = vehicleStats.movement.climbSpeed;
    const maxHorizontalSpeed = vehicleStats.movement.horizontalSpeed;

    let ratio = 1;
    if (maxVerticalSpeed > 0) {
        ratio = Math.min(1 - (verticalSpeed / maxVerticalSpeed), 1);
    }

    return maxHorizontalSpeed * Math.max(ratio, 0.45) * speedPercentage;
}

function getCurrentSpeedPercentage(tank, moveDirection, vehicleStats) {
    const acceleration = vehicleStats.movement.acceleration;
    const currentSpeed = tank.getProperty("tank:speed");
    const moveAmount = acceleration * moveDirection;
    const updated = clampNumber(currentSpeed + moveAmount, -1, 1);
    tank.setProperty("tank:speed", updated);
    return updated;
}

function getVerticalSpeed(tank, terrainHeight, vehicleStats) {
    const maxSpeed = vehicleStats.movement.climbSpeed;
    const climbAmount = terrainHeight > 0 ? terrainHeight + 0.2 : terrainHeight;
    return (climbAmount < 2) ? clampNumber(climbAmount, -maxSpeed, maxSpeed) : 0;
}

function setMovementDirectionAndScale(data) {
    const input = data.controlInput;
    const movementVector = input.movementVector;
    let result = (movementVector.x * 0) + (movementVector.y * 1);
    result += 0.05;
    result = Math.min(result, 1);

    const movement = {
        directionAndScale: result,
        direction: Math.sign(result),
        scale: Math.abs(result)
    };

    data.movement = movement;
    return movement;
}

function setBodyRotation(tank, data) {
    const controlInput = data.controlInput;
    if (!controlInput) return;

    const stats = getVehicleStats(tank);
    if (!stats) return;

    const maxRate = stats.rotations.hull;
    const vec = controlInput.movementVector;
    const rotationScale = Math.abs((vec.x * 1) + (vec.y * 0));
    const angleInput = controlInput.angle;

    const rotation = tank.getRotation();
    const rotationAmount = clampNumber(angleInput, -maxRate, maxRate) * rotationScale;

    rotation.y += rotationAmount;
    data.bodyRotation = rotation;
    data.bodyRotationAmount = rotationAmount;
    tank.setRotation(rotation);
}

function setTurretRotation(tank, data) {
    const stats = getVehicleStats(tank);
    if (!stats) return;

    const rotationSpeed = stats.rotations.turret;
    const mainGunRotationCap = {
        up: -30,
        down: 15
    };

    const player = data.pilot;
    if (!player) return;

    const targetRotation = player.getRotation();
    let rotationX = tank.getProperty("tank:turret_rotation_x");
    let rotationY = tank.getProperty("tank:turret_rotation_y");

    rotationX = moveAngleToward(rotationX, targetRotation.x, rotationSpeed);
    rotationX = clampNumber(rotationX, mainGunRotationCap.up, mainGunRotationCap.down);

    const horizontalClamp = stats.rotations.mainGun.horizontalClamp;
    if (horizontalClamp) {
        const bodyRotation = tank.getRotation();
        const clampedSpeed = horizontalClamp - Math.min(Math.abs(targetRotation.y - bodyRotation.y), horizontalClamp);
        rotationY = moveAngleToward(rotationY, targetRotation.y, Math.min(rotationSpeed, clampedSpeed));
    } else {
        rotationY = moveAngleToward(rotationY, targetRotation.y, rotationSpeed);
    }

    rotationY = addAngles(rotationY, data.bodyRotationAmount || 0);
    tank.setProperty("tank:turret_rotation_x", rotationX);
    tank.setProperty("tank:turret_rotation_y", rotationY);

    data.turret = {
        rotation: {
            x: rotationX,
            y: rotationY
        }
    };
}

function getTankMovementDirection(tank) {
    const viewDirection = tank.getViewDirection();
    const velocity = tank.getVelocity();
    return (viewDirection.x * velocity.x) + (viewDirection.z * velocity.z);
}

// ============================================================================
// Input - Player input handling
// ============================================================================

function getPlayerInput(player) {
    if (!player) return null;

    const inputInfo = player.inputInfo;
    const controlInputs = {};
    let movementInput = inputInfo.getMovementVector();
    const scale = Math.sqrt(movementInput.x ** 2 + movementInput.y ** 2);

    if (!scale) return null;

    movementInput = normalizeVector(movementInput);
    controlInputs.movementVector = movementInput;
    controlInputs.movementScale = scale;

    let angle = vectorToAngle(movementInput.x, movementInput.y);
    angle = addAngles(angle, -90);
    controlInputs.angle = angle;

    return controlInputs;
}

function getRiderByType(entity, typeId) {
    const riders = entity.getComponent("rideable").getRiders();
    for (const rider of riders) {
        if (rider.typeId === typeId) {
            return rider;
        }
    }
}

// ============================================================================
// Terrain - Terrain scanning and collision detection
// ============================================================================

function getHighestBlockInFront(tank, data, maxHeight = 1.5) {
    const direction = Math.sign(tank.getProperty("tank:speed"));
    const collisionRadius = tanksList[tank.typeId]?.collision?.width;
    const location = tank.location;
    location.y += 2;

    let viewDirection = tank.getViewDirection();
    viewDirection.y = 0;
    viewDirection = normalizeVector(viewDirection);
    viewDirection.x *= direction;
    viewDirection.z *= direction;

    const angleRotateAmount = getAngleRotateAmount(1, collisionRadius);
    const scanPerimeter = 120;
    const loopCount = scanPerimeter / angleRotateAmount;
    let highestPoint = 0;
    const scannerDistance = collisionRadius - 1.01;
    const startVector = rotateVectorXZ(viewDirection, (scanPerimeter / -2) + 2);

    for (let i = 0; i < loopCount; i++) {
        if (i == 0) {
            const scanLocation = {
                x: location.x + startVector.x * scannerDistance,
                y: location.y,
                z: location.z + startVector.z * scannerDistance
            };
            highestPoint = getTerrainHeight(tank, scanLocation);
            continue;
        }

        const rotateAmount = angleRotateAmount * i;
        const vector = rotateVectorXZ(startVector, rotateAmount);
        const scanLocation = {
            x: location.x + vector.x * scannerDistance,
            y: location.y,
            z: location.z + vector.z * scannerDistance
        };
        const currentHeight = getTerrainHeight(tank, scanLocation);
        highestPoint = Math.max(currentHeight, highestPoint);
    }

    return highestPoint;
}

function getTerrainHeight(tank, scanLocation) {
    const block = getNearestBlockDown(tank, scanLocation).block;
    const blockHeight = getBlockHeight(block);
    const globalHeight = block.location.y + blockHeight;
    const localHeight = globalHeight - tank.location.y;
    return localHeight;
}

function getNearestBlockDown(entity, location) {
    const maxDistance = 5;
    let block;

    for (let i = 0; i <= maxDistance; i++) {
        const newLoc = { x: location.x, y: location.y - i, z: location.z };
        block = entity.dimension.getBlock(newLoc);

        if (isValidBlock(block) || i == maxDistance) return { block: block };
    }
}

function isValidBlock(block) {
    if (!block) return false;
    if (block.isAir || block.isLiquid) return false;

    if (nonSolidBlocks.has(block.typeId)) {
        system.runTimeout(() => { destroyBlock(block); }, 1);
        return false;
    }

    return true;
}

function destroyBlock(block) {
    const { x, y, z } = block;
    block.dimension.runCommand(`fill ${x} ${y} ${z} ${x} ${y} ${z} air destroy`);
}

const defaultBlockHeight = 1;

function getAngleRotateAmount(width, radius) {
    if (width > 2 * radius) {
        return null;
    }

    const angleRadians = 2 * Math.asin(width / (2 * radius));
    const angleDegrees = angleRadians * (180 / Math.PI);
    return angleDegrees;
}

function getBlockHeight(block) {
    if (!block) return 0;

    const typeId = block.typeId;
    const splitIds = typeId.split("_");
    const splitId = splitIds[splitIds.length - 1];

    if (splitId == "slab") {
        return 0.5;
    } else if (splitId == "carpet") {
        return 0;
    } else if (splitId == "trapdoor") {
        return 0;
    } else if (unjumpableBlocks.has(block.typeId)) {
        return 1.5;
    }

    return defaultBlockHeight;
}

const nonSolidBlocks = new Set([
    "minecraft:short_grass",
    "minecraft:tall_grass",
    "minecraft:short_dry_grass",
    "minecraft:tall_dry_grass",
    "minecraft:fern",
    "minecraft:large_fern",
    "minecraft:vine",
    "minecraft:deadbush",
    "minecraft:flower",
    "minecraft:dandelion",
    "minecraft:poppy",
    "minecraft:blue_orchid",
    "minecraft:allium",
    "minecraft:azure_bluet",
    "minecraft:red_tulip",
    "minecraft:orange_tulip",
    "minecraft:white_tulip",
    "minecraft:pink_tulip",
    "minecraft:oxeye_daisy",
    "minecraft:cornflower",
    "minecraft:lily_of_the_valley",
    "minecraft:wither_rose",
    "minecraft:sunflower",
    "minecraft:lilac",
    "minecraft:rose_bush",
    "minecraft:peony",
    "minecraft:torchflower",
    "minecraft:pitcher_plant",
    "minecraft:sapling",
    "minecraft:oak_sapling",
    "minecraft:spruce_sapling",
    "minecraft:birch_sapling",
    "minecraft:jungle_sapling",
    "minecraft:acacia_sapling",
    "minecraft:dark_oak_sapling",
    "minecraft:mangrove_propagule",
    "minecraft:cherry_sapling",
    "minecraft:wheat",
    "minecraft:beetroots",
    "minecraft:carrots",
    "minecraft:potatoes",
    "minecraft:sweet_berry_bush",
    "minecraft:cactus",
    "minecraft:sugar_cane",
    "minecraft:bamboo",
    "minecraft:bamboo_sapling",
    "minecraft:kelp",
    "minecraft:seagrass",
    "minecraft:tall_seagrass",
    "minecraft:torch",
    "minecraft:campfire",
    "minecraft:flowering_azalea",
    "minecraft:azalea",
    "minecraft:moss_carpet",
    "minecraft:moss_block",
    "minecraft:small_dripleaf",
    "minecraft:big_dripleaf",
    "minecraft:spore_blossom",
    "minecraft:coral",
    "minecraft:coral_fan",
    "minecraft:coral_fan_dead",
    "minecraft:sea_pickle",
    "minecraft:fire",
    "minecraft:soul_fire",
    "minecraft:snow",
    "minecraft:snow_layer",
    "minecraft:powder_snow",
    "minecraft:stone_button",
    "minecraft:oak_button",
    "minecraft:spruce_button",
    "minecraft:birch_button",
    "minecraft:jungle_button",
    "minecraft:acacia_button",
    "minecraft:dark_oak_button",
    "minecraft:mangrove_button",
    "minecraft:cherry_button",
    "minecraft:bamboo_button",
    "minecraft:lever",
    "minecraft:tripwire",
    "minecraft:tripwire_hook",
    "minecraft:oak_pressure_plate",
    "minecraft:spruce_pressure_plate",
    "minecraft:birch_pressure_plate",
    "minecraft:jungle_pressure_plate",
    "minecraft:acacia_pressure_plate",
    "minecraft:dark_oak_pressure_plate",
    "minecraft:mangrove_pressure_plate",
    "minecraft:cherry_pressure_plate",
    "minecraft:bamboo_pressure_plate",
    "minecraft:stone_pressure_plate",
    "minecraft:light_weighted_pressure_plate",
    "minecraft:heavy_weighted_pressure_plate",
    "minecraft:oak_trapdoor",
    "minecraft:spruce_trapdoor",
    "minecraft:birch_trapdoor",
    "minecraft:jungle_trapdoor",
    "minecraft:acacia_trapdoor",
    "minecraft:dark_oak_trapdoor",
    "minecraft:mangrove_trapdoor",
    "minecraft:cherry_trapdoor",
    "minecraft:bamboo_trapdoor",
    "minecraft:iron_trapdoor",
    "minecraft:oak_door",
    "minecraft:spruce_door",
    "minecraft:birch_door",
    "minecraft:jungle_door",
    "minecraft:acacia_door",
    "minecraft:dark_oak_door",
    "minecraft:mangrove_door",
    "minecraft:cherry_door",
    "minecraft:bamboo_door",
    "minecraft:iron_door",
    "minecraft:glass",
    "minecraft:tinted_glass",
    "minecraft:white_stained_glass",
    "minecraft:orange_stained_glass",
    "minecraft:magenta_stained_glass",
    "minecraft:light_blue_stained_glass",
    "minecraft:yellow_stained_glass",
    "minecraft:lime_stained_glass",
    "minecraft:pink_stained_glass",
    "minecraft:gray_stained_glass",
    "minecraft:light_gray_stained_glass",
    "minecraft:cyan_stained_glass",
    "minecraft:purple_stained_glass",
    "minecraft:blue_stained_glass",
    "minecraft:brown_stained_glass",
    "minecraft:green_stained_glass",
    "minecraft:red_stained_glass",
    "minecraft:black_stained_glass",
    "minecraft:glass_pane",
    "minecraft:white_stained_glass_pane",
    "minecraft:orange_stained_glass_pane",
    "minecraft:magenta_stained_glass_pane",
    "minecraft:light_blue_stained_glass_pane",
    "minecraft:yellow_stained_glass_pane",
    "minecraft:lime_stained_glass_pane",
    "minecraft:pink_stained_glass_pane",
    "minecraft:gray_stained_glass_pane",
    "minecraft:light_gray_stained_glass_pane",
    "minecraft:cyan_stained_glass_pane",
    "minecraft:purple_stained_glass_pane",
    "minecraft:blue_stained_glass_pane",
    "minecraft:brown_stained_glass_pane",
    "minecraft:green_stained_glass_pane",
    "minecraft:red_stained_glass_pane",
    "minecraft:black_stained_glass_pane",
    "minecraft:candle",
    "minecraft:white_candle",
    "minecraft:orange_candle",
    "minecraft:magenta_candle",
    "minecraft:light_blue_candle",
    "minecraft:yellow_candle",
    "minecraft:lime_candle",
    "minecraft:pink_candle",
    "minecraft:gray_candle",
    "minecraft:light_gray_candle",
    "minecraft:cyan_candle",
    "minecraft:purple_candle",
    "minecraft:blue_candle",
    "minecraft:brown_candle",
    "minecraft:green_candle",
    "minecraft:red_candle",
    "minecraft:black_candle",
    "minecraft:campfire",
    "minecraft:soul_campfire",
    "minecraft:torch",
    "minecraft:soul_torch",
    "minecraft:lantern",
    "minecraft:soul_lantern",
    "minecraft:mangrove_roots",
    "minecraft:hanging_roots",
    "minecraft:rooted_dirt",
    "minecraft:cherry_leaves",
    "minecraft:flowering_azalea_leaves",
    "minecraft:azalea_leaves",
    "minecraft:oak_leaves",
    "minecraft:birch_leaves",
    "minecraft:spruce_leaves",
    "minecraft:jungle_leaves",
    "minecraft:acacia_leaves",
    "minecraft:dark_oak_leaves",
    "minecraft:mangrove_leaves",
    "minecraft:pink_petals",
    "minecraft:wildflowers"
]);

const unjumpableBlocks = new Set([
    "minecraft:oak_fence",
    "minecraft:spruce_fence",
    "minecraft:birch_fence",
    "minecraft:jungle_fence",
    "minecraft:acacia_fence",
    "minecraft:dark_oak_fence",
    "minecraft:crimson_fence",
    "minecraft:warped_fence",
    "minecraft:mangrove_fence",
    "minecraft:bamboo_fence",
    "minecraft:nether_brick_fence",
    "minecraft:cobblestone_wall",
    "minecraft:mossy_cobblestone_wall",
    "minecraft:stone_brick_wall",
    "minecraft:mossy_stone_brick_wall",
    "minecraft:granite_wall",
    "minecraft:diorite_wall",
    "minecraft:andesite_wall",
    "minecraft:sandstone_wall",
    "minecraft:red_sandstone_wall",
    "minecraft:brick_wall",
    "minecraft:prismarine_wall",
    "minecraft:nether_brick_wall",
    "minecraft:red_nether_brick_wall",
    "minecraft:end_stone_brick_wall",
    "minecraft:blackstone_wall",
    "minecraft:polished_blackstone_wall",
    "minecraft:polished_blackstone_brick_wall",
    "minecraft:deepslate_wall",
    "minecraft:polished_deepslate_wall",
    "minecraft:cobbled_deepslate_wall",
    "minecraft:tuff_wall",
    "minecraft:calcite_wall"
]);

// ============================================================================
// Math - Mathematical utility functions
// ============================================================================

function moveAngleToward(current, target, delta) {
    let difference = (target - current) % 360;
    if (difference > 180) {
        difference -= 360;
    } else if (difference < -180) {
        difference += 360;
    }

    if (Math.abs(difference) <= delta) {
        current = target;
    } else {
        current += Math.sign(difference) * delta;
    }

    current = (current + 180) % 360;
    if (current < 0) {
        current += 360;
    }
    current -= 180;

    return current;
}

function addAngles(angle1, angle2) {
    angle1 = ((angle1 + 180) % 360) - 180;
    angle2 = ((angle2 + 180) % 360) - 180;
    let diff = angle2 + angle1;
    if (diff > 180) {
        diff -= 360;
    } else if (diff < -180) {
        diff += 360;
    }
    return diff;
}

function normalizeVector(vector) {
    const normalized = {};
    let magnitude = 0;

    for (const p in vector) {
        magnitude += vector[p] * vector[p];
    }

    magnitude = Math.sqrt(magnitude);

    if (magnitude === 0) {
        for (const p in vector) {
            normalized[p] = 0;
        }
    } else {
        for (const p in vector) {
            normalized[p] = vector[p] / magnitude;
        }
    }

    return normalized;
}

function rotateVectorXZ(vector, angleDegrees) {
    const angleRadians = angleDegrees * (Math.PI / 180);
    const cosAngle = Math.cos(angleRadians);
    const sinAngle = Math.sin(angleRadians);

    const { x, y, z } = vector;
    const rotatedX = x * cosAngle - z * sinAngle;
    const rotatedZ = x * sinAngle + z * cosAngle;

    const roundedX = Math.round(rotatedX * 1e10) / 1e10;
    const roundedZ = Math.round(rotatedZ * 1e10) / 1e10;

    return {
        x: roundedX,
        y: y,
        z: roundedZ
    };
}

function clampNumber(value, min, max) {
    return Math.min(Math.max(value, min), max);
}

function vectorToAngle(x, y) {
    const radians = Math.atan2(y, x);
    let degrees = radians * (180 / Math.PI);

    if (degrees > 180) {
        degrees -= 360;
    }
    return degrees;
}