import { EntityComponentTypes, system, world } from "@minecraft/server";
import { bottomCenter, createVector, distance, isWithinRadius, queryBlock, sendAddonMessage, sendDebugMessage, vectorAbove, vectorEquals, vectorMultiplyByNumber } from "./JM_Utils";
import { couplingData, getSpeed, isRail, railPathDirection } from "./trainsAddOn";
import { generateTitle } from "./nameGen";
import { isStationBlock, nearbyStations, stationData } from "./stations";
import { speedSignData } from "./speedSign";

const atStationTag = "lb_ta:at_station"

// this defines the actual velocity the speed setting corresponds to. speed 1 = this amount, speed 2 = 2x this amount, etc.
export const speedLimitInterval = 0.2

// provides and manages the resume tick and last station visited for the given locomotive
export function trainStopData(locomotive) {
    const property = "lb_ta:station_resume_time"
    const defaultDelayData = {
        resumeTick: 0,
        lastStation: {x:0,y:-100,z:0}
    }
    if (!locomotive.getDynamicPropertyIds().includes(property)) override(defaultDelayData)

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

    function set(tick, location) {
        const newData = {}
        newData.resumeTick = tick
        newData.lastStation = location
        locomotive.setDynamicProperty(property,JSON.stringify(newData))
    }

    function override(data) {
        locomotive.setDynamicProperty(property,JSON.stringify(data))
    }
    return {
        get: function() {
            return get()
        },
        schedule: function(tick, stationLocation) {
            set(tick, stationLocation)
            if (stationData(stationLocation, locomotive.dimension).get().eject) ejectTrain(locomotive)
        },
        isTimerFinished: function() {
            const data = get()
            const station_data = stationData(data.lastStation, locomotive.dimension).get()
            // are we still within stop radius?
            if (isWithinRadius(locomotive.location, bottomCenter(vectorAbove(data.lastStation)), getSlowdownRadius(locomotive))) {
                if (data.resumeTick === -1 && locomotive.hasTag(atStationTag)) {
                    // it's a load station
                    try {
                        // check station power state
                        const block = locomotive.dimension.getBlock(data.lastStation)
                        if (block) {
                            if (!block.typeId.includes("load_station")) return true // station has been removed
                            if (block.typeId === "lifeboat_ta:load_station") {
                                // release next tick (done this way so departure message trigger still fires)
                                set(system.currentTick + 1, data.lastStation)
                            } else return false
                        } else return false
                    } catch (error) {
                        if (error.name !== "LocationOutOfWorldBoundariesError") sendDebugMessage(error)
                        return true // it's the default coordinate, which is intentionally outside of the world
                    }
                } else {
                    // it's a passenger station
                    return system.currentTick >= data.resumeTick
                }
            } else {
                // outside of stop radius; release (locomotive was moved manually)
                return true
            }
        },
        override: function(data) {
            return override(data ? data : defaultDelayData)
        },
        cancel: function() {
            const newData = get()
            newData.resumeTick = 0
            override(newData)
        }
    }
}

function ejectTrain(locomotive) {
    let curCar = locomotive
    while (curCar) {
        ejectMobs(curCar)
        const nextId = couplingData(curCar).getId()
        if (!nextId) return
        curCar = world.getEntity(nextId)
    }
}

export function ejectMobs(car, count = Number.MAX_VALUE) {
    const rideableComponent = car.getComponent(EntityComponentTypes.Rideable)
    if (!rideableComponent) return
    for (const entity of rideableComponent.getRiders()) {
        if (count <= 0) return
        if (entity.typeId !== "minecraft:player") {
            count--
            rideableComponent.ejectRider(entity)
        }
    }
}

// pushes the given locomotive according to its speed status.
// adjusts speed when passing nearby speed signs.
export function automatic(locomotive) {
    let speedProperty = locomotive.getProperty("lifeboat_ta:speed")
    if (!speedProperty) return
    // respect speed limit signs if there's no driver
    if (locomotive.getComponent(EntityComponentTypes.Rideable).getRiders().length === 0) {
        const speedSign = locomotive.dimension.getEntities(
            {
                location: locomotive.location,
                families: ["lb_ta_speed_sign"],
                maxDistance: 3,
                closest: 1
            }
        )[0]
        if (speedSign) {
            const signValue = speedSignData(speedSign).get()
            if (speedProperty === signValue) {
                // do nothing
            } else if (speedProperty > signValue) {
                // train is going too fast
                locomotive.setProperty("lifeboat_ta:speed", signValue)
                speedProperty = signValue
            } else {
                // bring train speed up to signValue
                while (speedProperty < signValue) {
                    try {
                        speedProperty++
                        locomotive.setProperty("lifeboat_ta:speed", speedProperty)
                    } catch (error) {
                        // locomotive has reached maximum speed
                        speedProperty--
                        break
                    }
                }
            }
        }
    }

    // waiting at station?
    const waitingData = trainStopData(locomotive)
    if (!waitingData.isTimerFinished()) return

    const nearby = nearbyStations(locomotive)
    let nearestStation = {
        location: undefined,
        distance: Number.POSITIVE_INFINITY
    }

    let slowdown = false

    for (const locationString in nearby.get()[locomotive.dimension.id]) {
        const location = createVector(locationString)
        const dist = distance(bottomCenter(vectorAbove(location)),locomotive.location)
        try {
            const block = locomotive.dimension.getBlock(location)
            if (block) {
                // don't bother slowing for unpowered load stations
                if (block.typeId === "lifeboat_ta:load_station") {
                    // if it's an unpowered load station and it's the same as the last station we were at, then we are just leaving
                    // and need it marked as nearest so the departurem message triggers.
                    // if it's not the last station we were at, don't mark as nearest, so there will be no messaging
                    if (dist < nearestStation.distance && vectorEquals(trainStopData(locomotive).get().lastStation, location)) {
                        nearestStation.location = location
                        nearestStation.distance = dist
                    }
                    continue
                }
                if (!isStationBlock(block)) {
                    nearby.remove(location)
                    continue
                }
            } else continue
        } catch (error) {
            sendDebugMessage(error)
            continue
        }
        const slowdownRadius = getSlowdownRadius(locomotive)
        // slow down if we're in this radius
        if (isWithinRadius(locomotive.location, bottomCenter(vectorAbove(location)), slowdownRadius)) {
            // are we connected? find station rail
            const stationBlock = locomotive.dimension.getBlock(location)
            const stationRail = isRail(stationBlock.above(1)) ? stationBlock.above(1) : isRail(stationBlock.above(2)) ? stationBlock.above(2) : undefined
            if (stationRail !== undefined && railPathDirection(locomotive.dimension.getBlock(locomotive.location), stationRail, true, slowdownRadius)) slowdown = true
        }
        if (dist < nearestStation.distance) {
            nearestStation.location = location
            nearestStation.distance = dist
        }
    }

    // note that we've already applied the slowdown effect if within a radius!
    if (nearestStation.location) {
        // a nearby station has been found
        const nearStationData = stationData(nearestStation.location, locomotive.dimension).get()
        // are we within the stopping distance?
        if (isWithinRadius(locomotive.location, bottomCenter(vectorAbove(nearestStation.location)), 2)) {
            // inside stopping distance
            if (nearStationData.notifications && nearStationData.nearby) {
                var playerMessageOptions = {
                    location: nearestStation.location,
                    maxDistance: 70
                }
            }
            // this tag controls if we should announce new stations,
            // and prevents stopping multiple times by only resetting after fully departed
            if (locomotive.hasTag(atStationTag)) {
                // inside stop distance, but locomotive already has atStationTag
                // this could be a station we're currently stopped at
                // or, it could be a new station that's so close we never exited the previous one.
                // timer is finished, but we have atStationTag; either we're leaving, or entering an immediately adjacent station
                const thisStopData = waitingData.get()
                if (vectorEquals(thisStopData.lastStation, nearestStation.location)) {
                    // nearest station is the last station we were at; we must be leaving (unless the player is messing with us and made a 2x2 circle, but I'm ignoring that)
                    if ((thisStopData.resumeTick + 1 === system.currentTick) && nearStationData.notifications) sendAddonMessage(
                        locomotive.dimension.getPlayers(playerMessageOptions),
                        locomotive.nameTag === "" ? `entity.${locomotive.typeId}.name` : locomotive.nameTag,
                        ["station_departure",[stationData(thisStopData.lastStation,locomotive.dimension).get()?.title]]
                    )
                    slowdown = false
                } else {
                    // nearest station is a new station, we are arriving!
                    // announce
                    if (nearStationData.notifications) sendAddonMessage(
                        locomotive.dimension.getPlayers(playerMessageOptions),
                        locomotive.nameTag === "" ? `entity.${locomotive.typeId}.name` : locomotive.nameTag,
                        ["station_arrival",[nearStationData.title]])
                    // mark at station
                    locomotive.addTag(atStationTag)
                    // stop
                    waitingData.schedule(nearStationData.stopTime === -1 ? -1 : system.currentTick + nearStationData.stopTime, nearestStation.location)
                    // return serves as a "stop" for the train - we skip the push part of this function
                    return
                }
            } else {
                // inside stop distance and we do NOT have the atStationTag. We are arriving.
                // announce
                if (nearStationData.notifications) sendAddonMessage(
                    locomotive.dimension.getPlayers(playerMessageOptions),
                    locomotive.nameTag === "" ? `entity.${locomotive.typeId}.name` : locomotive.nameTag,
                    ["station_arrival",[nearStationData.title]])
                // mark at station
                locomotive.addTag(atStationTag)
                // stop
                waitingData.schedule(nearStationData.stopTime === -1 ? -1 : system.currentTick + nearStationData.stopTime, nearestStation.location)
                // return serves as a "stop" for the train - we skip the push part of this function
                return
            }
        } else {
            // outside stopping distance
            waitingData.cancel()
            locomotive.removeTag(atStationTag)
            if (vectorEquals(waitingData.get().lastStation,nearestStation.location)) slowdown = false
        }
    } else {
        // no nearby stations
        waitingData.override()
    }

    // AT VALUES LESS THAN 1, TRAINS CANNOT MAKE IT UP HILLS
    const speedValue = (slowdown ? 0.5 : speedProperty) * speedLimitInterval

    const block = queryBlock(locomotive.location,locomotive.dimension)
    if (!block) return // train has gone outside of world, ignore
    if (!isRail(block) && !isRail(block.below())) { // derailed, stop!
        locomotive.setProperty("lifeboat_ta:speed",0)
        sendDebugMessage(`disabling ${locomotive.typeId}`)
        return
    }

    const direction = locomotive.getViewDirection()
    // equation: 
    //     limit^power              limit^power
    //   ----------------   -   ------------------ + (0.5 * limit)  =   push strength
    //   currentSpeed + 1            limit + 1
    const power = 0
    const magnitude = Math.max(
        0, 
        (0.05 * speedValue) + (Math.pow(speedValue,speedValue < 1 ? -power : power) / (getSpeed(locomotive.getVelocity()) + 1)) - (Math.pow(speedValue,speedValue < 1 ? -power : power) / (speedValue + 1))
    )
    // sendDebugMessage(`speed: ${getSpeed(locomotive.getVelocity()).toFixed(2)}, magnitude: ${magnitude.toFixed(6)}`)
    const impulse = vectorMultiplyByNumber(direction,magnitude)
    locomotive.applyImpulse(impulse)
}

export function getSlowdownRadius(locomotive) {
    return 9 * Math.log(locomotive.getProperty("lifeboat_ta:speed")) + 5
}