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

////////////////////////
////////////////////////
//// version 1.1.0////
////////////////////////
////////////////////////

// ¯\_(ツ)_/¯

export class Vector3 {
    x
    y
    z
    constructor(x, y, z) {
        if (y === undefined && z === undefined) {
            if (typeof x === "object") {
                this.x = x.x
                this.y = x.y
                this.z = x.z
            } else if (typeof x === "string") {
                const parts = x.replace(/\s+/g, ' ') // Replace multiple spaces with a single space
                .split(/[\s,]+/) // Split by spaces or commas
                if (parts.length !== 3) throw new Error(`invalid vector input: ${JSON.stringify(x)}`)
                const [newX, newY, newZ] = parts.map(Number)
                this.x = newX
                this.y = newY
                this.z = newZ
            }
        } else {
            this.x = x
            this.y = y
            this.z = z
        }
    }
    toString() {
        return Vector3.toString(this)
    }
    dot(vect2) {
        return Vector3.dot(this, vect2)
    }
    cheapDist(vect2) {
        return Vector3.cheapDist(this, vect2)
    }
    distance(vect2) {
        return Vector3.distance(this, vect2)
    }
    isWithinRadius(vect2, radius) {
        return Vector3.isWithinRadius(this, vect2, radius)
    }
    above(dist) {
        return Vector3.above(this, dist)
    }
    floor() {
        return Vector3.floor(this)
    }
    center() {
        return Vector3.center(this)
    }
    add(vect2) {
        return Vector3.add(this, vect2)
    }
    bottomCenter() {
        return Vector3.bottomCenter(this)
    }
    equals(vect2) {
        return Vector3.equals(this, vect2)
    }
    multiply(multiple) {
        return Vector3.multiply(this, multiple)
    }
    round(places) {
        return Vector3.round(this, places)
    }
    subtract(vect2, excludeY) {
        return Vector3.subtract(this, vect2, excludeY)
    }
    magnitude() {
        return Vector3.magnitude(this)
    }
    horizontalAngle() {
        return Vector3.horizontalAngle(this)
    }
    wiggle(range, includeY) {
        return Vector3.wiggle(this, range, includeY)
    }


    static toString(vect) {
        return `${vect.x} ${vect.y} ${vect.z}`
    }
    static dot(vect1, vect2) {
        return (vect1.x * vect2.x) + (vect1.y * vect2.y) + (vect1.z + vect2.z)
    }
    /**
     * returns distance squared, avoiding costly square root calculations
     */
    static cheapDist(vect1, vect2) {
        const dx = vect1.x - vect2.x
        const dy = vect1.y - vect2.y
        const dz = vect1.z - vect2.z
        return (dx * dx + dy * dy + dz * dz)
    }
    /**
    * returns actual distance
    */
    static distance(vect1, vect2) {
        return Math.sqrt(Vector3.cheapDist(vect1,vect2))
    }
    static isWithinRadius(vect1, vect2, radius) {
        return Vector3.cheapDist(vect1, vect2) <= radius * radius
    }
    /**
     * Note that {dist} can be negative, if desired.
     * @param {Vector} vector 
     * @param {number} dist 
     * @returns 
     */
    static above(vect, dist=1) {
        return {x:vect.x, y:vect.y + dist, z:vect.z}
    }
    static floor(vect) {
        return {
            x: Math.floor(vect.x),
            y: Math.floor(vect.y),
            z: Math.floor(vect.z)
        }
    }
    static center(vect) {
        return Vector3.add(Vector3.floor(vect), new Vector3(0.5, 0.5, 0.5))
    }
    static add(vect1, vect2) {
        if (vect1 === undefined || vect2 === undefined) throw new Error (`vector undefined, cannot add! ${JSON.stringify(vect1)}, ${JSON.stringify(vect2)}`)
        return {
            x:vect1.x + vect2.x,
            y:vect1.y + vect2.y,
            z:vect1.z + vect2.z
        }
    }
    static bottomCenter(vect) {
        return Vector3.add(Vector3.floor(vect), new Vector3(0.5, 0, 0.5))
    }
    static equals(vect1, vect2) {
        if (!vect1 || !vect2) return false
        return (vect1.x === vect2.x && vect1.y === vect2.y && vect1.z === vect2.z)
    }
    static multiply(vect, multiple) {
        return new Vector3(vect.x * multiple, vect.y * multiple, vect.z * multiple)
    }
    static round(vect, places) {
        const operand = places * 10
        return {
            x:Math.round(vect.x * operand)/operand,
            y:Math.round(vect.y * operand)/operand,
            z:Math.round(vect.z * operand)/operand
        }
    }
    static subtract(vect1, vect2, excludeY = false) {
        if (vect1 === undefined || vect2 === undefined) throw new Error (`vector undefined, cannot subtract! ${JSON.stringify(vect1)}, ${JSON.stringify(vect2)}, ${excludeY}`)
        return {
            x:vect1.x - vect2.x,
            y:excludeY ? 0 : vect1.y - vect2.y,
            z:vect1.z - vect2.z
        }
    }
    static magnitude(vect) {
        if (vect === undefined) throw new Error (`vector undefined, cannot get magnitude! ${JSON.stringify(vect)}`)
        return Math.sqrt((vect.x * vect.x) + (vect.y * vect.y) + (vect.z * vect.z))
    }
    static horizontalAngle(vect) {
        return -toDegrees(Math.atan2(vect.x, vect.z))
    }
    static wiggle(vect, range, includeY = false) {
        const newVect = new Vector3(vect)
        newVect.x += (Math.random() - 0.5) * range
        newVect.z += (Math.random() - 0.5) * range
        if (includeY && vect.y !== undefined) newVect.y += (Math.random() - 0.5) * range
        return newVect
    }
}

export function sendDebugMessage(message) {
	if (message instanceof Error) message = `§4${message.name}:§r\n${message.stack}${message.message}`
	world.getPlayers({tags: ["lifeboat:debug"]}).forEach(op => op.sendMessage(`§b[CC] §dDebug§r: ${message}`))
}

function hasTimePassed(start, length) {
    if (system.currentTick - start >= length) return true
    return false
}

/**
 * when used with the await keyword, pauses execution for <ticks> number of game ticks.
 * @param {int} ticks 
 * @returns 
 */
export async function pause(ticks = 1, debugReason) {
    if (ticks <= 0) return true
    if (ticks >= 600) sendDebugMessage(`${ticks} ticks is a long time!  That's ${ticks / 20} seconds - is this intentional??`)
    sendDebugMessage(`Pausing for ${ticks} tick${ticks > 1 ? "s" : ""}${debugReason ? `, reason: ${debugReason}` : "..."}`)
    const start = system.currentTick
    return new Promise(resolve => {
        const waiting = () => {
            const result = hasTimePassed(start,ticks)
            if (result) {
                resolve(true)
            } else system.run(waiting)
        }
        waiting()
    })
}

export function sendAddonMessage(recipients, sender = "text.lb_as.Add-OnNameTag", ...translationArgs) {
    if (Array.isArray(sender)) {
        translationArgs = [sender]
        sender = "text.lb_as.Add-OnNameTag"
    }
    const rawText = {
        rawtext: [
            { text: "[" },
            { translate: sender },
            { text: "§r]: " }
        ]
    };

    translationArgs.forEach(([translationKey, textArgs]) => {
        rawText.rawtext.push({ translate: "text.lb_as." + translationKey });
        if (textArgs && textArgs.length > 0) {
            textArgs = textArgs.map(String)
            rawText.rawtext[rawText.rawtext.length - 1].with = {
                rawtext: textArgs.map(arg => arg.startsWith("text.lb_as.") ? { translate: arg } : { text: arg })
            };
        }
        rawText.rawtext.push({ text: " " }); // Add a space after each translation
    });

    // Remove the last space if no translation keys were provided
    if (translationArgs.length > 0) {
        rawText.rawtext.pop(); // Remove the last space added
    }

    if (recipients instanceof Array) {
        recipients.forEach(player => player.sendMessage(rawText));
    } else {
        recipients.sendMessage(rawText);
    }
}

export function isInsideHeightLimits(height,dimension) {
    if (height >= dimension.heightRange.min && height <= dimension.heightRange.max) return true
    return false
}

export function isMovingTowards(entity1, entity2) {
    const velocity1 = entity1.getVelocity()
    const velocity2 = entity2.getVelocity()

    const relativeX = velocity2.x - velocity1.x
    const relativeZ = velocity2.z - velocity1.z

    const deltaX = entity2.location.x - entity1.location.x;
    const deltaZ = entity2.location.z - entity1.location.z

    return (relativeX * deltaX + relativeZ * deltaZ) < 0
}

export function queryBlock(location,dimension) {
    try {
        if (isInsideHeightLimits(location.y,dimension)) {
            return dimension.getBlock(location)
        } else return
    } catch (error) {
        sendDebugMessage(error)
    }
}

export function toDegrees(radians) {
    return radians * (180/Math.PI)
}

export function toRadians(degrees) {
    return degrees * (Math.PI/180)
}

export function directionToVect(directionString) {
    switch(directionString.toLowerCase()) {
        case "north":
            return new Vector3(0, 0, -1)
        case "east":
            return new Vector3(1, 0, 0)
        case "south":
            return new Vector3(0, 0, 1)
        case "west":
            return new Vector3(-1, 0, 0)
        case "northeast":
            return new Vector3(1, 0, -1)
        case "southeast":
            return new Vector3(1, 0, 1)
        case "southwest":
            return new Vector3(-1, 0, 1)
        case "northwest":
            return new Vector3(-1, 0, -1)
        case "up":
            return new Vector3(0, 1, 0)
        case "down":
            return new Vector3(0, -1, 0)
        default:
            sendDebugMessage(`§4unknown direction ${directionString}`)
            return undefined
    }
}

export const pistonDirectTranslate = [
    "down",
    "up",
    "south",
    "north",
    "east",
    "west"
]

export function reverseDirection(direction) {
    const index = pistonDirectTranslate.indexOf(direction.toLowerCase())
    if (index < 0) throw new Error(`${direction} has no index`)
    return pistonDirectTranslate[index % 2 === 0 ? index + 1 : index - 1]
}

export function perpendiculars(direction) {
    switch(direction.toLowerCase()) {
        case "north":
        case "south":
            return ["east", "west"]
        case "east":
        case "west":
            return ["north", "south"]
        default:
            sendDebugMessage(`§4unable to find perpendiculars for "${direction}"`)
            return undefined
    }
}

export function getCardinalDirection(vector) {
    const angle = Vector3.horizontalAngle(vector)
    if (Math.abs(angle) <= 45) return "south"
    if (Math.abs(angle) <= 135) {
        if (angle < 0) return "east"
        else return "west"
    } else return "north"
}

export function cardinalToDegrees(string) {
	return directionToVect(string).horizontalAngle()
}

export function blockFaceToDirection(face) {
    face = face.toLowerCase()
    switch(face) {
        case "up":
            return "above"
        case "down":
            return "below"
        default:
            return face
    }
}

export const formatColors = {
    lightGreen: "a",
    lightBlue: "b",
    lightRed: "c",
    hotPink: "d",
    lightYellow: "e",
    white: "f",
    darkYellow: "g",
    dust: "h",
    lightGray: "i",
    darkGray: "j",
    burgundy: "m",
    brown: "n",
    mustard: "p",
    darkGreen: "q",
    aqua: "s",
    dustyBlue: "t",
    purple: "u",
    darkBlue: "1",
    brightGreen: "2",
    teal: "3",
    darkRed: "4",
    hotPurple: "5",
    orange: "6",
    lightSilver: "7",
    darkSilver: "8",
    bluePurple: "9",
    black: "0"
}

export const formatCodes = {
    obfuscate: "k",
    bold: "l",
    italic: "o",
    revert: "r",
}

for (const color in formatColors) {
    formatColors[color] = "§" + formatColors[color]
}
for (const code in formatCodes) {
    formatCodes[code] = "§" + formatCodes[code]
}

function getAllFormats() {
    const result = {}
    Object.keys(formatColors).forEach(color => result[color] = formatColors[color])
    Object.keys(formatCodes).forEach(code => result[code] = formatCodes[code])
    return result
}

export const allFormats = getAllFormats()

export const normalColorValues = ["white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", "light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black"]

export function getRandomIntegerInclusive(min, max) {
    min = Math.ceil(min)
    max = Math.floor(max)
    return Math.floor(Math.random() * (max - min + 1)) + min
}

export function getDamagedItem(player, item, damage) {
    const durabilityComponent = item.getComponent(ItemComponentTypes.Durability)
    if (durabilityComponent.damage + damage > durabilityComponent.maxDurability) { //break!
        player.playSound("random.break")
        return undefined
    } else { //decrement
        durabilityComponent.damage += damage
        return item
    }
}

export function durability(item) {
    function durabilityComponent() {
        return item.getComponent(ItemComponentTypes.Durability)
    }

    function getDamage() {
        const damage = durabilityComponent()?.damage
        if (!damage) return 0
        return damage
    }

    function unbreaking() {
        const enchantComponent = item.getComponent(ItemComponentTypes.Enchantable)
        if (!enchantComponent) return 1
        const unbreakingLevel = (enchantComponent.hasEnchantment("unbreaking") ? enchantComponent.getEnchantment("unbreaking").level : 0),
        damageChance = 100/(unbreakingLevel + 1)/100
        return damageChance
    }

    function set(value) {
        if (value < 0) {
            sendDebugMessage(`cannot set damage negative! given value: ${value}, setting to 0...`)
            value = 0
        }
        const component = durabilityComponent()
        if (value > component.maxDurability) {
            sendDebugMessage(`${item.typeId} should break!!!`)
            return undefined
        }
        component.damage = value;
        return item
    }

    return {
        getRemaining: function() {
            const data = durabilityComponent()
            return (data.maxDurability - data.damage)
        },
        getDamage: function() {
            return getDamage()
        },
        damageChance: unbreaking(),
        durabilityComponent: function() {
            return durabilityComponent()
        },
        set: function(value) {
            return set(value)
        },
        // determine if item will be damaged
        damageEvent: function() {
            return Math.random() < unbreaking()
        },
        // returns the new damage value, or undefined if the tool has broken
        damageItem: function(player, damageAmount = 1, forceDamage = false) {
            if (player.getGameMode() === GameMode.Creative) sendDebugMessage(`§4Hey!§r ${player.nameTag} is in creative, but their ${item.typeId} is being damaged!`)
            const component = durabilityComponent()
            if (!component) {
                sendDebugMessage(`§c${item.typeId} does not have a durability component!`)
                return undefined
            }
            const newDamage = component.damage + (forceDamage || this.damageEvent() ? damageAmount : 0)
            if (newDamage > component.maxDurability) {
                // break!
                player.playSound("random.break")
                return undefined
            } else {
                if (newDamage > component?.damage) sendDebugMessage(`dealing ${damageAmount} damage to ${player.nameTag}'s ${item.typeId}, now ${newDamage}`)
                return set(newDamage)
            }
        }
    }
}