-- Estas propiedades del Inspector enlazan el script con los objetos que creas.
-- Escribe sus nombres exactamente; no los traduzcas ni los repitas en otros objetos.
playerName = "Player"
doorName = "Door"
statusTextName = "Status Text"
-- Agrupa los datos de este acertijo bajo un mismo prefijo. Si lo cambias, el
-- script buscará otro punto de control; las claves guardadas no se renombrarán.
savePrefix = "two_room_relay"

-- Busca los nombres una sola vez en Start y reutiliza los ID en los callbacks.
-- El HUD se agrega en el siguiente capítulo, así que su ID puede no existir aún.
local playerId = -1
local doorId = -1
local statusId = -1
-- Reset devuelve al jugador a esta posición, no al último punto de control.
local startPosition = nil
-- Lleva el estado actual de la puerta por separado del progreso guardado:
-- puede permanecer abierta aunque la Memory Card no esté disponible.
local doorOpen = false
-- Estos cuatro valores forman el punto de control completo del tutorial.
local fields = { "x", "y", "z", "door_open" }

-- Separa las claves de este acertijo de las que usan otros sistemas en PlayerPrefs.
local function key(name)
    return savePrefix .. "." .. name
end

local function status(message)
    -- Conserva el diagnóstico completo en la Console, incluso antes de crear el HUD.
    Log.Print(message)
    if statusId >= 0 then
        -- Ajusta los mensajes ASCII del tutorial al límite de texto de PS1: 62 caracteres
        -- y un salto de línea. Dos líneas de hasta 32 caracteres caben en este HUD.
        local display = string.sub(message, 1, 62)
        if #display > 32 then
            display = string.sub(display, 1, 32) .. "\n" .. string.sub(display, 33)
        end
        Text.SetText(statusId, display)
    end
end

-- Detén las acciones y el guardado si falta un objeto enlazado por nombre.
-- Door sigue existiendo al desactivarse, así que una puerta abierta es válida.
local function valid_targets()
    if not GameObject.Exists(playerId) or not GameObject.Exists(doorId) then
        status("SETUP ERROR: CHECK PLAYER AND DOOR NAMES")
        return false
    end
    return true
end

-- Abrir esta puerta desactiva tanto su malla como el collider que bloquea el paso.
-- Door Trigger sigue activo porque es un GameObject independiente.
local function set_door(open)
    if not GameObject.SetActive(doorId, not open) then
        status("SETUP ERROR: DOOR IS UNAVAILABLE")
        return false
    end
    doorOpen = open
    return true
end

-- Conserva una copia de los valores existentes del tutorial por si falla una
-- operación. Respeta sus tipos: las coordenadas son float y door_open es int.
local function capture_preferences()
    local previous = {}
    for _, name in ipairs(fields) do
        if PlayerPrefs.HasKey(key(name)) then
            previous[name] = name == "door_open"
                and PlayerPrefs.GetInt(key(name), 0)
                or PlayerPrefs.GetFloat(key(name), 0.0)
        end
    end
    return previous
end

-- Borra de memoria solo las claves del acertijo; conserva los demás datos.
-- Quien llama esta función debe ejecutar Save para guardar el reinicio.
local function clear_preferences()
    for _, name in ipairs(fields) do
        -- Si la clave no existe, devuelve false, pero ya no hay nada que borrar.
        PlayerPrefs.DeleteKey(key(name))
    end
end

-- Deshaz los cambios en memoria si falla una operación. Primero elimina las
-- claves parciales para no reutilizar un punto incompleto ni ocupar más entradas.
local function restore_preferences(previous)
    clear_preferences()
    for _, name in ipairs(fields) do
        local value = previous[name]
        if value ~= nil then
            if name == "door_open" then
                PlayerPrefs.SetInt(key(name), value)
            else
                PlayerPrefs.SetFloat(key(name), value)
            end
        end
    end
end

local function save_checkpoint(hostId, message)
    if not valid_targets() then return false end
    local position = Transform.GetPosition(playerId)
    if position == nil then
        status("SAVE FAILED: PLAYER IS UNAVAILABLE")
        return false
    end
    -- Prepara en memoria la posición y el estado de la puerta como un solo punto
    -- de control. Si falla un valor, detén la escritura para no guardar datos parciales.
    local previous = capture_preferences()
    local written = PlayerPrefs.SetFloat(key("x"), position.x)
        and PlayerPrefs.SetFloat(key("y"), position.y)
        and PlayerPrefs.SetFloat(key("z"), position.z)
        and PlayerPrefs.SetInt(key("door_open"), doorOpen and 1 or 0)
    -- SetFloat y SetInt no guardan en el dispositivo. Save escribe los datos
    -- en el archivo local del editor o en la Memory Card del puerto 1 de PS1.
    if not written or not PlayerPrefs.Save() then
        -- Obtén el motivo antes de restaurar valores, pues eso vuelve a usar la API
        -- de guardado. Mantén la puerta como está para permitir reintentar con Save.
        local reason = SaveData.GetLastError()
        restore_preferences(previous)
        status("SAVE FAILED - RETRY SAVE: " .. reason)
        return false
    end
    status(message)
    -- Confirma el éxito con el Audio Source de Door Trigger, donde vive el script.
    -- El siguiente capítulo lo agrega; no necesitas otro objeto para el audio.
    AudioSource.Play(hostId)
    return true
end

local function teleport(position)
    if not Transform.SetPosition(playerId, position.x, position.y, position.z) then
        status("LOAD FAILED: INVALID PLAYER POSITION")
        return false
    end
    -- Evita conservar la velocidad de una caída o salto al recuperar la posición.
    CharacterController.SetVelocityY(playerId, 0.0)
    return true
end

local function load_checkpoint()
    if not valid_targets() then return false end
    local previous = capture_preferences()
    -- Lee el último punto guardado en el dispositivo, no los cambios sin guardar.
    -- PlayerPrefs y SaveData comparten el mismo almacenamiento de datos.
    if not SaveData.Reload() then
        local reason = SaveData.GetLastError()
        -- Conserva los valores anteriores si no se puede leer el dispositivo;
        -- aún no se ha movido al jugador ni cambiado la puerta.
        restore_preferences(previous)
        status("LOAD FAILED: " .. reason)
        return false
    end
    -- Exige las cuatro claves antes de restaurar algo. La primera partida o un
    -- Reset exitoso no tienen punto de control; deja la escena como está.
    for _, name in ipairs(fields) do
        if not PlayerPrefs.HasKey(key(name)) then
            status("NO CHECKPOINT")
            return false
        end
    end
    -- Usa un valor fuera de rango si una coordenada no se puede leer, en lugar
    -- de sustituirla silenciosamente por una posición válida en el origen.
    local position = {
        x = PlayerPrefs.GetFloat(key("x"), 1e30),
        y = PlayerPrefs.GetFloat(key("y"), 1e30),
        z = PlayerPrefs.GetFloat(key("z"), 1e30)
    }
    if not teleport(position) then return false end
    -- Restaura la puerta con el mismo punto de control que la posición del jugador.
    if not set_door(PlayerPrefs.GetInt(key("door_open"), 0) == 1) then return false end
    status("CHECKPOINT LOADED")
    return true
end

-- Enlaza los objetos al iniciar este comportamiento, no en cada cuadro.
-- UI.Find busca elementos de UI; GameObject.Find busca GameObjects de la escena.
function Start(gameObjectId)
    statusId = UI.Find(statusTextName)
    playerId = GameObject.Find(playerName)
    doorId = GameObject.Find(doorName)
    if not valid_targets() then return end
    -- Conserva la posición inicial en Room A para Reset. Aplica el estado guardado
    -- de la puerta; Square recuperará la posición guardada cuando el jugador lo pida.
    startPosition = Transform.GetPosition(playerId)
    set_door(PlayerPrefs.GetInt(key("door_open"), 0) == 1)
    -- Selecciona un botón inicial para que el usuario del mando active Save con Cross.
    local saveButton = UI.Find("Save Button")
    if saveButton >= 0 then Selectable.Select(saveButton) end
    status(doorOpen and "DOOR OPEN - SQUARE LOADS" or "WALK TO THE DOOR")
end

-- Carga una vez al presionar Square, no en cada cuadro mientras se mantiene
-- presionado. Leer el dispositivo repetidamente interrumpiría el juego.
function Update(gameObjectId, dt)
    if Input.GetButtonDown(Input.SQUARE) then load_checkpoint() end
end

function OnTriggerEnter(gameObjectId, otherGameObjectId)
    -- Solo el jugador activa la puerta. Volver a entrar cuando ya está abierta
    -- no debe sobrescribir el punto de control una y otra vez.
    if otherGameObjectId ~= playerId or doorOpen then return end
    if not valid_targets() or not set_door(true) then return end
    -- Abre el paso antes de guardar: la falta de tarjeta no debe bloquear Room B.
    -- Save permite reintentar con la puerta abierta y la posición actual del jugador.
    save_checkpoint(gameObjectId, "DOOR OPEN - CHECKPOINT SAVED")
end

-- Save Button llama aquí mediante Target Entity y Function. gameObjectId
-- identifica a Door Trigger, no al botón de UI que envió el evento.
function saveNow(gameObjectId, elementId, elementName, eventName)
    save_checkpoint(gameObjectId, "CHECKPOINT SAVED")
end

-- Reset Button borra solo el progreso del tutorial. Conserva el estado de la
-- escena hasta confirmar que ese reinicio se haya guardado.
function resetProgress(gameObjectId, elementId, elementName, eventName)
    if not valid_targets() or startPosition == nil then return end
    local previous = capture_preferences()
    clear_preferences()
    if not PlayerPrefs.Save() then
        -- Obtén el error antes de restaurar los valores para explicar por qué falló Reset.
        local reason = SaveData.GetLastError()
        restore_preferences(previous)
        status("RESET FAILED: " .. reason)
        return
    end
    -- Regresa a Room A antes de cerrar la puerta para no dejar atrapado al jugador.
    if not teleport(startPosition) then return end
    if not set_door(false) then return end
    status("PROGRESS RESET")
end
