// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use thiserror::Error;

use chrono::{Datelike, Timelike, Utc};
use crc::Crc;

const WRITE_USR_ACCESS: [u8; 4] = [0x30, 0x01, 0xa0, 0x01];
const WRITE_CRC_REG: [u8; 4] = [0x30, 0x00, 0x00, 0x01];
const NOOP: [u8; 4] = [0x20, 0x00, 0x00, 0x00];

#[derive(Error, Debug, Serialize, Deserialize)]
pub enum Error {
    #[error("Command {0:#x?} not found in bitstream")]
    CommandNotFound(u32),
}

fn find_cmd(bitstream: &[u8], cmd: &[u8; 4]) -> Result<usize> {
    Ok(bitstream
        .chunks(4)
        .enumerate()
        .find(|(_, op)| *op == cmd)
        .ok_or_else(|| Error::CommandNotFound(u32::from_be_bytes(*cmd)))?
        .0
        * 4)
}

/// Searches for the following pattern in the bitstream
///
/// 0x30000001 (write the following word to the CRC register and check)
/// 0xXXXXXXXX (value to write to the CRC register)
/// 0x20000000 (NOOP)
/// 0x20000000 (NOOP)
///
/// and replaces it with
///
/// 0x20000000 (NOOP)
/// 0x20000000 (NOOP)
/// 0x20000000 (NOOP)
/// 0x20000000 (NOOP)
///
/// to remove the CRC check during FPGA configuration.
fn remove_crc(bitstream: &mut [u8]) {
    let mut start = 0;
    while start < bitstream.len() {
        match find_cmd(&bitstream[start..], &WRITE_CRC_REG) {
            Ok(mut i) => {
                i += start;
                if (i + 16) <= bitstream.len()
                    && bitstream[i + 8..i + 12] == NOOP
                    && bitstream[i + 12..i + 16] == NOOP
                {
                    log::info!("Replaced WRITE_CRC_REG command at {:#x} with NOOP", i);
                    bitstream[i..i + 4].copy_from_slice(&NOOP);
                    bitstream[i + 4..i + 8].copy_from_slice(&NOOP);
                }
                start = i + 16;
            }
            _ => return,
        }
    }
}

pub fn usr_access_get(bitstream: &[u8]) -> Result<u32> {
    let i = find_cmd(bitstream, &WRITE_USR_ACCESS)?;
    if (i + 8) > bitstream.len() {
        return Err(Error::CommandNotFound(u32::from_be_bytes(WRITE_USR_ACCESS)).into());
    }
    let operand = &bitstream[i + 4..i + 8];
    let usr_access = u32::from_be_bytes(operand.try_into()?);
    log::info!("Bitstream file USR_ACCESS value: {:#x}", usr_access);
    Ok(usr_access)
}

/// Returns a 32-bit timestamp suitable to be used as a USR_ACCESS value:
///
/// |-------+-------+-------+-------------------------+-------+--------+--------|
/// | Bits: | 31:27 | 26:23 | 22:17                   | 16:12 | 11:6   | 5:0    |
/// |-------+-------+-------+-------------------------+-------+--------+--------|
/// | Data: | Day   | Month | Year (since 2000, 0-63) | Hour  | Minute | Second |
/// |-------+-------+-------+-------------------------+-------+--------+--------|
///
/// From
/// <https://www.xilinx.com/content/dam/xilinx/support/documents/application_notes/xapp1232-bitstream-id-with-usr_access.pdf>
pub fn usr_access_timestamp() -> u32 {
    let now = Utc::now();
    now.day() << 27
        | now.month() << 23
        | now.year_ce().1.checked_sub(2000u32).unwrap() << 17
        | now.hour() << 12
        | now.minute() << 6
        | now.second()
}

/// Returns the crc32 hash of the bitstream to be used as a USR_ACCESS value
pub fn usr_access_crc32(bitstream: &mut [u8]) -> Result<u32> {
    usr_access_set(bitstream, 0)?; // Clear usr_access before hashing
    Ok(Crc::<u32>::new(&crc::CRC_32_ISO_HDLC).checksum(bitstream))
}

pub fn usr_access_set(bitstream: &mut [u8], val: u32) -> Result<()> {
    let i = find_cmd(bitstream, &WRITE_USR_ACCESS)?;
    log::info!(
        "Bitstream file old USR_ACCESS value: {:#x}",
        u32::from_be_bytes(bitstream[i + 4..i + 8].try_into()?)
    );
    bitstream[i + 4..i + 8].copy_from_slice(&val.to_be_bytes());
    log::info!("Bitstream file new USR_ACCESS value: {:#x}", val);
    remove_crc(bitstream);
    Ok(())
}
