fn parse_phys(node_type: NodeType<'_>) -> Result<Vec<PhyRef>, OnProbeError> {
    let node = node_type.as_node();
    let Some(prop) = node.get_property("phys") else {
        return Ok(Vec::new());
    };
    let cells = prop.get_u32_iter().collect::<Vec<_>>();
    if cells.is_empty() {
        return Ok(Vec::new());
    }
    let phy_names = prop_str_list(node, "phy-names");
    let mut refs = Vec::new();
    let mut index = 0;
    let mut offset = 0;
    while offset < cells.len() {
        let phandle = Phandle::from(cells[offset]);
        offset += 1;
        let specifier_cells = phy_cells(phandle)?;
        if offset + specifier_cells > cells.len() {
            return Err(OnProbeError::other(format!(
                "[{}] has truncated phys entry for phandle {phandle:?}",
                node.name()
            )));
        }
        let specifier = cells[offset..offset + specifier_cells].to_vec();
        offset += specifier_cells;
        refs.push(PhyRef {
            phandle,
            specifier,
            name: phy_names.get(index).cloned(),
        });
        index += 1;
    }
    Ok(refs)
}

fn init_phys(host_node: NodeType<'_>, phys: &[PhyRef]) -> Result<(), OnProbeError> {
    if phys.is_empty() {
        warn!(
            "Rockchip RK3588 PCIe host {} has no phys property",
            host_node.name()
        );
        return Ok(());
    }
    let fdt = live_fdt()?;
    for phy_ref in phys {
        let phy = fdt.get_by_phandle(phy_ref.phandle).ok_or_else(|| {
            OnProbeError::other(format!(
                "PCIe PHY phandle {:?} for {} not found",
                phy_ref.phandle,
                host_node.name()
            ))
        })?;
        if is_compatible(phy.as_node(), "rockchip,rk3588-pcie3-phy") {
            let resources = parse_pcie3_phy(phy)?;
            init_pcie3_phy(&resources)?;
        } else if is_compatible(phy.as_node(), "rockchip,rk3588-naneng-combphy") {
            let Some(&phy_type) = phy_ref.specifier.first() else {
                return Err(OnProbeError::other(format!(
                    "RK3588 combphy {} referenced by {} has no PHY type specifier",
                    phy.name(),
                    host_node.name()
                )));
            };
            if phy_type != PHY_TYPE_PCIE {
                return Err(OnProbeError::other(format!(
                    "RK3588 combphy {} referenced by {} is type {}, expected PCIe",
                    phy.name(),
                    host_node.name(),
                    phy_type
                )));
            }
            let resources = parse_combphy(phy)?;
            init_combphy(&resources)?;
        } else {
            return Err(OnProbeError::other(format!(
                "unsupported RK3588 PCIe PHY {} referenced by {}",
                phy.name(),
                host_node.name()
            )));
        }
    }
    Ok(())
}

fn parse_pcie3_phy(phy: NodeType<'_>) -> Result<Pcie3PhyResources, OnProbeError> {
    let node = phy.as_node();
    let reg = phy
        .regs()
        .into_iter()
        .next()
        .ok_or_else(|| OnProbeError::other(format!("[{}] has no reg", phy.name())))?;
    let phy_grf = prop_phandle(node, "rockchip,phy-grf")
        .ok_or_else(|| OnProbeError::other(format!("[{}] has no rockchip,phy-grf", phy.name())))?;
    let mut pcie30_phymode =
        prop_u32(node, "rockchip,pcie30-phymode").unwrap_or(RK3588_PCIE3PHY_DEFAULT_MODE);
    if pcie30_phymode > RK3588_PCIE3PHY_DEFAULT_MODE {
        pcie30_phymode = RK3588_PCIE3PHY_DEFAULT_MODE;
    }
    Ok(Pcie3PhyResources {
        name: phy.name().to_string(),
        reg,
        phy_grf,
        pipe_grf: prop_phandle(node, "rockchip,pipe-grf"),
        pcie30_phymode,
        clocks: clock_specs_for_node(phy),
        resets: parse_resets(phy)?,
    })
}

fn init_pcie3_phy(phy: &Pcie3PhyResources) -> Result<(), OnProbeError> {
    let _mmio = RegMmio::map_reg(phy.reg)?;
    let phy_grf = RegMmio::map_phandle(phy.phy_grf, "rk3588-pcie3-phy rockchip,phy-grf")?;
    let pipe_grf = phy
        .pipe_grf
        .map(|phandle| RegMmio::map_phandle(phandle, "rk3588-pcie3-phy rockchip,pipe-grf"))
        .transpose()?;

    enable_clocks(&phy.clocks)?;
    for reset in &phy.resets {
        rk3588_reset_assert(reset.id).map_err(|err| {
            OnProbeError::other(format!(
                "failed to assert RK3588 PCIe3 PHY reset {:?} ({:#x}): {err}",
                reset.name, reset.id
            ))
        })?;
    }
    axklib::time::busy_wait(Duration::from_micros(1));

    phy_grf.write32(
        RK3588_PCIE3PHY_CMN_CON0,
        (0x7 << BIT_WRITEABLE_SHIFT) | phy.pcie30_phymode,
    );
    if let Some(pipe_grf) = pipe_grf.as_ref() {
        let mode = phy.pcie30_phymode & 3;
        if mode != 0 {
            pipe_grf.write32(PHP_GRF_PCIESEL_CON, (mode << BIT_WRITEABLE_SHIFT) | mode);
        }
    }
    phy_grf.write32(RK3588_PCIE3PHY_CMN_CON0, (1 << 24) | (1 << 8));

    for reset in &phy.resets {
        rk3588_reset_deassert(reset.id).map_err(|err| {
            OnProbeError::other(format!(
                "failed to deassert RK3588 PCIe3 PHY reset {:?} ({:#x}): {err}",
                reset.name, reset.id
            ))
        })?;
    }
    poll_pcie3_sram_ready(&phy_grf, RK3588_PCIE3PHY_PHY0_STATUS1, &phy.name)?;
    poll_pcie3_sram_ready(&phy_grf, RK3588_PCIE3PHY_PHY1_STATUS1, &phy.name)?;
    info!(
        "RK3588 PCIe3 PHY {} initialized, mode={}",
        phy.name, phy.pcie30_phymode
    );
    Ok(())
}

fn poll_pcie3_sram_ready(phy_grf: &RegMmio, offset: usize, name: &str) -> Result<(), OnProbeError> {
    for _ in 0..500 {
        if phy_grf.read32(offset) & PCIE3PHY_SRAM_INIT_DONE != 0 {
            return Ok(());
        }
        axklib::time::busy_wait(Duration::from_micros(1));
    }
    Err(OnProbeError::other(format!(
        "RK3588 PCIe3 PHY {name} SRAM ready timeout at GRF offset {offset:#x}"
    )))
}

fn parse_combphy(phy: NodeType<'_>) -> Result<CombphyResources, OnProbeError> {
    let node = phy.as_node();
    let reg = phy
        .regs()
        .into_iter()
        .next()
        .ok_or_else(|| OnProbeError::other(format!("[{}] has no reg", phy.name())))?;
    let pipe_grf = prop_phandle(node, "rockchip,pipe-grf")
        .ok_or_else(|| OnProbeError::other(format!("[{}] has no rockchip,pipe-grf", phy.name())))?;
    let pipe_phy_grf = prop_phandle(node, "rockchip,pipe-phy-grf").ok_or_else(|| {
        OnProbeError::other(format!("[{}] has no rockchip,pipe-phy-grf", phy.name()))
    })?;
    let pcie1ln_sel_bits = node
        .get_property("rockchip,pcie1ln-sel-bits")
        .map(|prop| {
            let vals = prop.get_u32_iter().collect::<Vec<_>>();
            if vals.len() != 4 {
                return Err(OnProbeError::other(format!(
                    "[{}] malformed rockchip,pcie1ln-sel-bits",
                    phy.name()
                )));
            }
            Ok([vals[0], vals[1], vals[2], vals[3]])
        })
        .transpose()?;

    Ok(CombphyResources {
        name: phy.name().to_string(),
        reg,
        pipe_grf,
        pipe_phy_grf,
        pcie1ln_sel_bits,
        refclk_rate: assigned_clock_rate(node).unwrap_or(100_000_000),
        clocks: clock_specs_for_node(phy),
        resets: parse_resets(phy)?,
    })
}

fn init_combphy(phy: &CombphyResources) -> Result<(), OnProbeError> {
    let mmio = RegMmio::map_reg(phy.reg)?;
    let pipe_grf = RegMmio::map_phandle(phy.pipe_grf, "rk3588-naneng-combphy rockchip,pipe-grf")?;
    let phy_grf = RegMmio::map_phandle(
        phy.pipe_phy_grf,
        "rk3588-naneng-combphy rockchip,pipe-phy-grf",
    )?;

    assert_resets(&phy.resets)?;
    enable_clocks(&phy.clocks)?;
    if let Some([offset, start, end, value]) = phy.pcie1ln_sel_bits {
        let mask = bit_range_mask(start, end)?;
        pipe_grf.write32(
            offset as usize,
            (mask << BIT_WRITEABLE_SHIFT) | (value << start),
        );
    }
    combphy_update(&mmio, 0x7c, bit_range_mask(4, 5)?, 1 << 4);
    combphy_param_write(&phy_grf, 0x0000, 0, 15, 0x1000)?;
    combphy_param_write(&phy_grf, 0x0004, 0, 15, 0x0000)?;
    combphy_param_write(&phy_grf, 0x0008, 0, 15, 0x0101)?;
    combphy_param_write(&phy_grf, 0x000c, 0, 15, 0x0200)?;

    match phy.refclk_rate {
        24_000_000 => init_combphy_refclk_24m(&mmio, &phy_grf)?,
        25_000_000 => combphy_param_write(&phy_grf, 0x0004, 13, 14, 0x01)?,
        100_000_000 => init_combphy_refclk_100m(&mmio, &phy_grf)?,
        rate => {
            return Err(OnProbeError::other(format!(
                "RK3588 combphy {} unsupported refclk rate {}",
                phy.name, rate
            )));
        }
    }

    combphy_update(&mmio, 0x19 << 2, 1 << 5, 1 << 5);
    deassert_resets(&phy.resets)?;
    info!(
        "RK3588 Naneng combphy {} initialized for PCIe, refclk={}Hz",
        phy.name, phy.refclk_rate
    );
    Ok(())
}

fn init_combphy_refclk_24m(mmio: &RegMmio, phy_grf: &RegMmio) -> Result<(), OnProbeError> {
    combphy_param_write(phy_grf, 0x0004, 13, 14, 0x00)?;
    combphy_update(mmio, 0x20 << 2, bit_range_mask(2, 4)?, 0x4 << 2);
    mmio.write32(0x1b << 2, 0x00);
    mmio.write32(0x0a << 2, 0x90);
    mmio.write32(0x0b << 2, 0x02);
    mmio.write32(0x0d << 2, 0x57);
    mmio.write32(0x0f << 2, 0x5f);
    Ok(())
}

fn init_combphy_refclk_100m(mmio: &RegMmio, phy_grf: &RegMmio) -> Result<(), OnProbeError> {
    combphy_param_write(phy_grf, 0x0004, 13, 14, 0x02)?;
    mmio.write32(0x74, 0xc0);
    combphy_update(mmio, 0x20 << 2, bit_range_mask(2, 4)?, 0x4 << 2);
    mmio.write32(0x1b << 2, 0x4c);
    mmio.write32(0x0a << 2, 0x90);
    mmio.write32(0x0b << 2, 0x43);
    mmio.write32(0x0c << 2, 0x88);
    mmio.write32(0x0d << 2, 0x56);
    Ok(())
}

fn combphy_param_write(
    mmio: &RegMmio,
    offset: usize,
    start: u32,
    end: u32,
    value: u32,
) -> Result<(), OnProbeError> {
    let mask = bit_range_mask(start, end)?;
    mmio.write32(offset, (value << start) | (mask << BIT_WRITEABLE_SHIFT));
    Ok(())
}

fn combphy_update(mmio: &RegMmio, offset: usize, mask: u32, value: u32) {
    mmio.update32(offset, mask, value);
}

fn bit_range_mask(start: u32, end: u32) -> Result<u32, OnProbeError> {
    if start > end || end >= 32 {
        return Err(OnProbeError::other(format!(
            "invalid bit range {}..={}",
            start, end
        )));
    }
    let width = end - start + 1;
    Ok(if width == 32 {
        u32::MAX
    } else {
        ((1_u32 << width) - 1) << start
    })
}

fn assigned_clock_rate(node: &Node) -> Option<u32> {
    node.get_property("assigned-clock-rates")
        .and_then(|prop| prop.get_u32_iter().next())
}
