//Made originally by phantompig, but updated by FSang18
const MotionPacket = Java.loadClass("net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket")

let FTBChunksAPI = null
let Protection = null

try {
    FTBChunksAPI = Java.loadClass('dev.ftb.mods.ftbchunks.api.FTBChunksAPI')
    Protection = Java.loadClass('dev.ftb.mods.ftbchunks.api.Protection')
} catch (e) {
    FTBChunksAPI = null
    Protection = null
}

const IMMOVABLE_BLOCKS = [
    '#minecraft:portals',
    '#minecraft:shulker_boxes',
    '#forge:chests',
    'minecraft:bedrock',
    'minecraft:barrier',
    'minecraft:end_portal',
    'minecraft:end_gateway',
    'minecraft:end_portal_frame',
    'minecraft:nether_portal',
    'minecraft:command_block',
    'minecraft:repeating_command_block',
    'minecraft:chain_command_block',
    'minecraft:structure_block',
    'minecraft:jigsaw',
    'minecraft:chest',
    'minecraft:trapped_chest',
    'minecraft:ender_chest',
    'minecraft:barrel',
    'minecraft:spawner'
]

const UNREPLACEABLE_BLOCKS = [
    '#minecraft:portals',
    '#minecraft:shulker_boxes',
    '#forge:chests',
    'minecraft:bedrock',
    'minecraft:barrier',
    'minecraft:end_portal',
    'minecraft:end_gateway',
    'minecraft:end_portal_frame',
    'minecraft:nether_portal',
    'minecraft:command_block',
    'minecraft:repeating_command_block',
    'minecraft:chain_command_block',
    'minecraft:structure_block',
    'minecraft:jigsaw',
    'minecraft:spawner',
    'minecraft:reinforced_deepslate',
    'minecraft:chest',
    'minecraft:trapped_chest',
    'minecraft:ender_chest',
    'minecraft:barrel',
    'minecraft:beacon',
    'minecraft:enchanting_table',
    'minecraft:respawn_anchor',
    'minecraft:lodestone',
    'minecraft:dragon_egg',
    'minecraft:conduit'
]

const MOVABLE_BLOCKS = [
    'minecraft:stone',
    'minecraft:cobblestone',
    'minecraft:dirt',
    'minecraft:dirt_path',
    'minecraft:grass_block',
    'minecraft:sand',
    'minecraft:red_sand',
    'minecraft:gravel',
    'minecraft:smooth_stone',
    'minecraft:end_stone',
    'minecraft:netherrack',
    'minecraft:deepslate',
    'minecraft:blackstone',
    'minecraft:sandstone',
    'minecraft:terracotta',
    'minecraft:andesite',
    'minecraft:basalt',
    'minecraft:coal_block',
    'minecraft:raw_copper_block',
    'minecraft:raw_iron_block',
    'minecraft:raw_gold_block',
    'minecraft:amethyst_block',
    'minecraft:magma_block',
    'minecraft:mossy_cobblestone',
    'minecraft:mud',
    'minecraft:mycelium',
    'minecraft:packed_mud',
    'minecraft:podzol',
    'minecraft:red_sandstone',
    'minecraft:redstone_ore',
    'minecraft:tuff',
    'minecraft:warped_nylium',
    'palladium:lead_ore',
    'palladium:deepslate_lead_ore',
    'palladium:titanium_ore',
    'palladium:vibranium_ore',
    'palladium:redstone_flux_crystal_geode',
    'palladium:deepslate_redstone_flux_crystal_geode'
]

function isShulkerBox(id) {
    return id.endsWith('_shulker_box') || id === 'minecraft:shulker_box'
}

function blockMatches(block, list) {
    for (let entry of list) {
        if (entry.startsWith('#')) {
            let tag = entry.substring(1)
            if (block.hasTag(tag)) return true
        } else {
            if (block.id == entry) return true
        }
    }
    return false
}

function isMovableBlock(id) {
    return MOVABLE_BLOCKS.indexOf(id) !== -1
}

function isImmovable(block) {
    return isShulkerBox(block.id) || blockMatches(block, IMMOVABLE_BLOCKS)
}

function isUnreplaceable(block) {
    return isShulkerBox(block.id) || blockMatches(block, UNREPLACEABLE_BLOCKS)
}

function toBlockPos(obj) {
    try {
        if (!obj) return null
        if (obj.pos) return obj.pos
        if (typeof obj.x !== 'undefined' && typeof obj.y !== 'undefined' && typeof obj.z !== 'undefined') {
            return BlockPos.containing(obj.x, obj.y, obj.z)
        }
        if (obj.blockPosition) return obj.blockPosition()
    } catch (_) { }
    return null
}

function isFTBClaimProtected(player, pos) {
    if (!FTBChunksAPI || !Protection || !player || !pos) return false

    try {
        let api = FTBChunksAPI.api()
        if (!api) return false

        if (api.getProtectionManager) {
            let manager = api.getProtectionManager()
            if (!manager) return false

            let result = manager.checkBlockAction(
                player,
                player.level,
                pos,
                Protection.BLOCK_EDIT
            )

            return result && !result.isAllowed()
        }

        if (api.getManager) {
            let manager = api.getManager()
            if (!manager) return false

            if (manager.shouldPreventInteraction) {
                return manager.shouldPreventInteraction(
                    player,
                    null,
                    pos,
                    Protection.EDIT_BLOCK,
                    null
                )
            }
        }
    } catch (e) {
    }

    return false
}

function isProtectedBlock(player, blockOrPos) {
    let pos = toBlockPos(blockOrPos)
    return isFTBClaimProtected(player, pos)
}

function isProtectedEntityTarget(player, entity) {
    if (!entity) return false

    try {
        return isFTBClaimProtected(player, entity.blockPosition())
    } catch (_) {
    }

    return false
}

function canTelekinesisInteract(player, block) {
    if (!block) return false
    if (isImmovable(block)) return false
    if (isProtectedBlock(player, block)) return false
    return true
}

function canTelekinesisPlace(player, block) {
    if (!block) return false
    if (isUnreplaceable(block)) return false
    if (isProtectedBlock(player, block)) return false
    return true
}

const NINJAGO_HELD = {
    ZERO: '00000000-0000-0000-0000-000000000000',
    key(mode) { return 'ninjago.' + mode + '.held_uuid' },
    getHeldId(player, mode) {
        let v = ''
        try { v = String(player.persistentData.getString(this.key(mode)) || '') } catch (_) { }
        if (!v || v.length < 36) {
            player.persistentData.putString(this.key(mode), this.ZERO)
            return this.ZERO
        }
        return v
    },
    setHeldId(player, mode, uuid) {
        player.persistentData.putString(this.key(mode), (uuid && String(uuid).length >= 36) ? String(uuid) : this.ZERO)
    },
    find(level, id) {
        if (!id || id === this.ZERO) return null
        let list = level.getEntities()
        for (let i = 0; i < list.size(); i++) {
            try {
                if (String(list.get(i).uuid) === id) return list.get(i)
            } catch (_) { }
        }
        return null
    },
    get(player, mode) {
        let id = this.getHeldId(player, mode)
        if (id === this.ZERO) return null
        return this.find(player.level, id)
    },
    set(player, mode, e) {
        if (!e) {
            this.setHeldId(player, mode, this.ZERO)
            return
        }
        this.setHeldId(player, mode, String(e.uuid))
    }
}

function spawnBlockDisplay(block, player) {
    if (!block) return null
    if (!canTelekinesisInteract(player, block)) return null

    let e = block.level.createEntity('minecraft:block_display')
    e.setPosition(block)
    e.mergeNbt({
        block_state: {
            Name: block.id,
            Properties: block.properties
        }
    })
    e.mergeNbt({
        ninjago_origin: {
            x: block.pos.x,
            y: block.pos.y,
            z: block.pos.z
        }
    })

    block.set('air')
    e.spawn()
    return e
}

function safePlaceBlockDisplay(entity, player) {
    if (!entity || entity.type !== 'minecraft:block_display') return

    let target = entity.block

    if (!target) {
        entity.discard()
        return
    }

    let steps = 0
    while (steps < 5 && !canTelekinesisPlace(player, target)) {
        target = target.up
        steps++
    }

    if (!target) {
        entity.discard()
        return
    }

    if (isProtectedBlock(player, target)) {
        entity.discard()
        return
    }
    if (target.id !== 'minecraft:air') {
        if (isUnreplaceable(target)) {
            entity.discard()
            return
        }

        try {
            target.level.destroyBlock(target.pos, true)
        } catch (_) {
            target.set('air')
        }
    }

    let props = {}
    if (entity.nbt.block_state && entity.nbt.block_state.Properties) {
        for (let [k, v] of Object.entries(entity.nbt.block_state.Properties)) {
            props[k] = v
        }
    }

    target.set(entity.nbt.block_state.Name, props)
    entity.discard()
}

function getRangeFromScore(entity, entry) {
    let obj = entry.getPropertyByName('range_objective')
    let r = 0
    try { r = palladium.scoreboard.getScore(entity, obj, 0) } catch (_) { }
    if (r <= 0) r = 1
    return r
}

function getRangeFromProperty(entity, entry) {
    let key = entry.getPropertyByName('range_property')
    if (!key) return 1
    let val = 0
    try { val = palladium.getProperty(entity, key) } catch (_) { }
    let n = Number(val)
    if (!isFinite(n) || n <= 0) return 1
    return n
}

function getDirectTargetPos(player, range) {
    let eye = player.getEyePosition()
    let look = player.getLookAngle().normalize()
    let target = eye.add(look.scale(range))
    let pos = BlockPos.containing(target.x(), target.y(), target.z())
    if (isFTBClaimProtected(player, pos)) return null
    return target
}

function getSafeTargetPos(player, range) {
    let direct = getDirectTargetPos(player, range)
    if (!direct) return null

    let ray = null
    try {
        ray = player.rayTrace(range, true)
    } catch (_) {
        try {
            ray = player.rayTrace(range)
        } catch (_) {
            ray = null
        }
    }

    if (ray && ray.block && ray.hit) {
        let pos = BlockPos.containing(ray.hit.x(), ray.hit.y(), ray.hit.z())
        if (isFTBClaimProtected(player, pos)) return null
        return ray.hit
    }

    return direct
}

function moveHeldEntity(player, held, strength, range) {
    if (!held) return
    if (isProtectedEntityTarget(player, held)) return

    let target = getDirectTargetPos(player, range)
    if (!target) return

    let anchor = held.getBoundingBox ? held.getBoundingBox().getCenter() : held.position()
    let dx = target.x() - anchor.x()
    let dy = target.y() - anchor.y()
    let dz = target.z() - anchor.z()
    let vel = Vec3d(dx, dy, dz).scale(strength)

    held.setDeltaMovement(vel)
    held.hurtMarked = true
    held.resetFallDistance()

    if (held.isPlayer()) held.connection.send(new MotionPacket(held))
}

StartupEvents.registry('palladium:abilities', event => {
    event.create('ninjago:telekinesis')
        .icon(palladium.createItemIcon('minecraft:player_head'))
        .documentationDescription('Telekinesis')
        .addProperty('range_objective', 'string', 'Ninjago.Range', 'Range scoreboard')
        .addProperty('strength', 'float', 0.8, 'Strength')
        .addProperty('damage', 'float', 2, 'Damage')

        .firstTick((player, entry, holder, enabled) => {
            if (!enabled || !player || !player.isPlayer()) return

            let range = getRangeFromScore(player, entry)
            let ray = player.rayTrace(range, false)

            if (ray.entity) {
                if (isProtectedEntityTarget(player, ray.entity)) return
                NINJAGO_HELD.set(player, 'telekinesis', ray.entity)
            } else if (ray.block) {
                if (!canTelekinesisInteract(player, ray.block)) return
                let spawned = spawnBlockDisplay(ray.block, player)
                if (spawned) NINJAGO_HELD.set(player, 'telekinesis', spawned)
            }
        })

        .tick((player, entry, holder, enabled) => {
            if (!enabled || !player || !player.isPlayer()) return

            let held = NINJAGO_HELD.get(player, 'telekinesis')
            if (!held) return

            let range = getRangeFromScore(player, entry)

            if (held.type === 'minecraft:block_display') {
                let target = getSafeTargetPos(player, range)
                if (!target) return
                held.setPosition(target.x() - 0.5, target.y() - 0.5, target.z() - 0.5)
            } else {
                moveHeldEntity(player, held, entry.getPropertyByName('strength'), range)
            }
        })

        .lastTick((player) => {
            let held = NINJAGO_HELD.get(player, 'telekinesis')
            if (!held) return
            if (held.type === 'minecraft:block_display') safePlaceBlockDisplay(held, player)
            NINJAGO_HELD.set(player, 'telekinesis', null)
        })
})

StartupEvents.registry('palladium:abilities', event => {
    event.create('ninjago:telekinesis_property_range')
        .icon(palladium.createItemIcon('minecraft:player_head'))
        .documentationDescription('Telekinesis Property Range')
        .addProperty('range_property', 'string', 'ninjago_telekinesis_range', 'Range property')
        .addProperty('strength', 'float', 0.8, 'Strength')
        .addProperty('damage', 'float', 2, 'Damage')
        .addProperty('particle', 'string', 'minecraft:enchant', 'Particle')
        .addProperty('particle_x', 'float', 1, 'Particle X')
        .addProperty('particle_y', 'float', 1, 'Particle Y')
        .addProperty('particle_z', 'float', 1, 'Particle Z')

        .firstTick((player, entry, holder, enabled) => {
            if (!enabled || !player || !player.isPlayer()) return

            let range = getRangeFromProperty(player, entry)
            let ray = player.rayTrace(range, false)

            if (ray.entity) {
                if (isProtectedEntityTarget(player, ray.entity)) return
                NINJAGO_HELD.set(player, 'telekinesis_property_range', ray.entity)
            } else if (ray.block) {
                if (!canTelekinesisInteract(player, ray.block)) return
                let spawned = spawnBlockDisplay(ray.block, player)
                if (spawned) NINJAGO_HELD.set(player, 'telekinesis_property_range', spawned)
            }
        })

        .tick((player, entry, holder, enabled) => {
            if (!enabled || !player || !player.isPlayer()) return

            let held = NINJAGO_HELD.get(player, 'telekinesis_property_range')
            if (!held) return

            let range = getRangeFromProperty(player, entry)

            if (held.type === 'minecraft:block_display') {
                let target = getSafeTargetPos(player, range)
                if (!target) return
                held.setPosition(target.x() - 0.5, target.y() - 0.5, target.z() - 0.5)
            } else {
                moveHeldEntity(player, held, entry.getPropertyByName('strength'), range)
            }
        })

        .lastTick((player) => {
            let held = NINJAGO_HELD.get(player, 'telekinesis_property_range')
            if (!held) return
            if (held.type === 'minecraft:block_display') safePlaceBlockDisplay(held, player)
            NINJAGO_HELD.set(player, 'telekinesis_property_range', null)
        })
})

StartupEvents.registry('palladium:abilities', event => {
    event.create('ninjago:geokinesis')
        .icon(palladium.createItemIcon('minecraft:lodestone'))
        .documentationDescription('Move whitelisted blocks')
        .addProperty('range_objective', 'string', 'ninjago.Range', 'Range scoreboard')
        .addProperty('strength', 'float', 0.8, 'Strength')
        .addProperty('damage', 'float', 2, 'Damage')

        .firstTick((player, entry, holder, enabled) => {
            if (!enabled || !player || !player.isPlayer()) return

            let range = getRangeFromScore(player, entry)
            let ray = player.rayTrace(range, false)

            if (!ray.block) return
            if (isFTBClaimProtected(player, ray.block.pos)) return

            if (!isMovableBlock(ray.block.id)) return
            if (!canTelekinesisInteract(player, ray.block)) return

            let spawned = spawnBlockDisplay(ray.block, player)
            if (spawned) NINJAGO_HELD.set(player, 'geokinesis', spawned)
        })

        .tick((player, entry, holder, enabled) => {
            if (!enabled || !player || !player.isPlayer()) return

            let held = NINJAGO_HELD.get(player, 'geokinesis')
            if (!held) return

            let range = getRangeFromScore(player, entry)
            let ray = player.rayTrace(range)
            if (ray && ray.hit) {
                let pos = BlockPos.containing(ray.hit.x(), ray.hit.y(), ray.hit.z())
                if (isFTBClaimProtected(player, pos)) return
            }

            let target = getSafeTargetPos(player, range)
            if (!target) return

            if (held.type === 'minecraft:block_display') {
                held.setPosition(target.x() - 0.5, target.y() - 0.5, target.z() - 0.5)
            }
        })

        .lastTick((player) => {
            let held = NINJAGO_HELD.get(player, 'geokinesis')
            if (!held) return

            if (held.type === 'minecraft:block_display') {
                let block = held.block
                if (block && isFTBClaimProtected(player, block.pos)) {
                    held.discard()
                    NINJAGO_HELD.set(player, 'geokinesis', null)
                    return
                }

                safePlaceBlockDisplay(held, player)
            }

            NINJAGO_HELD.set(player, 'geokinesis', null)
        })
})

StartupEvents.registry('palladium:abilities', event => {
    event.create('ninjago:telekinesis_grab')
        .icon(palladium.createItemIcon('minecraft:lead'))
        .documentationDescription('Short range grab')
        .addProperty('strength', 'float', 0.7, 'Strength')

        .firstTick((player, entry, holder, enabled) => {
            if (!enabled || !player || !player.isPlayer()) return

            let ray = player.rayTrace(3, false)
            if (ray.entity && ray.entity.type !== 'minecraft:block_display') {
                if (isProtectedEntityTarget(player, ray.entity)) return
                NINJAGO_HELD.set(player, 'grab', ray.entity)
            }
        })

        .tick((player, entry, holder, enabled) => {
            if (!enabled || !player || !player.isPlayer()) return

            let held = NINJAGO_HELD.get(player, 'grab')
            if (!held) return

            moveHeldEntity(player, held, entry.getPropertyByName('strength'), 1)
        })

        .lastTick((player) => {
            NINJAGO_HELD.set(player, 'grab', null)
        })
})

StartupEvents.registry('mob_effect', event => {
    event.create('ninjago:telekinetic_hold')
        .effectTick((entity) => {
            if (!entity.server) return
            superpowerUtil.addSuperpower(entity, "ninjago:telekinetic_hold")
        })
        .color(0x68d4ff)
})