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

use crate::otp::lc_state::LcSecded;
use crate::util::present::Present;

use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt::Write;

use anyhow::{anyhow, bail, ensure, Result};

use zerocopy::AsBytes;

enum ItemType {
    Bytes(Vec<u8>),
    Unvalued(usize),
}

/// The hex representation of an OTP item.
pub struct VmemItem {
    value: ItemType,
    offset: usize,
    name: String,
}

impl VmemItem {
    pub fn new(bytes: Vec<u8>, offset: usize, name: String) -> VmemItem {
        VmemItem {
            value: ItemType::Bytes(bytes),
            offset,
            name,
        }
    }

    pub fn new_unvalued(size: usize, offset: usize, name: String) -> VmemItem {
        VmemItem {
            value: ItemType::Unvalued(size),
            offset,
            name,
        }
    }

    pub fn 
(&self) -> usize {
        match &self.value {
            ItemType::Bytes(b) => b.len(),
            ItemType::Unvalued(size) => *size,
        }
    }
}

pub type DigestIV = u64;
pub type DigestCnst = u128;

/// Digest information for an OTP partition.
#[derive(PartialEq)]
pub enum DigestType {
    Unlocked,
    Software,
    Hardware(DigestIV, DigestCnst),
}

/// The hex representation of an OTP partition.
pub struct VmemPartition {
    /// Items associated with this partition.
    items: Vec<VmemItem>,
    /// The name of this partition.
    /// Used in annotations.
    name: String,
    /// The type of digest used for this partition.
    /// For software digests, the value of the digest is provided and appended to the list of
    /// items. For hardware digests, we must compute the digest value and append to the list of
    /// items.
    digest_type: DigestType,
    /// Partition size.
    size: usize,
    /// The key name for this parition.
    /// If specified, the serializer will attempt to scramble this parition using the key named in
    /// this field.
    key_name: Option<String>,
}

impl VmemPartition {
    pub fn new(
        name: String,
        size: usize,
        digest_type: DigestType,
        key_name: Option<String>,
    ) -> VmemPartition {
        VmemPartition {
            items: Vec::new(),
            name,
            digest_type,
            size,
            key_name,
        }
    }

    /// Set the size of the partition.
    ///
    /// For partitions that don't specify their size, this is used to set the size of the partition
    /// including the digest.
    pub fn set_size(&mut self, size: usize) {
        self.size = size;
    }

    /// Add an item to this partition.
    pub fn push_item(&mut self, item: VmemItem) {
        self.items.push(item);
    }

    /// Produces a tuple containing OTP HEX lines with annotations.
    fn write_to_buffer(&self, keys: &HashMap<String, Vec<u8>>) -> Result<(Vec<u8>, Vec<String>)> {
        if self.size % 8 != 0 {
            bail!("Partition {} must be 64-bit alligned", self.name);
        }

        let mut defined = vec![false; self.size];
        let mut annotations: Vec<String> = vec!["unallocated".to_owned(); self.size];

        let mut data_bytes: Vec<u8> = vec![0; self.size];

        for item in &self.items {
            let end = item.offset + item.size();
            annotations[item.offset..end].fill(format!("{}: {}", self.name, item.name).to_string());
            let defined = &mut defined[item.offset..end];
            if let Some(collision) = defined.iter().position(|defined| *defined) {
                bail!(
                    "Unexpected item collision with item {} at 0x{:x}",
                    item.name,
                    collision
                );
            }
            defined.fill(true);
            if let ItemType::Bytes(bytes) = &item.value {
                data_bytes[item.offset..end].copy_from_slice(bytes);
            }
        }

        let mut data_blocks = Vec::<u64>::new();
        let mut data_blocks_defined = Vec::<bool>::new();
        for (k, chunk) in data_bytes.chunks(8).enumerate() {
            data_blocks.push(u64::from_le_bytes(chunk.try_into().unwrap()));
            let byte_offset = k * 8;
            data_blocks_defined.push(
                defined[byte_offset..byte_offset + 8]
                    .iter()
                    .fold(false, |a, &b| a || b),
            );
        }

        if let Some(key_name) = &self.key_name {
            let key = keys
                .get(key_name)
                .ok_or_else(|| anyhow!("Key not found {}", key_name))?;

            let cipher = Present::try_new(key.clone())?;

            for i in 0..data_blocks.len() {
                if data_blocks_defined[i] {
                    data_blocks[i] = cipher.encrypt_block(data_blocks[i]);
                }
            }
        }

        if let DigestType::Hardware(iv, fin_const) = self.digest_type {
            ensure!(
                matches!(data_blocks.last(), None | Some(0)),
                "Digest of partition {} cannot be overridden manually",
                self.name
            );
            let last = data_blocks.len() - 1;
            data_blocks[last] = present_digest_64(&data_blocks[0..last], iv, fin_const);
        }

        let data = data_blocks.as_bytes().to_vec();

        if data.len() != self.size {
            Err(anyhow!("Partition {} size mismatch", self.name))
        } else {
            Ok((data, annotations))
        }
    }
}

pub struct VmemImage {
    partitions: Vec<VmemPartition>,
    width: usize,
    depth: usize,
}

impl VmemImage {
    pub fn new(partitions: Vec<VmemPartition>, width: usize, depth: usize) -> VmemImage {
        VmemImage {
            partitions,
            width,
            depth,
        }
    }
    pub fn generate(
        &self,
        keys: HashMap<String, Vec<u8>>,
        secded: &LcSecded,
    ) -> Result<Vec<String>> {
        let mut data: Vec<u8> = vec![0; self.width * self.depth];
        let mut annotations: Vec<String> = vec![Default::default(); data.len()];
        let mut offset = 0;
        for partition in &self.partitions {
            let (part_data, part_annotation) = partition.write_to_buffer(&keys)?;
            let end = offset + partition.size;
            if end > data.len() {
                bail!(
                    "Partition {} out of bounds, ends at 0x{:x}",
                    partition.name,
                    end
                );
            }
            data[offset..end].clone_from_slice(&part_data);
            annotations[offset..end].clone_from_slice(&part_annotation);
            offset += partition.size;
        }

        let width_ecc = self.width + secded.ecc_byte_len();
        let num_words = data.len() / self.width;

        let mut output = vec![format!(
            "// OTP memory hexfile with {} x {}bit layout",
            self.depth,
            width_ecc * 8
        )];

        for i in 0..num_words {
            let mut word = Vec::<u8>::new();
            let mut word_annotation = Vec::<String>::new();
            for j in 0..self.width {
                let idx = i * self.width + j;
                word.push(data[idx]);
                if !word_annotation.contains(&annotations[idx]) {
                    word_annotation.push(annotations[idx].clone());
                }
            }
            let word_with_ecc = secded.ecc_encode(word)?;
            let mut word_str = String::new();
            for byte in word_with_ecc.iter().rev() {
                write!(word_str, "{:02x}", byte)?;
            }
            output.push(format!(
                "{} // {:06x}: {}",
                word_str,
                i * self.width,
                word_annotation.join(", ")
            ));
        }

        Ok(output)
    }
}

fn present_digest_64(message: &[u64], iv: DigestIV, fin_const: DigestCnst) -> u64 {
    let mut state = iv;
    for i in (0..message.len() + 2).step_by(2) {
        let b128: [u8; 16] = if i + 1 < message.len() {
            (message[i] as u128) << 64 | message[i + 1] as u128
        } else if i < message.len() {
            (message[i] as u128) << 64 | message[i] as u128
        } else {
            fin_const
        }
        .as_bytes()
        .try_into()
        .unwrap();

        let cipher = Present::new_128(&b128);
        state ^= cipher.encrypt_block(state);
    }

    state
}

size