import { Block, EntityComponentTypes, EquipmentSlot, ItemStack, system, world } from '@minecraft/server';
import "./drill"
import "./hopper"
import "./lights"
import "./tnt"
import "./automatic"
import "./controls"
import "./stations"
import "./whistle"
import { createVector, isMovingTowards, queryBlock, sendAddonMessage, sendDebugMessage, vectorAdd, vectorEquals, vectorH_Angle, vectorMagnitude, vectorMultiplyByNumber, vectorRoundDown, vectorSubtract } from './JM_Utils';
import { automatic } from './automatic';
import { checkCollision } from './damage';

// system.afterEvents.scriptEventReceive.subscribe(event => {
    // sendAddonMessage(event.sourceEntity,"text.lifeboat_ta.Add-OnNameTag",
    // [translationKey,[args (optional)]],
    // ...
    // )
// })

const decoupleDistMultiple = 2

export const locomotiveFamily = "lb_ta_locomotive",
    dimensions = ["overworld","the_end","nether"],
    trainFamily = "lb_ta_train",
    cabooseFamily = "lb_ta_caboose",
    lightProperty = "lifeboat_ta:light_on",
    railTypes = ["rail","activator_rail","detector_rail","golden_rail"]

const decouplerItem = "lifeboat_ta:decoupling_tool",
    carSizes = [
        [
            "minecraft:minecart",
            1
        ],
		[
            "lifeboat_ta:hopper_wagon",
            1.3
        ],
		[
            "lifeboat_ta:chest_wagon",
            1.3
        ],
        [
            "lifeboat_ta:handcar",
            1.7
        ],
        [
            "lifeboat_ta:drill",
            1.89
        ],
        [
            "lifeboat_ta:train_t",
            3
        ],
        [
            "lifeboat_ta:train_emdgp",
            2.9
        ],
        [
            "lifeboat_ta:train_upgtel",
            2.95
        ],
        [
            "lifeboat_ta:train_subway",
            2.8
        ],
        [
            "lifeboat_ta:subway_car",
            2.85
        ],
        [
            "lifeboat_ta:train_bullet",
            4.4
        ],
        [
            "lifeboat_ta:highspeed_car",
            3.75
        ],
        [
            "lifeboat_ta:train_aem_7",
            2.93
        ],
        [
            "lifeboat_ta:train_quarry",
            1.95
        ],
        [
            "lifeboat_ta:train_e2_steam",
            2.65
        ],
        [
            "lifeboat_ta:train_metro",
            2.8
        ]
    ],
    dropInventoryOnDeath = [
        "lifeboat_ta:drill",
        "lifeboat_ta:chest_trainvagon",
        "lifeboat_ta:coal_trainvagon",
        "lifeboat_ta:chest_wagon",
        "lifeboat_ta:hopper_wagon"
    ]

world.afterEvents.playerSpawn.subscribe(event => {
    if (!event.initialSpawn) return

    const player = event.player
    if (!player.hasTag("lb_ta:start_book")) {
        player.runCommand('structure load \"lb_TA/lb_ta_start_book\" ~~1~')
        player.addTag("lb_ta:start_book")
    }
})

// manages the stored ID of the train car entity being pulled by the given entity
export function couplingData(entity) {
    const connectsProperty = "LB_t:Connections"
    if (!entity.getDynamicPropertyIds().includes(connectsProperty)) set()

    function get() {
        try {
            return JSON.parse(entity.getDynamicProperty(connectsProperty))
        } catch (error) {
            return undefined
        }
    }

    function getEntity() {
        try {
            const id = get()
            if (!id) throw new Error(`${entity.typeId} is not pulling anything`)
            const pulledCar = world.getEntity(id)
            return pulledCar
        } catch (error) {
            sendDebugMessage(`unable to get pulled car, is it out of bounds? decoupling...`)
            sendDebugMessage(error)
            return decouple()
        }
    }

    function decouple(sendMessage) {
        set()
        entity.dimension.playSound("random.break",entity.location)
        entity.dimension.playSound("dig.chain",entity.location)
        if (sendMessage) sendAddonMessage(world.getAllPlayers(),["uncoupled"])
        return undefined
    }

    function couple(id) {
        if (!id) throw new Error(`cannot couple to id ${id}!`)
        if (id === entity.id) throw new Error(`cannot couple ${entity.typeId} to itself!`)
        set(id)
        entity.dimension.playSound("dig.chain",entity.location)
        entity.dimension.playSound("random.anvil_land",entity.location,{volume:0.18})
    }

    function set(id) {
        try {
            if (id === null || id === undefined) {
                entity.setDynamicProperty(connectsProperty)
            } else {
                entity.setDynamicProperty(connectsProperty,JSON.stringify(id))
            }
        } catch (error) {
            world.sendMessage(`${error}`)
        }
    }

    return {
        getId: function() {
            return get()
        },
        getEntity: function() {
            return getEntity()
        },
        decouple: function(sendMessage) {
            return decouple(sendMessage)
        },
        couple: function(id) {
            return couple(id)
        },
        // set: function(id) {
        //     set(id)
        // },
        reset: function() {
            set()
        }
    }
}

// decoupler item functionality
world.afterEvents.entityHitEntity.subscribe(event => {
    if (event.damagingEntity.typeId !== "minecraft:player") return
    const heldItem = event.damagingEntity.getComponent("equippable").getEquipment(EquipmentSlot.Mainhand)?.typeId
    if (heldItem !== decouplerItem) return
    if (!event.hitEntity.getComponent("type_family")?.hasTypeFamily("minecart")) return
    const car = event.hitEntity,
        pulledBy = getPullingCar(car.id,car.dimension)
    if (!pulledBy) {
        sendDebugMessage(`could not find anything pulling ${car.typeId} :(`)
        return
    }
    couplingData(pulledBy).decouple()
})

// gets car pulling provided identifier, if one exists; else undefined
export function getPullingCar(pulledId,dimension) {
    const potentials = dimension.getEntities({families:[trainFamily]})
    const index = potentials.findIndex(potential => couplingData(potential).getId() === pulledId)
    if (index >= 0) return potentials[index]
    return undefined
}

// handle cars being spontaneously removed from the world (decouple relevant cars, drop inventories)
world.beforeEvents.entityRemove.subscribe(event => {
    const entity = event.removedEntity
    if (entity.hasComponent("type_family") && !entity.getComponent("type_family").hasTypeFamily(trainFamily)) return
    const train = event.removedEntity
    if (dropInventoryOnDeath.includes(train.typeId)) {
        let inventoryComponent = train.getComponent('inventory');
        let inventorySize = inventoryComponent.inventorySize;
        let container = inventoryComponent.container;
        const dimension = train.dimension
        const location = train.location
        const items = []
        for (let i = 0; i < inventorySize; i++) {
            let item = container.getItem(i);
            if (item !== undefined) {
                items.push(item)
            }
        }
        system.run(() => {
            items.forEach(item => {
                dimension.spawnItem(item, location);
                sendDebugMessage(`spawned ${item.typeId} at ${JSON.stringify(location)}`)
            })
        })
    }
    const pullCar = getPullingCar(entity.id,entity.dimension)
    if (pullCar) {
        sendDebugMessage(`${entity.typeId} is being removed from the world!  removing connection data from ${JSON.stringify(pullCar)}`)
        system.run(() => couplingData(pullCar).decouple())
    }
})

// makes coffee for whoever calls this function
function couplingParticles() {
    function spawnLocation(location,height) {
        return {x:location.x,y:location.y+height,z:location.z}
    }
    function particleLocation(start, end, t) {
        return {
            x: start.x + (end.x - start.x) * t,
            y: start.y + (end.y - start.y) * t,
            z: start.z + (end.z - start.z) * t
        };
    }

    function spawnParticles(car, red = false) {
        const connects = couplingData(car).getId()
        if (!connects) return
        const pulledCar = world.getEntity(connects)

        const start = spawnLocation(pulledCar.location,3)
        const end = spawnLocation(car.location,3)
        // const dist = Math.hypot(end.x - start.x, end.y - start.y, end.z - start.z) + 3
        const numParticles = 7 //Math.floor(dist)
        const particleOffset = (system.currentTick % 30) / 30 // cycles between 0 and 1 every 30 ticks
        for (let i = 0; i < numParticles; i++) {
            const segmentOffset = i / numParticles // Fractional position between 0 and 1 for each particle
            const adjustedOffset = segmentOffset + particleOffset * (1 / numParticles);
            const particlePos = particleLocation(start, end, adjustedOffset);
            try {
                car.dimension.spawnParticle(`lb_ta:couple_${red ? "red" : "green"}`, particlePos);
            } catch (error) {
                // meh
            }
        }
    }

    dimensions.forEach(element => {
        const dimension = world.getDimension(element);
        const allTrains = dimension.getEntities({families:[trainFamily]})
        const locomotives = dimension.getEntities({families:[locomotiveFamily]})
        locomotives.forEach(car => {
            const thisTrain = [car.id]
            let connects = couplingData(car).getId()
            if (!connects) return
            while(true) {
                allTrains.splice(allTrains.findIndex(train => train.id === car.id),1)
                spawnParticles(car)
                connects = couplingData(car).getId()
                if (!connects) break
                if (thisTrain.includes(connects)) {
                    sendDebugMessage(`infinite loop!`)
                    couplingData(car).reset()
                    break
                }
                car = world.getEntity(connects)
                thisTrain.push(car.id)
            }
        })
        allTrains.forEach(car => {
            const connects = couplingData(car).getId()
            if (!connects) return
            spawnParticles(car,true)
        });
    });
}

////////////////////////////////////////////////////////
// set direction based on movement and rail direction //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// states: 0 = n/s, 1 = e/w, 2 = up E, 3 = up W, 4 = up N, 5 = up S, 6 = s/e, 7 = s/w, 8 = n/w, 9 = n/e //
// states 6, 7, 8, and 9 are exclusive to normal rails (these are the curved rails)                     //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
function rotateCar(car) {
    // this may fail to find a relevant block if the car is on a slope and happens to be positioned above the relevant rail block
    // in this case, we can ignore it because the car will remain rotated correctly regardless.
    // there are no other edge cases that I have found or thought of to account for
    try {
        const carBlock = queryBlock(car.location,car.dimension),
            railState = carBlock?.permutation.getState("rail_direction"),
            carVel = car.getVelocity()
        // skip if unloaded
        if (carBlock === undefined) return
        // rail based rotations
        if (car.getComponent("type_family").hasTypeFamily(trainFamily) && isRail(carBlock) && getSpeed(carVel) > 0.025) {
            const curDir = car.getViewDirection()
            let curDegrees = Math.round(Math.atan2(0 - curDir.x, curDir.z) * (180 / Math.PI))
            // if (curDegrees < 0) curDegrees += 360
            let degrees = getDirectionFromRailState(railState,car) // the direction the train should be rotated to
            // prevents rotating more than 90 degrees
            if (exceedsAngle(90,degrees,curDegrees)) degrees += 180
            car.setRotation({x: 0, y: degrees})
        }
    } catch (error) {
        sendDebugMessage(error)
    }
}

function applyGravity(car) {
    car.applyImpulse(getGravityImpulse(car))
}

const gravConsts = {
    // 0.05 = normal drag for minecarts, 0.04 = normal acceleration for minecarts
    g: -0.4,
    k: 0.00,
    dt: 1/20 // 1 tick
}

function getGravityImpulse(car) {
    const carVel = car.getVelocity(); // { x, y, z }

    // Gravity force (assumes +Y is up, so gravity is negative)
    const gravityForce = gravConsts.g; // e.g., -0.001

    // Drag force
    const dragForce = gravConsts.k * carVel.y * carVel.y;
    const dragDirection = carVel.y > 0 ? -1 : (carVel.y < 0 ? 1 : 0);
    const drag = dragForce * dragDirection;

    // Net acceleration = gravity + drag (opposing motion)
    const acceleration = gravityForce + drag;

    // Impulse = current velocity + acceleration over time
    const result = (acceleration * gravConsts.dt);

    return { x: 0, y: result, z: 0 };
}

export function getCarSize(carType) {
    for (let i = 0; i < carSizes.length; i++) {
        if (carSizes[i][0] === carType) return carSizes[i][1]
    }
    return 2.35 // default size
}

function getDistFromEdge(position,datumBlock) {
    const offset = vectorRoundDown(position)
    offset.x = datumBlock.x - offset.x
    offset.z = datumBlock.z - offset.z
    if (offset.x > 1 || offset.z > 1) throw new Error(`${JSON.stringify(vectorRoundDown(position))} is too far away from ${JSON.stringify(datumBlock)}!`)
    let edge = offset.x === 0 ? (offset.z > 0 ? "south" : "north" ) : (offset.x > 0 ? "east" : "west")
    switch (edge) {
        case "north":
            return position.z - Math.floor(position.z)
        case "east":
            return datumBlock.x - position.x
        case "south":
            return datumBlock.z - position.z
        case "west":
            return position.x - Math.floor(position.x)
    }
}

// height off of rail is needed to determine gravity calculations
function vertDistFromRail(car, block) {
    const direction = block.permutation.getState("rail_direction")
    if ([2, 3, 4, 5].includes(direction)) {
        // it's a sloped rail
        // since all sloped rails are at a 45 degree angle and we need the tangent of the angle to get the height and tan(45) = 1,
        // we take a shortcut and don't even bother.  Car horizontal location equals track height at that location,
        // so vertical height - track height = height above track
        // technically, this could be adjusted by 45 again to take into consideration the height perpendicular to the track, but meh 🤷‍♂️
        let trackHeight = 0
        const carHeight = car.location.y - Math.floor(car.location.y)
        switch(direction) {
            case 2: // west up to east
                trackHeight = car.location.x - Math.floor(car.location.x) // * tan(45)
                break
            case 3: // east up to west
                trackHeight = 1 - (car.location.x - Math.floor(car.location.x))
                break
            case 4: // south up to north
                trackHeight = 1 - (car.location.z - Math.floor(car.location.z))
                break
            case 5: // north up to south
                trackHeight = car.location.z - Math.floor(car.location.z)
                break
            default:
                throw new Error('this is impossible')
        }
        return carHeight - trackHeight
    } else {
        // it's a flat rail
        return car.location.y - block.y
    }
}

// I don't remember what this does or why it exists, sorry.  Good luck.
function loadedNotificationFlag(locomotive) {
    const property = "lb_ta:notify_status_flag"
    if (!locomotive.getDynamicPropertyIds().includes(property)) locomotive.setDynamicProperty(property,JSON.stringify(false))

    function get() {
        return JSON.parse(locomotive.getDynamicProperty(property))
    }

    function set(bool) {
        locomotive.setDynamicProperty(property,JSON.stringify(bool))
    }

    return {
        status: function() {
            return get()
        },
        flag: function() {
            if (!get()) sendAddonMessage(locomotive.dimension.getPlayers(),["train_unloaded"])
            set(true)
        },
        reset: function() {
            set(false)
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////// MAIN GAMEPLAY LOOP ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
// coupling particles, train manipulation
system.runInterval(() => {
    let particlesAlreadyDisplayed = false
    world.getAllPlayers().forEach(player => {
        const heldItem = player.getComponent("equippable").getEquipment("Mainhand")
        if (heldItem && heldItem.typeId === decouplerItem && !particlesAlreadyDisplayed) {
            couplingParticles()
            particlesAlreadyDisplayed = true
        }
    })

    // manipulate trains: couple, push/pull
    dimensions.forEach(element => {
        const dimension = world.getDimension(element)
        // break all minecarts carrying our trains
        dimension.getEntities({type:"minecraft:minecart"}).forEach(cart => {
            const rideableComponent = cart.getComponent("rideable"),
                riders = rideableComponent.getRiders()

            for (let r = 0; r < riders.length; r++) {
                const rider = riders[r];
                if (rider.getComponent("type_family").hasTypeFamily(trainFamily)) { // eject
                    // rideableComponent.ejectRiders()
                    cart.kill()
                    return
                }
            }
            return
        })

        const locomotives = dimension.getEntities({families:[locomotiveFamily]})
        const cars = dimension.getEntities({families:[trainFamily]})

        cars.forEach(car => {
            const velocity = car.getVelocity()
            const facingDir = -(car.getRotation().y + 90)
            const facingVect = {x:Math.cos(facingDir), z: -Math.sin(facingDir)}

            const dotProd = (velocity.x * facingVect.x) + (velocity.z * facingVect.z)
            car.setProperty("lb_ta:forward",dotProd > 0 ? true : false)


            let gravity = true,
                rotate = true
            const carBlock = queryBlock(car.location,car.dimension)
            if (carBlock && isRail(carBlock)) {
                if (vertDistFromRail(car, carBlock) < 0.3) gravity = false
            } else rotate = false
            if (gravity) applyGravity(car)
            if (rotate) rotateCar(car)
            checkCollision(car)
        })

        // loop through locomotives and identify all train cars
        const trains = []
        locomotives.forEach(locomotive => {
            let isFullyLoaded = true
            // identify all cars in this train, remove from cars[]
            const train = [locomotive],
                ids = [locomotive.id]
                if (queryBlock(locomotive.location, locomotive.dimension) === undefined) isFullyLoaded = false
            for (let c = 0; c < train.length; c++) {
                const curCar = train[c];
                if (!curCar) {
                    // pulled car is probably out of range, or dead.  decouple!
                    couplingData(train[c-1]).reset()
                    // add to cars array to search for new couplings later
                    cars.push(train[c-1])
                    train.splice(c,1)
                    continue
                }
                const curData = couplingData(curCar)
                if (!curData.getId()) continue
                const pulledCar = curData.getEntity()
                if (!pulledCar) continue
                if (pulledCar.dimension.getBlock(pulledCar.location) === undefined) isFullyLoaded = false
                // pulled car exists and is validly connected; remove PULLING car from array
                const index = cars.findIndex(foundCar => foundCar.id === curCar.id)
                if (index >= 0) cars.splice(index,1)
                if (ids.includes(pulledCar.id)) {
                    sendDebugMessage("infinite loop! breaking...")
                    curData.reset()
                    break
                }
                train.push(pulledCar)
                ids.push(pulledCar.id)
                // if (pulledCar.getComponent("type_family").hasTypeFamily(locomotiveFamily)) break
                if (c > 1000) {
                    sendDebugMessage("unhandled infinite loop detected!")
                    return
                }
            }
            if (isFullyLoaded) {
                loadedNotificationFlag(locomotive).reset()
                automatic(locomotive)
            } else {
                loadedNotificationFlag(locomotive).flag()
            }
            // sendDebugMessage(`final train: ${JSON.stringify(train)}`)
            trains.push(train)
        })
        
        trains.forEach(train => {
            const lastCar = train[train.length - 1]
            if (!lastCar.getComponent("type_family")?.hasTypeFamily(cabooseFamily)) {
                const closeCars = lastCar.dimension.getEntities({
                    excludeFamilies:[locomotiveFamily],
                    families:[trainFamily],
                    location:lastCar.location,
                    minDistance:0.001,
                    maxDistance:getCarSize(lastCar.typeId) + 5
                })
                for (let p = 0; p < closeCars.length; p++) {
                    const potential = closeCars[p]
                    // if already in a train, skip
                    if (trains.some(train => train.some(trainCar => trainCar.id === potential.id))) {
                        closeCars.splice(p,1)
                        continue
                    }
                    // if our potential car is found within [cars], it must be an orphan car, since all non-orphans have been removed already
                    const orphanId = cars.findIndex(orphan => orphan.id === potential.id)
                    if (orphanId >= 0) {
                        // is it on a connected track?
                        const path = railPathDirection(queryBlock(lastCar.location,lastCar.dimension),queryBlock(potential.location,potential.dimension),false)?.path
                        if (!path) continue
                        // are the cars moving towards eachother?
                        if (isMovingTowards(lastCar,potential)) {
                            const pullingCar = getPullingCar(potential.id,potential.dimension)
                            if (pullingCar) couplingData(pullingCar).reset()
                            couplingData(lastCar).couple(potential.id)
                            cars.splice(orphanId,1)
                            // car faces the engine
                            const deltaX = path[1].x - path[0].x
                            const deltaZ = path[1].z - path[0].z
                            let angleRad = Math.atan2(deltaZ, deltaX)
                            let rotation = (90 + (angleRad * 180 / Math.PI))
                            potential.setRotation({x:0,y:rotation})
                            // engine faces away
                            if (lastCar.getComponent(EntityComponentTypes.TypeFamily).hasTypeFamily(locomotiveFamily)) {
                                angleRad = Math.atan2(
                                    lastCar.location.x - potential.location.x,
                                    lastCar.location.z - potential.location.z
                                )
                                rotation = -((angleRad * 180 / Math.PI))
                                lastCar.setRotation({x:0,y:rotation})
                            }
                            break
                        }
                    }
                }
                cars.splice(cars.findIndex(car => car.id === lastCar.id),1)
            } else { // caboose-specific behavior:
                // sync light with engine
                const cabooseLight = lastCar.getProperty(lightProperty),
                    engineLight = train[0].getProperty(lightProperty)
                if (engineLight !== undefined && engineLight !== cabooseLight) {
                    lastCar.setProperty(lightProperty,engineLight)
                }
            }

            ////////////////////////////////////////

            // skip single locomotives
            if (train.length <= 1) return

            const carVel = train[0].getVelocity()

            let pullForward = true,
            carBlock = queryBlock(train[0].location,dimension)
            if (!carBlock) return
            const targetSpeed = getSpeed(carVel)

            // Compares pulling car's direction to the found path to determine which direction it is going.  If not going backwards, assume forwards.
            const pullingCarDirection = vectorDirection(carVel),
                decoupleDist = Math.ceil((Math.ceil(getCarSize(train[0].typeId)) + Math.ceil(getCarSize(train[1]))) * decoupleDistMultiple),
                path = railPathDirection(queryBlock(train[1].location,dimension),carBlock,true,decoupleDist)?.path
            if (path === undefined) { // too far away or rail has been broken. break connection!
                sendDebugMessage(`failed to find path from ${JSON.stringify(vectorRoundDown(train[1].location))} to ${JSON.stringify(carBlock)}`)
                // const property = train[0].getProperty("lifeboat_ta:speed")
                // if (property === 0) { // if not under automatic movement, decouple
                    couplingData(train[0]).decouple(true)
                    train.splice(0)
                // } else {
                    // automatic movement enabled - do not break up the train!
                // }
                return
            }
            for (let i = 0; i < pullingCarDirection.length; i++) {
                if (
                    // if the vectors are equal, then the engine is moving towards the car it is pulling; i.e., backwards
                    vectorEquals(
                        // calculates the block that this car's velocity is taking it to
                        vectorAdd(
                            pullingCarDirection[i],
                            vectorRoundDown(train[0].location)
                        ),
                        // the first block in the direction of the car this engine is pulling.
                        // path is FROM pulled car TO pulling car, so length - 1 should be the last block, which is carBlock.
                        // length - 2 is the one before that in the path.
                        path[path.length - 2]
                    )
                    // || targetSpeed === 0
                ) {
                    pullForward = false
                    break
                }
            }
            // starting with the 2nd car, locomotives are pushed via automatic.js
            for (let i = 1; i < train.length; i++) {
                const curCar = train[i]
                // spacing should be size of previous car plus size of this car
                const targetSpacing = getCarSize(train[i-1].typeId) + getCarSize(curCar.typeId)
                if (["lifeboat_ta:coal_trainvagon", "lifeboat_ta:tender_trainvagon"].includes(curCar.typeId) && targetSpeed > 0) dropCoal(curCar)

                const curBlock = queryBlock(curCar.location,dimension)
                if (!curBlock) return
                const finishBlock = queryBlock(train[i-1].location,dimension)
                if (!finishBlock) return
                const decoupleDist = Math.ceil(Math.ceil(getCarSize(train[i-1].typeId)) + Math.ceil(getCarSize(curCar.typeId)) * decoupleDistMultiple)
                const railData = railPathDirection(curBlock,finishBlock,true,decoupleDist + 1)
                const pathToPullingCar = railData?.path
                if (pathToPullingCar === undefined) { // too far away or rail isn't connected, break connection!
                    couplingData(train[i-1]).decouple(true)
                    return
                } else if ([1,2,3].includes(pathToPullingCar)) return
                const curCarLocation = vectorRoundDown(curCar.location)
                // note that this length does NOT include the first and last block
                let dist = railData.length
                const pulledDist = getDistFromEdge(curCar.location,pathToPullingCar[1])
                dist += pulledDist
                const pullerDist = getDistFromEdge(train[i-1].location,pathToPullingCar[pathToPullingCar.length - 2])
                dist += pullerDist
                
                let nextBlock
                if (pullForward) { // if going forward
                    // if they've stacked themselves on the same block, we need to make sure the car doesn't move
                    nextBlock = (dist > 1 ? pathToPullingCar[1] : pathToPullingCar[0])
                } else { // if going backward or not moving at all
                    if (dist > 1) {
                        // get the possible directions from the block we're currently on
                        const possBlocks = railEndDirections(curBlock)
                        // figure out which one is the backwards direction by removing the one known to be forward
                        for (let p = possBlocks.length - 1; p >= 0; p--) {
                            if (!possBlocks[p]) continue
                            if (possBlocks[p].x === pathToPullingCar[1].x && possBlocks[p].z === pathToPullingCar[1].z) {
                                possBlocks.splice(p,1)
                            }
                        }
                        nextBlock = possBlocks[0]
                    } else {
                        nextBlock = curBlock
                    }
                }
                if (!nextBlock) return

                const vectToNext = vectorSubtract(nextBlock,curCarLocation)
                let speedAdjust = 0.00,
                    multiplier = 5 * (Math.abs(dist - targetSpacing))
                // // const buffer = 0.5
                if (pullForward) {
                    if (dist < targetSpacing) { // foward, but too close; allow to fall away
                        speedAdjust = -0.01 * (targetSpacing / dist) * multiplier
                    } else if (dist > targetSpacing) { // forward, too far away; push closer
                        speedAdjust = 0.01 * (targetSpacing / dist) * multiplier
                    }
                } else {
                    if (dist < targetSpacing) { // backwards, too close; push further away
                        speedAdjust = 0.01 * (targetSpacing / dist) * multiplier
                    } else if (dist > targetSpacing) { // backwards, too far away; allow train to catch up
                        speedAdjust = -0.01 * (targetSpacing / dist) * multiplier
                    }
                }
                
                const impulseVect = vectorAdd(
                    // original definition
                    vectorMultiplyByNumber(
                        vectToNext,
                        targetSpeed/2 + speedAdjust
                    ),
                    // added gravity because SERIOUSLY WHY
                    vectorMultiplyByNumber(
                        getGravityImpulse(curCar),
                        5
                    )
                )

                curCar.clearVelocity()
                curCar.applyImpulse(impulseVect)
            }
        })
    });
})

export function getSpeed(velocity) {
    return Math.sqrt(velocity.x * velocity.x + 
                        velocity.y * velocity.y + 
                        velocity.z * velocity.z);
}

function exceedsAngle(comparisonAngle, angle1, angle2) {
    let difference = Math.abs(angle1 - angle2);
    if (difference > 180) difference = 360 - difference
    return difference > comparisonAngle;
}
/**
 * Note that if velocity is 0, this will default to N or W directions for straight tracks, and NW/NE for curved tracks.
 * For my use case, this is fine; I'll never be calling this unless the train is already in motion.
 * @param {int} state 
 * @param {vector3} velocity 
 * @returns 
 */
export function getDirectionFromRailState(state, car) {
    const velocity = car.getVelocity()
    const curRot = car.getRotation().y

    const center = {
        x:Math.floor(car.location.x) + 0.5,
        z:Math.floor(car.location.z) + 0.5
    }

    const vectToCenter = {
        x:center.x - car.location.x,
        z:center.z - car.location.z
    }

    const majorDirection = Math.abs(vectToCenter.x) > Math.abs(vectToCenter.z) ? "x" : "z"

    const goingIn = velocity.x * vectToCenter.x + velocity.z * vectToCenter.z > 0

    switch(state) {
        case 0: // N/S, along Z axis
        case 4:
        case 5:
            if (velocity.z > 0) {
                return 0
            } else return 180
            break
        case 1: // E/W, along X axis
        case 2:
        case 3:
            if (velocity.x > 0) {
                return 270
            } else return 90
            break
        // SE/NW diagonal
        case 7: // S-W
        case 9: // N-E
            if (goingIn) {
                // no need to check which direction is major, since we return the same values regardless
                if (vectToCenter[majorDirection] > 0) {
                    return 315
                } else return 135
            }
            return curRot
            break
        case 6: // S-E
        case 8: // N-W
            if (goingIn) {
                if (majorDirection === "x") {    
                    if (vectToCenter[majorDirection] > 0) {
                        return 225
                    } else return 45
                } else {
                    if (vectToCenter[majorDirection] > 0) {
                        return 45
                    } else return 225
                }
            }
            return curRot
            break
    }
}

const offsets = {
    north:{
        above: createVector("0,1,-1"),
        below: createVector("0,-1,-1")
    },
    east: {
        above: createVector("1,1,0"),
        below: createVector("1,-1,0")
    },
    south: {
        above: createVector("0,1,1"),
        below: createVector("0,-1,1")
    },
    west: {
        above: createVector("-1,1,0"),
        below: createVector("-1,-1,0")
    }
}

/**
 * given a rail block, returns an array of the blocks this rail leads to.
 * returns undefined if the provided block is not a rail, or if an error is encountered while identifying rail branches
 * @param {Block} block 
 * @returns 
 */
function railEndDirections(block,allowIllegals = false) {
    if (!isRail(block)) {
        if (isRail(block.below())) {
            block = block.below()
        } else {
            // sendDebugMessage(`${block.type.id} is not a rail block! :(`)
            return undefined
        }
    }
    const state = block.permutation.getState("rail_direction")
    try {
        let data = []
        switch(state) {
            case 0: // N/S, along Z axis
                data.push(
                    block.south(),
                    block.offset(offsets.south.below),
                    block.north(),
                    block.offset(offsets.north.below)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.west(),
                        block.offset(offsets.west.below),
                        block.east(),
                        block.offset(offsets.east.below)
                    )
                }
                break
            case 4: // slopes down from north to south
                data.push(
                    block.offset(offsets.south.below),
                    block.south(),
                    block.offset(offsets.north.above)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.west(),
                        block.east()
                    )
                }
                break
            case 5: // slopes up from north to south
                data.push(
                    block.offset(offsets.south.above),
                    block.north(),
                    block.offset(offsets.north.below)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.west(),
                        block.east()
                    )
                }
                break
            case 1: // E/W, along X axis
                data.push(
                    block.west(),
                    block.offset(offsets.west.below),
                    block.east(),
                    block.offset(offsets.east.below)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.north(),
                        block.offset(offsets.north.below),
                        block.south(),
                        block.offset(offsets.south.below)
                    )
                }
                break
            case 2: // slopes down from east to west
                data.push(
                    block.offset(offsets.west.below),
                    block.west(),
                    block.offset(offsets.east.above)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.north(),
                        block.south()
                    )
                }
                break
            case 3: // slopes up from east to west
                data.push(
                    block.offset(offsets.west.above),
                    block.east(),
                    block.offset(offsets.east.below)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.north(),
                        block.south()
                    )
                }
                break
            case 7: // W to S
                data.push(
                    block.south(),
                    block.offset(offsets.south.below),
                    block.west(),
                    block.offset(offsets.west.below)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.north(),
                        block.offset(offsets.north.below),
                        block.east(),
                        block.offset(offsets.east.below)
                    )
                }
                break
            case 9: // N to E
                data.push(
                    block.east(),
                    block.offset(offsets.east.below),
                    block.north(),
                    block.offset(offsets.north.below)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.south(),
                        block.offset(offsets.south.below),
                        block.west(),
                        block.offset(offsets.west.below)
                    )
                }
                break
            case 6: // S to E
                data.push(
                    block.east(),
                    block.offset(offsets.east.below),
                    block.south(),
                    block.offset(offsets.south.below)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.west(),
                        block.offset(offsets.west.below),
                        block.north(),
                        block.offset(offsets.north.below)
                    )
                }
                break
            case 8: // W to N
                data.push(
                    block.north(),
                    block.offset(offsets.north.below),
                    block.west(),
                    block.offset(offsets.west.below)
                )
                // illegals:
                if (allowIllegals) {
                    data.push(
                        block.south(),
                        block.offset(offsets.south.below),
                        block.east(),
                        block.offset(offsets.east.below)
                    )
                }
                break
            default:
                throw new Error(`Unknown rail state: ${JSON.stringify(state)}`)
        }
        return data
    } catch (error) {
        sendDebugMessage(error)
        return undefined
    }
}

export function isRail(block) {
    if (!block) return false
    if (railTypes.includes(block.type.id.split(":")[1])) {
        return true
    }
    return false
}

function squaredVectDistance(pointA, pointB) {
    return Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2) + Math.pow(pointA.z - pointB.z, 2);
}

function railSearch(currentBlock, destination, maxDepth, allowIllegals, visited = [], path = [], pathLength = 0, currentDepth = 0) {
    if (currentDepth > maxDepth) return { path: null, length: null }
    path.push(currentBlock.location)
    visited.push(JSON.stringify(currentBlock.location))
    if (vectorEquals(currentBlock.location, destination)) return { path: path, length: pathLength }
    if (currentDepth > 0) pathLength += getBlockLength(currentBlock)
    const connectedBlocks = railEndDirections(currentBlock,allowIllegals)
    if (connectedBlocks) {
        // Sort connected blocks by proximity to the destination (closer first)
        connectedBlocks.sort((a, b) => {
            return squaredVectDistance(a.location,destination) - squaredVectDistance(b.location,destination)
        })
        for (const nextBlock of connectedBlocks) {
            if (!isRail(nextBlock)) continue
            if (!visited.includes(JSON.stringify(nextBlock.location))) {
                const result = railSearch(nextBlock, destination, maxDepth, allowIllegals, visited, path, pathLength, currentDepth + 1)
                if (result.path) return result
            }
        }
    }

    path.pop()
    return { path: null, length: null }
}

function getBlockLength(block) {
    if (block.permutation.getState("rail_direction") > 5) return 0.75
    return 1
}


/**
 * takes two Block objects and finds the path between them, if one exists in less than <depth> steps.
 * 
 * 1 = startBlock or finishBlock was undefined
 * 
 * 2 = railEndDirections received an undefined startBlock
 * 
 * 3 = railEndDirections received undefined fork block
 * 
 * undefined = no path was found in under <depth> steps, or at least one of the provided blocks was not a rail
 * @param {*} startBlock 
 * @param {*} finishBlock 
 * @param {*} depth 
 * @returns 
 */
export function railPathDirection(startBlock, finishBlock, allowIllegals = false, depth=10) {
    if (!startBlock || !finishBlock) {
        return 1
    }
    if (!isRail(startBlock) && isRail(queryBlock({x:startBlock.x,y:startBlock.y - 1,z:startBlock.z},startBlock.dimension))) {
        startBlock = startBlock.below()
    }
    if (!isRail(finishBlock) && isRail(queryBlock({x:finishBlock.x,y:finishBlock.y - 1,z:finishBlock.z},finishBlock.dimension))) {
        finishBlock = finishBlock.below()
    }
    if (!isRail(startBlock) || !isRail(finishBlock)) return undefined
    if (vectorEquals(startBlock.location,finishBlock.location)) return [startBlock]

    const data = railSearch(startBlock,finishBlock.location,depth,allowIllegals)
    if (data.path) return data
    // sendDebugMessage(`no path was found! time: ${Date.now() - startTick}ms, ${JSON.stringify(startBlock.location)}, ${JSON.stringify(finishBlock.location)} ${depth}`)
    return undefined
}

/**
 * returns unit vectors corresponding to the angle provided.
 * If a cardinal direction is provided, 1 vector is returned.
 * If a multiple of 45, 2 vectors are returned.
 * @param {vector3} vector 
 * @returns 
 */
function vectorDirection(vector) {
    const angle = vectorH_Angle(vector),
        roundedAngle = Math.round(angle / 45) * 45

    switch (roundedAngle) {
        case 0:
        case 360:
            return [{x:0,y:0,z:1}];
        case 45:
            return [{x:-1,y:0,z:0},{x:0,y:0,z:1}]
        case 90:
            return [{x:-1,y:0,z:0}];
        case 135:
            return [{x:-1,y:0,z:0},{x:-1,y:0,z:0}];
        case 180:
        case -180:
            return [{x:0,y:0,z:-1}];
        case -135:
            return [{x:1,y:0,z:0}, {x:0,y:0,z:-1}];
        case -90:
            return [{x:1,y:0,z:0}];
        case -45:
            return [{x:0,y:0,z:1}, {x:1,y:0,z:0}];
        default:
            return [`unknown: ${angle}, ${roundedAngle}`];
    }
}

function dropCoal(car) {
    let type = "coal",
        count = 1 + Math.floor(Math.random() * 3)
    if (Math.random() <= 0.001) {
        if (Math.random() <= 0.05) {
            type = "coal_block"
            count = Math.random() < 0.4 ? 2 : 1
        }
        const item = new ItemStack(type,count)
        sendDebugMessage(`spawning ${count} ${type}`)
        car.dimension.spawnItem(item,car.location)
        car.dimension.playSound("mob.chicken.plop",car.location)
        car.dimension.playSound("random.pop",car.location)
    }
}

let seriouslyIndex = 0
system.afterEvents.scriptEventReceive.subscribe(event => {
    if (event.id === "lb_ta:despawn_entity") {
        const sourceEntity = event.sourceEntity
        if (!sourceEntity) return
        if (sourceEntity.typeId === "minecraft:player") {
            sendAddonMessage(sourceEntity,[`seriously_${seriouslyIndex}`])
            if (seriouslyIndex < 13) seriouslyIndex++
            return
        } else {
            sourceEntity.remove()
        }
    }
    const [key, id] = event.id.split(":")
    if (key !== "lifeboat_ta") return
    switch(id) {
        case "lights_on":
            sendAddonMessage(event.sourceEntity.getComponent(EntityComponentTypes.Rideable).getRiders(),["lights_on"])
            break
        case "lights_off":
            sendAddonMessage(event.sourceEntity.getComponent(EntityComponentTypes.Rideable).getRiders(),["lights_off"])
            break
        default:
            // ignore            
    }
})

const gridOrient = [
    "lifeboat_ta:searchlight_signal",
    "lifeboat_ta:train_signal",
    "lifeboat_ta:train_signal_2",
    "lifeboat_ta:semaphore",
    "lifeboat_ta:semaphore_disc",
    "lifeboat_ta:lights_1",
    "lifeboat_ta:lights_2",
    "lifeboat_ta:lights_3",
    "lifeboat_ta:gpls",
    "lifeboat_ta:crossing_stop",
    "lifeboat_ta:crossing_signal",
    "lifeboat_ta:crossing_signal_2",
    "lifeboat_ta:color_position_lights",
    "lifeboat_ta:speed_sign"
]

world.afterEvents.entitySpawn.subscribe(event => {
    // grid orient
    const entity = event.entity
    if (gridOrient.includes(entity.typeId)) entity.setRotation({x:0,y:Math.round(entity.getRotation().y/45) * 45})

    // check for stacked cars
    const familyComponent = entity.getComponent(EntityComponentTypes.TypeFamily)
    if (familyComponent && familyComponent.hasTypeFamily(trainFamily)) {
        entity.dimension.getEntities({location:entity.location, maxDistance:0.0001,type:entity.typeId}).forEach(stack => {
            if (stack.id !== entity.id) {
                stack.remove()
            }
        })
    }
})