// SPDX-License-Identifier: GPL-2.0
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.

use kernel::prelude::*;

use kernel::{
    device,
    dma::Coherent,
    io::poll::read_poll_timeout,
    time::Delta,
    types::ScopeGuard, //
};

use crate::{
    falcon::{
        gsp::Gsp as GspEngine,
        Falcon, //
    },
    fb::FbSizes,
    firmware::gsp::GspFirmware,
    fsp::FmcBootArgs,
    gsp::{
        hal::{
            GspHal,
            UnloadBundle, //
        },
        Gsp,
        GspBootContext,
        GspFmcBootParams,
        GspFwWprMeta, //
    },
};

/// GSP falcon mailbox state, used to track lockdown release status.
struct GspMbox {
    mbox0: u32,
    mbox1: u32,
}

impl GspMbox {
    /// Reads both mailboxes from the GSP falcon.
    fn read(gsp_falcon: &Falcon<'_, GspEngine>) -> Self {
        Self {
            mbox0: gsp_falcon.read_mailbox0(),
            mbox1: gsp_falcon.read_mailbox1(),
        }
    }

    /// Combines mailbox0 and mailbox1 into a 64-bit address.
    fn combined_addr(&self) -> u64 {
        (u64::from(self.mbox1) << 32) | u64::from(self.mbox0)
    }

    /// Returns `true` if GSP lockdown has been released or a GSP-FMC error happened.
    ///
    /// Returns `true` both on successful lockdown release and on GSP-FMC-reported errors, since
    /// either condition should stop the poll loop.
    fn lockdown_released_or_error(
        &self,
        gsp_falcon: &Falcon<'_, GspEngine>,
        fmc_boot_params: &Coherent<GspFmcBootParams>,
    ) -> bool {
        // GSP-FMC normally clears the boot parameters address from the mailboxes early during
        // boot. If the address is still there, keep polling rather than treating it as an error.
        // Any other non-zero mailbox0 value is a GSP-FMC error code.
        if self.mbox0 != 0 {
            return self.combined_addr() != fmc_boot_params.dma_address();
        }

        !gsp_falcon.riscv_branch_privilege_lockdown()
    }
}

/// Waits for GSP lockdown to be released after FSP Chain of Trust.
fn wait_for_gsp_lockdown_release(
    dev: &device::Device<device::Bound>,
    gsp_falcon: &Falcon<'_, GspEngine>,
    fmc_boot_params: &Coherent<GspFmcBootParams>,
) -> Result {
    dev_dbg!(dev, "Waiting for GSP lockdown release\n");

    let mbox = read_poll_timeout(
        || {
            // While the PRIV target mask is still locked to FSP, GSP register and mailbox reads
            // are not meaningful. Wait until HWCFG2 says the CPU can read them.
            Ok(match gsp_falcon.priv_target_mask_released() {
                false => None,
                true => Some(GspMbox::read(gsp_falcon)),
            })
        },
        |mbox| match mbox {
            None => false,
            Some(mbox) => mbox.lockdown_released_or_error(gsp_falcon, fmc_boot_params),
        },
        Delta::from_millis(10),
        Delta::from_secs(30),
    )
    .inspect_err(|_| {
        dev_err!(dev, "GSP lockdown release timeout\n");
    })?
    .ok_or(EIO)?;

    // If polling stopped with a non-zero mailbox0, it was not the boot parameters address
    // anymore and therefore represents a GSP-FMC error code.
    if mbox.mbox0 != 0 {
        dev_err!(dev, "GSP-FMC boot failed (mbox: {:#x})\n", mbox.mbox0);
        return Err(EIO);
    }

    dev_dbg!(dev, "GSP lockdown released\n");
    Ok(())
}

struct FspUnloadBundle;

impl UnloadBundle for FspUnloadBundle {
    fn run(&self, ctx: &mut GspBootContext<'_, '_>) -> Result {
        // GSP falcon does most of the work of resetting, so just wait for it to finish.
        read_poll_timeout(
            || {
                // GSP register reads are not meaningful until the PRIV target mask is released.
                if !ctx.gsp_falcon.priv_target_mask_released() {
                    return Ok(false);
                }

                ctx.gsp_falcon.is_riscv_halted()
            },
            |&halted| halted,
            Delta::from_millis(10),
            Delta::from_secs(5),
        )
        .map(|_| ())
        .inspect_err(|_| dev_err!(ctx.dev(), "GSP falcon failed to halt\n"))
    }
}

struct Gh100;

impl GspHal for Gh100 {
    /// Boot GSP via FSP Chain of Trust (Hopper/Blackwell+ path).
    ///
    /// This path uses FSP to establish a chain of trust and boot GSP-FMC. FSP handles
    /// the GSP boot internally - no manual GSP reset/boot is needed.
    fn boot(
        &self,
        gsp: &Gsp,
        ctx: &mut GspBootContext<'_, '_>,
        gsp_fw: &GspFirmware,
    ) -> Result<Option<crate::gsp::UnloadBundle>> {
        let dev = ctx.dev();
        let chipset = ctx.chipset;
        let gsp_falcon = ctx.gsp_falcon;

        let fb_sizes = FbSizes::new(chipset, ctx.bar, ctx.vgpu.state())?;
        dev_dbg!(dev, "{:#x?}\n", fb_sizes);

        let wpr_meta =
            Coherent::init(dev, GFP_KERNEL, GspFwWprMeta::from_sizes(gsp_fw, &fb_sizes))?;
        let args = FmcBootArgs::new(dev, chipset, wpr_meta, &gsp.libos, false)?;

        let unload_bundle = crate::gsp::UnloadBundle(
            KBox::new(FspUnloadBundle, GFP_KERNEL)? as KBox<dyn UnloadBundle>
        );

        // Wait for the GSP RISC-V core to halt in case of error. We create this guard after `args`
        // to make sure that the boot args and the WPR metadata they own are kept alive until halt,
        // in case they are still being accessed.
        let mut unload_guard =
            ScopeGuard::new_with_data((unload_bundle, ctx), |(unload_bundle, ctx)| {
                let _ = unload_bundle.0.run(ctx);
            });

        let fsp = unload_guard.1.fsp.as_mut().ok_or(ENODEV)?;

        fsp.boot_fmc(dev, &fb_sizes, &args)?;

        // Wait for GSP-FMC to release the GSP lockdown, indicating that `args` is not accessed
        // anymore.
        wait_for_gsp_lockdown_release(dev, gsp_falcon, args.boot_params())?;

        Ok(Some(unload_guard.dismiss().0))
    }
}

const GH100: Gh100 = Gh100;
pub(super) const GH100_HAL: &dyn GspHal = &GH100;