--!/rom/programs/startup
-- Main program for controlling a Create Big Cannon with ComputerCraft.
-- This script uses CC: Create Bridge to interact with Sequenced Gearshifts
-- for aiming, and bundled cables for building and firing.
-- It also communicates with a Pocket Computer client via Rednet.

-- Configuration Parameters (YOU MUST EDIT THESE!)
-- These values will depend on your specific cannon build and Minecraft world.
local config = {
    -- Peripheral names for your Sequenced Gearshifts
    -- Use the exact name given when you wrap it, e.g., "Create_SequencedGearshift_3"
    yawGearshiftSide = "Create_SequencedGearshift_3",
    pitchGearshiftSide = "Create_SequencedGearshift_2",

    -- Bundled cable side for ALL Redstone signals (build/fire/etc.)
    bundledCableSide = "left",

    -- Bundled cable colors (channels) for specific actions
    buildSignalColor = colors.black, -- The color of the channel for building/resetting (on/off)
    fireSignalColor = colors.red,    -- The color of the channel for firing (pulse)

    -- Rednet Communication
    modemSide = "back",              -- Side where your Wireless Modem is attached
    protocol = "cannon_control",     -- Custom Rednet protocol for unique communication
    status_protocol = "cannon_control_status", -- Protocol for status messages from the cannon

    -- Cannon specific parameters
    barrelLength = 15,           -- Length of the cannon barrel in blocks

    -- Gear Ratio: How many degrees the gearshift needs to rotate for 1 degree of cannon rotation.
    -- If 1 gearshift degree = 0.125 cannon degrees, then 1 cannon degree = 1 / 0.125 = 8 gearshift degrees.
    gearRatio = 8.0,             -- (1.0 / 0.125) Every 1 degree of cannon movement requires 8 degrees of gearshift rotation.

    -- Default cannon orientation offset (for built-in direction)
    -- This value should be the yaw angle (in degrees, 0-360) that your cannon faces
    -- when it's in its default, 'built' orientation (e.g., 0 for North, 90 for East, etc.)
    defaultYawOffset = 180,        -- Example: 0 for North, 90 for East, 180 for South, 270 for West

    -- Offsets in blocks from the COMPUTER'S position to the CANNON'S BARREL TIP
    -- These are crucial for accurate GPS-based trajectory calculations.
    xOffset = 0.0,               -- X-offset (East/West)
    yOffset = 1.0,               -- Y-offset (Up/Down)
    zOffset = 0.0,               -- Z-offset (North/South)

    -- --- PHYSICS PARAMETERS (REQUIRED FOR AUTOMATIC AIMING) ---
    -- You MUST fill these in based on your Create Big Cannons setup.
    -- These are crucial for accurate trajectory calculation.
    projectileVelocity = 50.0,   -- 💥 Initial speed of the projectile in blocks per tick (e.g., 50)
                                 --    (SET THIS VALUE TO YOUR CANNON'S ACTUAL MUZZLE VELOCITY!)
    gravity = 0.08,              -- ⬇️ Minecraft's default gravity, blocks per tick^2 (0.08)
                                 --    Adjust if you have mods changing gravity.
    airResistance = 0.00,        -- 🌬️ Simple drag coefficient (e.g., 0.01-0.05). Set to 0 for no drag.
                                 --    (SET THIS VALUE based on your observation, or leave 0 for no drag!)
    maxPitch = 90.0,             -- ⬆️ Maximum physical pitch angle your cannon can achieve (degrees)
    minPitch = -30.0,              -- ⬇️ Minimum physical pitch angle your cannon can achieve (degrees)
}

-- Global peripheral variables
local yawGearshift = nil
local pitchGearshift = nil
-- 'redstone' is a global API in CC:Tweaked, no need to peripheral.wrap it.

-- Internal state to track current angles
-- IMPORTANT: These values are tracked by the program, the physical gearshifts
-- will rotate relatively. It's critical that your physical cannon is aligned
-- to its 'defaultYawOffset' and 0 pitch when the program starts or `resetcannon` is used.
local currentYaw = 0.0
local currentPitch = 0.0

-- Internal state for build status
local isCannonBuilt = false

-- Cannon computer's own ID
local cannonComputerID = os.getComputerID()

-- Console input buffer
local commandBuffer = ""
local _, termHeight = term.getSize() -- Get terminal size once for height

--- Draws the command prompt and current buffer.
local function drawPrompt()
    term.setCursorPos(1, termHeight) -- Move to the last line of the terminal
    term.clearLine() -- Clear line to ensure old text is gone
    term.write("> " .. commandBuffer)
    term.setCursorPos(3 + string.len(commandBuffer), termHeight) -- Set cursor after buffer
end

--- Sends a status update message back to a given recipient via Rednet.
-- If recipientID is 0, broadcasts the status.
-- @param recipientID The ID of the computer to send the message to (or 0 to broadcast).
-- @param statusMessage The string message to send.
local function sendStatus(recipientID, statusMessage)
    if not rednet.isOpen(config.modemSide) then
        print("Rednet not open, cannot send status.")
        return
    end
    if recipientID == 0 then
        rednet.broadcast(statusMessage, config.status_protocol)
    else
        rednet.send(recipientID, statusMessage, config.status_protocol)
    end
end

--- Initializes all required peripherals.
-- @return boolean True if all peripherals are found, false otherwise.
local function initializePeripherals()
    print("Initializing peripherals...")
    yawGearshift = peripheral.wrap(config.yawGearshiftSide)
    pitchGearshift = peripheral.wrap(config.pitchGearshiftSide)

    if not yawGearshift then print("Error: Yaw Gearshift '" .. config.yawGearshiftSide .. "' not found. Check name/side and connection.") end
    if not pitchGearshift then print("Error: Pitch Gearshift '" .. config.pitchGearshiftSide .. "' not found. Check name/side and connection.") end

    -- Open Rednet for communication
    local success, message = pcall(rednet.open, config.modemSide)
    if not success then
        print("Error opening Rednet modem on " .. config.modemSide .. ": " .. tostring(message))
    else
        print("Rednet modem opened successfully on " .. config.modemSide)
    end

    if yawGearshift and pitchGearshift then
        print("All required peripherals initialized successfully.")
        return true
    else
        print("Failed to initialize one or both gearshift peripherals. Please check your config and in-game setup.")
        return false
    end
end

--- Sends a brief pulse on a specific bundled cable channel.
-- This function uses bitwise operations to safely toggle a single color channel
-- without affecting others on the same bundled cable.
-- @param color The color constant (e.g., `colors.red`) for the channel.
local function pulseBundled(color)
    local currentOutput = redstone.getBundledOutput(config.bundledCableSide)
    redstone.setBundledOutput(config.bundledCableSide, bit.bor(currentOutput, color))
    os.sleep(0.2) -- Keep the signal active briefly to ensure it registers in Create
    currentOutput = redstone.getBundledOutput(config.bundledCableSide) -- Re-read in case others changed
    redstone.setBundledOutput(config.bundledCableSide, bit.band(currentOutput, bit.bnot(color)))
end

--- Sets the state of a bundled cable channel (on/off).
-- This is used for the 'build' signal which is not a pulse.
-- @param color The color constant for the channel.
-- @param state True for on, false for off.
local function setBundledState(color, state)
    local currentOutput = redstone.getBundledOutput(config.bundledCableSide)
    if state then
        redstone.setBundledOutput(config.bundledCableSide, bit.bor(currentOutput, color))
    else
        redstone.setBundledOutput(config.bundledCableSide, bit.band(currentOutput, bit.bnot(color)))
    end
end

--- Normalizes an angle to be within the -180 to 180 degree range.
-- This helps in calculating the shortest rotation path.
-- @param angle The angle in degrees.
-- @return The normalized angle.
local function normalizeAngle(angle)
    angle = angle % 360
    if angle > 180 then
        angle = angle - 360
    elseif angle <= -180 then
        angle = angle + 360
    end
    return angle
end

--- Gets the cannon's current GPS location, applying configured offsets.
-- The returned coordinates represent the theoretical firing point (barrel tip).
-- @return table A table with x, y, z coordinates, or nil if GPS fails.
local function getCannonLocation()
    -- print("Attempting to get cannon GPS location...") -- Comment out for less console spam
    local x, y, z = gps.locate()
    if x then
        -- Apply configured offsets to the base GPS location for precise barrel position
        x = x + config.xOffset
        y = y + config.yOffset
        z = z + config.zOffset
        -- print(string.format("Cannon Location (with offsets): X:%.2f Y:%.2f Z:%.2f", x, y, z)) -- Comment out for less console spam
        return {x = x, y = y, z = z}
    else
        print("Could not get GPS location. Ensure wireless modems are set up and in range.")
        return nil
    end
end

--- Sets the yaw (horizontal) angle of the cannon using the Sequenced Gearshift.
-- Calculates the relative rotation needed from the current tracked angle to the target.
-- Compensates for the gear ratio: target_cannon_angle * config.gearRatio = gearshift_angle_to_rotate.
-- This function is blocking and waits for the gearshift to stop moving.
-- @param targetCannonAngle The desired absolute yaw angle in degrees (for the cannon).
-- @return boolean True if the command was sent successfully and completed, false otherwise.
local function setYaw(targetCannonAngle)
    if not yawGearshift then
        print("Yaw Gearshift not initialized. Cannot set yaw.")
        return false
    end

    -- Adjust target angle by the cannon's fixed orientation offset
    local adjustedTargetAngle = normalizeAngle(targetCannonAngle - config.defaultYawOffset)

    local deltaCannonAngle = adjustedTargetAngle - currentYaw
    -- Adjust deltaCannonAngle for the shortest path if crossing the -180/180 boundary
    if math.abs(deltaCannonAngle) > 180 then
        deltaCannonAngle = -(360 - math.abs(deltaCannonAngle)) * math.sign(deltaCannonAngle)
    end
    
    local modifier = 1 -- Default forward rotation for gearshift
    if deltaCannonAngle < 0 then
        modifier = -1 -- Reverse rotation for gearshift
    end

    local cannonAngleToRotate = math.abs(deltaCannonAngle)
    
    -- If the cannon angle to rotate is very small, we consider it already at target.
    if cannonAngleToRotate < 0.01 then -- Using a small epsilon for floating point comparison
        print(string.format("Yaw already at target %.2f degrees (actual cannon angle).", targetCannonAngle))
        currentYaw = adjustedTargetAngle -- Ensure internal state is precise
        return true
    end

    -- Apply the gear ratio to get the required gearshift rotation
    local gearshiftAngleToRotate = cannonAngleToRotate * config.gearRatio
    
    -- The 'rotate' method expects a positive integer angle.
    -- Round to the nearest integer, ensuring a minimum of 1 if rotation is needed.
    local roundedGearshiftAngleToRotate = math.max(1, math.floor(gearshiftAngleToRotate + 0.5)) 

    print(string.format("Setting yaw from %.2f (relative) to %.2f (relative) (rotating gearshift by %d with modifier %d)...",
                        currentYaw, adjustedTargetAngle, roundedGearshiftAngleToRotate, modifier))
    
    yawGearshift.rotate(roundedGearshiftAngleToRotate, modifier) -- No return value expected from rotate()

    -- Wait for the gearshift to finish rotating (blocking call)
    while yawGearshift.isRunning() do
        os.sleep(0.1) -- Small delay to avoid busy-waiting
    end
    print("Yaw Gearshift stopped.")

    currentYaw = adjustedTargetAngle -- Update internal state after physical movement is done
    return true
end

--- Sets the pitch (vertical) angle of the cannon using the Sequenced Gearshift.
-- Compensates for the gear ratio. Clamps the target angle to physical min/max pitch.
-- This function is blocking and waits for the gearshift to stop moving.
-- @param targetCannonAngle The desired absolute pitch angle in degrees (for the cannon).
-- @return boolean True if the command was sent successfully and completed, false otherwise.
local function setPitch(targetCannonAngle)
    if not pitchGearshift then
        print("Pitch Gearshift not initialized. Cannot set pitch.")
        return false
    end

    -- Clamp pitch angle to the configured physical range
    targetCannonAngle = math.max(config.minPitch, math.min(config.maxPitch, targetCannonAngle))

    local deltaCannonAngle = targetCannonAngle - currentPitch
    
    local modifier = 1
    if deltaCannonAngle < 0 then
        modifier = -1
    end

    local cannonAngleToRotate = math.abs(deltaCannonAngle)

    if cannonAngleToRotate < 0.01 then
        print(string.format("Pitch already at target %.2f degrees (actual cannon angle).", targetCannonAngle))
        currentPitch = targetCannonAngle
        return true
    end

    -- Apply the gear ratio to get the required gearshift rotation
    local gearshiftAngleToRotate = cannonAngleToRotate * config.gearRatio

    local roundedGearshiftAngleToRotate = math.max(1, math.floor(gearshiftAngleToRotate + 0.5))

    print(string.format("Setting pitch from %.2f to %.2f (rotating gearshift by %d with modifier %d)...",
                        currentPitch, targetCannonAngle, roundedGearshiftAngleToRotate, modifier))
    
    pitchGearshift.rotate(roundedGearshiftAngleToRotate, modifier) -- No return value expected from rotate()

    -- Wait for the gearshift to finish rotating (blocking call)
    while pitchGearshift.isRunning() do
        os.sleep(0.1) -- Small delay to avoid busy-waiting
    end
    print("Pitch Gearshift stopped.")

    currentPitch = targetCannonAngle -- Update internal state after physical movement is done
    return true
end

--- Resets the program's internal angle tracking and physically resets the cannon.
-- This function will toggle the 'build' signal off then on, which you've stated
-- causes the cannon to reset to the default position.
local function resetCannon()
    print("Performing full cannon reset sequence...")
    -- First, set the build signal OFF (to un-build/reset state)
    setBundledState(config.buildSignalColor, false)
    isCannonBuilt = false
    os.sleep(1.0) -- Give Minecraft/Create time to process the un-build/reset

    -- Then, set the build signal ON (to build state, which defaults position)
    setBundledState(config.buildSignalColor, true)
    isCannonBuilt = true
    os.sleep(1.0) -- Give Minecraft/Create time to process the build

    -- Reset internal angle tracking based on the cannon's default orientation
    currentYaw = normalizeAngle(0.0) -- Relative to defaultYawOffset, so 0 is the baseline
    currentPitch = 0.0 -- Assuming 0 pitch is default for reset

    print(string.format("Cannon reset sequence complete. Internal angles reset to Yaw: %.2f (relative), Pitch: %.2f.", currentYaw, currentPitch))
    print("Ensure your physical cannon is aligned with its default (initial) orientation.")
    sendStatus(0, "reset_complete") -- Broadcast reset completion to all connected clients
end

--- Toggles the cannon's 'built' state (on/off).
-- If the cannon is currently built, it will un-build it. If not built, it will build it.
-- Toggling from un-built to built should reset its position.
local function toggleBuildState()
    if isCannonBuilt then
        print("Un-building cannon (Black Bundled Cable channel OFF)...")
        setBundledState(config.buildSignalColor, false)
        isCannonBuilt = false
        print("Cannon un-built.")
        sendStatus(0, "unbuilt")
    else
        print("Building cannon (Black Bundled Cable channel ON)...")
        setBundledState(config.buildSignalColor, true)
        isCannonBuilt = true
        print("Cannon built.")
        -- When building, we assume it snaps to default position
        currentYaw = normalizeAngle(0.0) -- Relative to defaultYawOffset, so 0 is the baseline
        currentPitch = 0.0
        print(string.format("Internal angles reset to Yaw: %.2f (relative), Pitch: %.2f after building.", currentYaw, currentPitch))
        sendStatus(0, "built")
    end
end

--- Triggers the cannon to fire.
-- This sends a brief pulse on the red bundled cable channel.
-- @return boolean True if the signal was sent.
local function fireCannon()
    print("Activating cannon fire signal (Red Bundled Cable channel)...")
    pulseBundled(config.fireSignalColor)
    print("Fire signal sent. Expect explosion!")
    sendStatus(0, "fired")
    return true
end

--- Calculates the required yaw and pitch to hit a target.
-- This is where the physics-based trajectory calculation happens.
-- Uses the provided physics parameters from the config.
-- @param targetX Target X coordinate.
-- @param targetY Target Y coordinate.
-- @param targetZ Target Z coordinate.
-- @return table A table {yaw = angle, pitch = angle} if successful, nil otherwise.
local function calculateTrajectory(targetX, targetY, targetZ)
    local cannonLoc = getCannonLocation()
    if not cannonLoc then
        print("Cannot calculate trajectory: Cannon location unknown.")
        sendStatus(0, "error|location_unknown")
        return nil
    end

    local dx = targetX - cannonLoc.x
    local dy = targetY - cannonLoc.y
    local dz = targetZ - cannonLoc.z

    print(string.format("Target relative to cannon (DX:%.2f, DY:%.2f, DZ:%.2f)", dx, dy, dz))

    -- Yaw Calculation:
    -- Standard Minecraft convention (0=North, +X=East, +Z=South)
    -- atan2(y, x) returns angle in radians from positive x-axis counter-clockwise.
    -- For Minecraft yaw (0=North, clockwise):
    -- We want angle from +Z (North) increasing clockwise, which maps to math.atan2(dx, dz) then adjust.
    local calculatedYaw = math.deg(math.atan2(dx, dz))
    -- Adjust for 0 degrees being North and increasing clockwise:
    calculatedYaw = 90 - calculatedYaw -- 90 is usually +X (East), so subtract from 90 to get angle from +Z (North)
    calculatedYaw = normalizeAngle(calculatedYaw) -- Ensure it's in -180 to 180 range for setYaw

    -- Pitch Calculation: Projectile motion in 2D (horizontal distance vs vertical distance)
    local horizontalDistance = math.sqrt(dx^2 + dz^2)
    
    local calculatedPitch = nil -- Will store the pitch in degrees

    if horizontalDistance < 0.1 then -- Too close, pitch straight up or down (handle edge case)
        if dy > 0 then calculatedPitch = 90.0 else calculatedPitch = config.minPitch end -- Or a negative pitch if cannon allows
    else
        -- Check if projectileVelocity is set to a non-zero value
        if config.projectileVelocity == 0 then
            print("Error: projectileVelocity is 0. Cannot calculate trajectory with physics. Please set 'config.projectileVelocity'.")
            sendStatus(0, "error|no_projectile_velocity")
            return nil
        end

        -- Quadratic equation for tan(theta) from projectile motion:
        -- A * tan(theta)^2 + B * tan(theta) + C = 0
        local A_quad = config.gravity * horizontalDistance^2 / (2 * config.projectileVelocity^2)
        local B_quad = -horizontalDistance
        local C_quad = dy + A_quad -- Corrected C term from derivation

        local discriminant = B_quad^2 - 4 * A_quad * C_quad

        if discriminant < 0 then
            print("Error: Target unreachable with current velocity/gravity. Try adjusting target or velocity.")
            sendStatus(0, "error|unreachable_target")
            return nil
        else
            local tan_theta_1 = (-B_quad + math.sqrt(discriminant)) / (2 * A_quad)
            local tan_theta_2 = (-B_quad - math.sqrt(discriminant)) / (2 * A_quad)

            local pitch1 = math.deg(math.atan(tan_theta_1))
            local pitch2 = math.deg(math.atan(tan_theta_2))

            local validPitches = {}
            if pitch1 >= config.minPitch and pitch1 <= config.maxPitch then
                table.insert(validPitches, pitch1)
            end
            if pitch2 >= config.minPitch and pitch2 <= config.maxPitch then
                table.insert(validPitches, pitch2)
            end

            if #validPitches == 0 then
                print("Error: No valid pitch found within cannon's physical limits for this trajectory.")
                sendStatus(0, "error|no_valid_pitch")
                return nil
            elseif #validPitches == 1 then
                calculatedPitch = validPitches[1]
            else -- Two valid pitches, prefer the lower (flatter) trajectory
                calculatedPitch = math.min(validPitches[1], validPitches[2])
            end
        end
    end

    print(string.format("Calculated Trajectory (Cannon Angles): Yaw: %.2f, Pitch: %.2f", calculatedYaw, calculatedPitch))
    sendStatus(0, string.format("calculated_angles|%.2f|%.2f", calculatedYaw, calculatedPitch))
    return {yaw = calculatedYaw, pitch = calculatedPitch}
end

--- Processes a command string (from console or Rednet).
-- This separates the command logic from the input method.
-- @param fullCommandString The raw string containing the command and arguments.
-- @param senderID The ID of the sender (for Rednet commands), or nil for console.
local function processCommand(fullCommandString, senderID)
    local args = {}
    for s in string.gmatch(fullCommandString, "%S+") do
        table.insert(args, s)
    end
    local command = args[1] and string.lower(args[1])

    term.clear() -- Clear the entire screen before printing new output
    term.setCursorPos(1, 1) -- Move cursor to top-left after clearing

    if command == "help" then
        print("Commands:")
        print("  id                           - Display this computer's ID.")
        print("  location                     - Get current cannon GPS location (with configured offsets).")
        print("  getangles                    - Display the program's current tracked yaw and pitch angles.")
        print("  setyaw <angle>               - Set the absolute yaw angle of the cannon (degrees, -180 to 180).")
        print("  setpitch <angle>             - Set the absolute pitch angle of the cannon (degrees, clamped " .. config.minPitch .. "-" .. config.maxPitch .. ").")
        print("  aim <x> <y> <z>              - Calculate and set yaw/pitch to hit target XYZ coordinates.")
        print("  fire                         - Trigger the cannon to fire (via Red bundled cable).")
        print("  togglebuild                  - Toggle cannon build/un-build state (via Black bundled cable).")
        print("  resetcannon                  - Perform a full reset sequence (un-build -> build) which aligns cannon to default.")
        print("  exit                         - Exit the program.")
    elseif command == "id" then
        print("This Cannon Computer's ID is: " .. cannonComputerID)
    elseif command == "location" then
        local loc = getCannonLocation()
        if loc then
            print(string.format("Cannon Location (with offsets): X:%.2f Y:%.2f Z:%.2f", loc.x, loc.y, loc.z))
        end
    elseif command == "getangles" then
        print(string.format("Current Program-Tracked Angles: Yaw: %.2f (relative to default), Pitch: %.2f", currentYaw, currentPitch))
    elseif command == "setyaw" then
        local angle = tonumber(args[2])
        if angle ~= nil then
            setYaw(angle)
        else
            print("Usage: setyaw <angle> (angle must be a number)")
        end
    elseif command == "setpitch" then
        local angle = tonumber(args[2])
        if angle ~= nil then
            setPitch(angle)
        else
            print("Usage: setpitch <angle> (angle must be a number)")
        end
    elseif command == "aim" then
        local targetX = tonumber(args[2])
        local targetY = tonumber(args[3])
        local targetZ = tonumber(args[4])
        if targetX and targetY and targetZ then
            print(string.format("Attempting to aim at target: X:%d Y:%d Z:%d", targetX, targetY, targetZ))
            if senderID then sendStatus(senderID, "status|Aiming cannon...") end
            local angles = calculateTrajectory(targetX, targetY, targetZ)
            if angles then
                if setYaw(angles.yaw) and setPitch(angles.pitch) then
                    print("Cannon aimed successfully!")
                    if senderID then sendStatus(senderID, "status|Aimed_successfully") end
                else
                    print("Failed to set cannon angles.")
                    if senderID then sendStatus(senderID, "error|aim_failed") end
                end
            else
                print("Could not calculate trajectory.")
                -- Specific error already sent by calculateTrajectory
            end
        else
            print("Usage: aim <x> <y> <z> (coordinates must be numbers)")
            if senderID then sendStatus(senderID, "error|invalid_aim_format") end
        end
    elseif command == "fire" then
        fireCannon()
        if senderID then sendStatus(senderID, "status|Fired_cannon") end
    elseif command == "togglebuild" then
        toggleBuildState()
        -- Status update already handled by toggleBuildState
    elseif command == "resetcannon" then
        resetCannon()
        -- Status update already handled by resetCannon
    elseif command == "exit" then
        print("Exiting program. Goodbye!")
        rednet.close(config.modemSide)
        os.reboot() -- Reboot to clear program state for clean restart
    else
        print("Unknown command. Type 'help' for available commands.")
        if senderID then sendStatus(senderID, "error|unknown_command") end
    end
    -- Redraw the prompt at the bottom after command output
    drawPrompt()
end

--- Main program execution loop.
local function main()
    -- Attempt to initialize peripherals. Exit if critical ones are missing.
    if not initializePeripherals() then
        return -- Program stops if peripherals cannot be wrapped
    end

    -- --- CANNON RESET ON BOOT ---
    print("\n--- Performing automatic cannon reset on boot ---")
    resetCannon() -- Ensure cannon is in a known, default state on startup.

    -- Initialize build state by reading the bundled cable on startup
    isCannonBuilt = (bit.band(redstone.getBundledOutput(config.bundledCableSide), config.buildSignalColor) ~= 0)

    term.clear() -- Clear initial startup messages
    term.setCursorPos(1,1)

    print("\n--- Cannon Control Program Initialized ---")
    print("Cannon Computer ID: " .. cannonComputerID)
    print("Current tracked Yaw (relative to cannon's default): " .. string.format("%.2f", currentYaw) .. " degrees")
    print("Current tracked Pitch: " .. string.format("%.2f", currentPitch) .. " degrees")
    print("Cannon Built Status: " .. (isCannonBuilt and "BUILT" or "NOT BUILT"))
    print("Type 'help' for available commands.")
    print("Waiting for commands from console or Rednet...")

    -- Draw initial prompt
    drawPrompt()

    -- Event loop to handle both console input and Rednet messages
    while true do
        local event, p1, p2, p3 = os.pullEvent()

        if event == "rednet_message" then
            processCommand(p2, p1) -- p1=senderID, p2=message
        elseif event == "char" then
            commandBuffer = commandBuffer .. p1 -- Append character
            drawPrompt()
        elseif event == "key" then
            if p1 == keys.enter then
                -- Process command, then clear buffer
                local commandToProcess = commandBuffer
                commandBuffer = ""
                processCommand(commandToProcess, nil) -- nil indicates console sender
            elseif p1 == keys.backspace then
                if #commandBuffer > 0 then
                    commandBuffer = string.sub(commandBuffer, 1, #commandBuffer - 1)
                    drawPrompt()
                end
            -- Add other key handling as needed
            end
        end
    end
end

-- Run the main program when the script is executed
main()
