//!
//! Gather IOp argument in a common type
//! Provides a FromStr implementation for parsing

use super::*;
use field::{
    FwMode, IOpHeader, IOpcode, ImmBundle, Immediate, Operand, OperandBlock, OperandBundle,
};
use lazy_static::lazy_static;

pub const ASM_OPCODE_WIDTH: usize = 8;

/// Parsing error
#[derive(thiserror::Error, Debug, Clone)]
pub enum ParsingError {
    #[error("Opcode {0} is in in reserved range")]
    Opcode(u8),
    #[error("Unknown IOp alias {0}")]
    Opalias(String),
    #[error("Unmatch Asm Operation: {0}")]
    Unmatch(String),
    #[error("Invalid arguments number: expect {0}, get {1}")]
    ArgNumber(usize, usize),
    #[error("Invalid arguments type: expect {0}, get {1}")]
    ArgType(String, Arg),
    #[error("Invalid arguments: {0}")]
    InvalidArg(String),
    #[error("Empty line")]
    Empty,
}

// Asm arguments are slightly different that hex word
// Thus we can't directly mapped ASM arg to fmt structure
// Below, we define a set of arguments for parsing purpose

/// Define fixed inner IOp (opposed as user available IOP)
/// Those IOp are generated by the Fw and have a fixed number of arguments
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct IOpFormat {
    pub name: String,
    pub opcode: IOpcode,
    pub proto: IOpProto,
}

/// Opcode asm parsing utility
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AsmIOpcode {
    pub(crate) opcode: IOpcode,
    pub(crate) format: Option<IOpFormat>,
}

impl AsmIOpcode {
    pub fn from_opcode(opcode: IOpcode) -> Self {
        if let Some(alias) = IOP_LUT.hex.get(&opcode) {
            Self {
                opcode,
                format: Some(alias.clone()),
            }
        } else {
            Self {
                opcode,
                format: None,
            }
        }
    }
    pub fn opcode(&self) -> IOpcode {
        self.opcode
    }

    pub fn format(&self) -> Option<&IOpFormat> {
        self.format.as_ref()
    }
    pub fn has_imm(&self) -> bool {
        if let Some(alias) = self.format.as_ref() {
            alias.proto.imm != 0
        } else {
            false
        }
    }
}

impl std::fmt::Display for AsmIOpcode {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let name = if let Some(alias) = &self.format {
            &alias.name
        } else {
            &format!("IOP[0x{:x}]", self.opcode.0)
        };
        write!(f, "{name: <ASM_OPCODE_WIDTH$}")
    }
}

/// Extract AsmOpcode from IOpcode
impl From<IOpcode> for AsmIOpcode {
    fn from(opcode: IOpcode) -> Self {
        if let Some(alias) = IOP_LUT.hex.get(&opcode) {
            Self {
                opcode,
                format: Some(alias.clone()),
            }
        } else {
            Self {
                opcode,
                format: None,
            }
        }
    }
}

/// Extract AsmOpcode from IOp
/// This is used from proper rendering in asm
impl From<&IOp> for AsmIOpcode {
    fn from(iop: &IOp) -> Self {
        Self::from(iop.header.opcode)
    }
}

impl std::str::FromStr for AsmIOpcode {
    type Err = ParsingError;

    #[tracing::instrument(level = "trace", ret)]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref OPCODE_ARG_RE: regex::Regex = regex::Regex::new(
                r"(?<raw>^IOP\[((?<hex_val>0x[0-9a-fA-F]+)|(?<val>[0-9]+))\])|^(?<alias>\w+)"
            )
            .expect("Invalid regex");
        }

        if let Some(caps) = OPCODE_ARG_RE.captures(s) {
            if let Some(_raw) = caps.name("raw") {
                let value = if let Some(raw_val) = caps.name("val") {
                    raw_val
                        .as_str()
                        .parse::<u8>()
                        .map_err(|err| ParsingError::InvalidArg(err.to_string()))?
                } else {
                    // One of them must match, otherwise error will be arose before
                    let raw_hex_val = caps.name("hex_val").unwrap();
                    u8::from_str_radix(&raw_hex_val.as_str()[2..], 16)
                        .map_err(|err| ParsingError::InvalidArg(err.to_string()))?
                };
                if (opcode::USER_RANGE_LB..=opcode::USER_RANGE_UB).contains(&value) {
                    Ok(AsmIOpcode {
                        opcode: IOpcode(value),
                        format: None,
                    })
                } else {
                    Err(ParsingError::Opcode(value))
                }
            } else if let Some(alias) = caps.name("alias") {
                if let Some(alias) = IOP_LUT.asm.get(alias.as_str()) {
                    Ok(AsmIOpcode {
                        opcode: alias.opcode,
                        format: Some(alias.clone()),
                    })
                } else {
                    Err(ParsingError::Opalias(alias.as_str().to_string()))
                }
            } else {
                Err(ParsingError::Unmatch(format!(
                    "Invalid argument format {s}"
                )))
            }
        } else {
            Err(ParsingError::Unmatch(format!(
                "Invalid argument format {s}"
            )))
        }
    }
}

impl From<&AsmIOpcode> for IOpcode {
    fn from(asm: &AsmIOpcode) -> Self {
        asm.opcode
    }
}

/// Properties asm parsing utility
#[derive(Debug, Clone)]
pub struct Properties {
    fw_mode: FwMode,
    dst_align: OperandBlock,
    src_align: OperandBlock,
}

impl std::fmt::Display for Properties {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let mode = match self.fw_mode {
            FwMode::Static => "",
            FwMode::Dynamic => "dyn ",
        };
        write!(
            f,
            "{}I{} I{}",
            mode,
            (self.dst_align.0 + 1) * MSG_WIDTH,
            (self.src_align.0 + 1) * MSG_WIDTH,
        )
    }
}

/// Extract properties from IOpHeader
impl From<&IOpHeader> for Properties {
    fn from(value: &IOpHeader) -> Self {
        Self {
            fw_mode: value.fw_mode,
            dst_align: value.dst_align,
            src_align: value.src_align,
        }
    }
}

impl std::str::FromStr for Properties {
    type Err = ParsingError;

    #[tracing::instrument(level = "trace", ret)]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref PROPERTIES_ARG_RE: regex::Regex =
                regex::Regex::new(r"(?<fw>dyn)?\s*I(?<dst>\d+)\s*I(?<src>\d+)")
                    .expect("Invalid regex");
        }

        if let Some(caps) = PROPERTIES_ARG_RE.captures(s) {
            let fw_mode = if caps.name("fw").is_some() {
                FwMode::Dynamic
            } else {
                FwMode::Static
            };
            let src_width = caps["src"]
                .parse::<u16>()
                .map_err(|err| ParsingError::InvalidArg(err.to_string()))?;
            let src_align = OperandBlock::new((src_width / MSG_WIDTH as u16) as u8);
            let dst_width = caps["dst"]
                .parse::<u16>()
                .map_err(|err| ParsingError::InvalidArg(err.to_string()))?;
            let dst_align = OperandBlock::new((dst_width / MSG_WIDTH as u16) as u8);
            Ok(Properties {
                fw_mode,
                dst_align,
                src_align,
            })
        } else {
            Err(ParsingError::Unmatch(format!(
                "Invalid argument format {s}"
            )))
        }
    }
}

impl std::fmt::Display for Operand {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        // Block/vec_size are zeroed indexed value
        // -> Transform them in one indexed for human readability
        let block = self.block.0 + 1;
        let vec_size = self.vec_size.0 + 1;
        if vec_size != 1 {
            write!(
                f,
                "I{}[{}]@0x{:0>2x}",
                block * MSG_WIDTH,
                vec_size,
                self.base_cid.0,
            )
        } else {
            write!(f, "I{}@0x{:0>2x}", block * MSG_WIDTH, self.base_cid.0,)
        }
    }
}

// OperandBundle
// Addr are packed in <> in the ASM format and thus we only parse them by bundle
impl std::fmt::Display for OperandBundle {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{}",
            self.iter()
                .fold(" ".to_string(), |acc, x| format!("{acc}{x} "))
                .trim()
        )
    }
}

impl std::str::FromStr for OperandBundle {
    type Err = ParsingError;

    #[tracing::instrument(level = "trace", ret)]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref ADDR_ARG_RE: regex::Regex =
                regex::Regex::new(r"I(?<width>\d+)((?<vec>\[\s*(?<vec_len>\d+)\s*\]@(0x(?<vec_hex_cid>[0-9a-fA-F]+)|(?<vec_cid>\d+))\s*)|(?<single>@(0x(?<hex_cid>[0-9a-fA-F]+)|(?<cid>\d+))\s*))")
                    .expect("Invalid regex");
        }
        let mut operands = ADDR_ARG_RE
            .captures_iter(s)
            .map(|caps| {
                let width = caps["width"]
                    .parse::<u16>()
                    .map_err(|err| ParsingError::InvalidArg(err.to_string()))?;
                let block = (width / MSG_WIDTH as u16) as u8;

                if let Some(_vec) = caps.name("vec") {
                    let base_cid = if let Some(raw_cid) = caps.name("vec_cid") {
                        raw_cid
                            .as_str()
                            .parse::<u16>()
                            .map_err(|err| ParsingError::InvalidArg(err.to_string()))?
                    } else {
                        // One of them must match, otherwise error will be arose before
                        let raw_hex_cid = caps.name("vec_hex_cid").unwrap();
                        u16::from_str_radix(raw_hex_cid.as_str(), 16)
                            .map_err(|err| ParsingError::InvalidArg(err.to_string()))?
                    };
                    let len = caps["vec_len"]
                        .parse::<u8>()
                        .map_err(|err| ParsingError::InvalidArg(err.to_string()))?;

                    Ok(Operand::new(block, base_cid, len, None))
                } else if let Some(_single) = caps.name("single") {
                    let base_cid = if let Some(raw_cid) = caps.name("cid") {
                        raw_cid
                            .as_str()
                            .parse::<u16>()
                            .map_err(|err| ParsingError::InvalidArg(err.to_string()))?
                    } else {
                        // One of them must match, otherwise error will be arose before
                        u16::from_str_radix(&caps["hex_cid"], 16)
                            .map_err(|err| ParsingError::InvalidArg(err.to_string()))?
                    };
                    Ok(Operand::new(block, base_cid, 1, None))
                } else {
                    Err(ParsingError::Unmatch(format!(
                        "Invalid argument format {s}"
                    )))
                }
            })
            .collect::<Result<Vec<_>, ParsingError>>()?;

        // Empty OperandBundle is considered as parsing error
        if operands.is_empty() {
            Err(ParsingError::Unmatch(format!(
                "Invalid argument: Empty OperandBundle {s}"
            )))
        } else {
            // Update is_last token
            operands.last_mut().unwrap().is_last = true;
            Ok(operands.into())
        }
    }
}

// ImmBundle
// Imm are packed in <> in the ASM format and thus we only parse them by bundle
impl std::fmt::Display for ImmBundle {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{}",
            self.iter()
                .fold(" ".to_string(), |acc, x| format!(
                    "{acc}0x{:x} ",
                    x.cst_value()
                ))
                .trim()
        )
    }
}

impl std::str::FromStr for ImmBundle {
    type Err = ParsingError;

    #[tracing::instrument(level = "trace", ret)]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref IMM_ARG_RE: regex::Regex =
                regex::Regex::new(r"(0x(?<hex_imm>[0-9a-fA-F]+))|(?<imm>\d+)")
                    .expect("Invalid regex");
        }
        let mut imms = IMM_ARG_RE
            .captures_iter(s)
            .map(|caps| {
                let imm = if let Some(raw_imm) = caps.name("imm") {
                    raw_imm
                        .as_str()
                        .parse::<u128>()
                        .map_err(|err| ParsingError::InvalidArg(err.to_string()))?
                } else {
                    // One of them must match, otherwise error will be arose before
                    let raw_hex_imm = caps.name("hex_imm").unwrap();
                    u128::from_str_radix(raw_hex_imm.as_str(), 16)
                        .map_err(|err| ParsingError::InvalidArg(err.to_string()))?
                };

                Ok(Immediate::from_cst(imm))
            })
            .collect::<Result<Vec<_>, ParsingError>>()?;

        // Empty ImmBundle is considered as parsing error
        if imms.is_empty() {
            Err(ParsingError::Unmatch(format!(
                "Invalid argument format {s}"
            )))
        } else {
            // Update is_last token
            imms.last_mut().unwrap().is_last = true;
            Ok(imms.into())
        }
    }
}

/// Generic arguments
/// Used to pack argument under the same type
#[derive(Debug, Clone)]
pub enum Arg {
    Opcode(AsmIOpcode),
    Properties(Properties),
    Operand(OperandBundle),
    Imm(ImmBundle),
}

/// Use Display trait to convert into asm human readable file
/// Simply defer to inner type display impl while forcing the display width
impl std::fmt::Display for Arg {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Arg::Opcode(inner) => write!(f, "{inner}"),
            Arg::Properties(inner) => write!(f, "{inner}"),
            Arg::Operand(inner) => write!(f, "{inner}"),
            Arg::Imm(inner) => write!(f, "{inner}"),
        }
    }
}

/// Use FromStr trait to decode from asm file
impl std::str::FromStr for Arg {
    type Err = ParsingError;

    #[tracing::instrument(level = "trace", ret)]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match (
            OperandBundle::from_str(s),
            AsmIOpcode::from_str(s),
            Properties::from_str(s),
            ImmBundle::from_str(s),
        ) {
            (Ok(operand), ..) => Ok(Self::Operand(operand)),
            (Err(_), Ok(opcode), ..) => Ok(Self::Opcode(opcode)),
            (Err(_), Err(_), Ok(props), ..) => Ok(Self::Properties(props)),
            (Err(_), Err(_), Err(_), Ok(imm)) => Ok(Self::Imm(imm)),
            (Err(addr), Err(opcode), Err(props), Err(imm)) => Err(ParsingError::Unmatch(format!(
                "{s}:
            Addr failed with{addr}
            Opcode failed with{opcode}
            Props failed with{props}
            Imm failed with{imm}
            "
            ))),
        }
    }
}

impl std::fmt::Display for IOp {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let opcode = AsmIOpcode::from(self);
        write!(f, "{opcode}")?;

        let props = Properties::from(&self.header);
        write!(f, " <{props}>")?;

        // Destination operands list
        write!(f, " <{}>", self.dst)?;

        // Source operands list
        write!(f, " <{}>", self.src)?;

        // Immediate operands list [Optional]
        if self.header.has_imm {
            write!(f, " <{}>", self.imm)?;
        }

        Ok(())
    }
}

/// Use FromStr trait to decode from asm file
impl std::str::FromStr for IOp {
    type Err = ParsingError;

    #[tracing::instrument(level = "trace", ret)]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        lazy_static! {
            static ref IOP_RE: regex::Regex = regex::Regex::new(
                r"^(?<opcode>\S+)\s*(?<props><.*?>)\s*(?<dst><.*?>)\s*(?<src><.*?>)\s*(?<imm><.*?>)?"
            )
            .expect("Invalid regex");
        }

        if let Some(caps) = IOP_RE.captures(s) {
            let opcode = AsmIOpcode::from_str(caps["opcode"].trim_matches(['<', '>', ' ']))?;
            let props = Properties::from_str(caps["props"].trim_matches(['<', '>', ' ']))?;
            let dst = {
                let mut bundle =
                    OperandBundle::from_str(caps["dst"].trim_matches(['<', '>', ' ']))?;
                bundle.set_kind(OperandKind::Dst);
                bundle
            };
            let src = {
                let mut bundle =
                    OperandBundle::from_str(caps["src"].trim_matches(['<', '>', ' ']))?;
                bundle.set_kind(OperandKind::Src);
                bundle
            };
            let (imm, has_imm) = if let Some(imm) = caps.name("imm") {
                (
                    ImmBundle::from_str(imm.as_str().trim_matches(['<', '>', ' ']))?,
                    true,
                )
            } else {
                (ImmBundle::from(vec![]), false)
            };

            // Aggregate some fields together to build real IOp
            let header = IOpHeader {
                fw_mode: props.fw_mode,
                has_imm,
                opcode: opcode.opcode,
                dst_align: props.dst_align,
                src_align: props.src_align,
            };

            Ok(IOp {
                header,
                dst,
                src,
                imm,
            })
        } else {
            Err(ParsingError::Unmatch(format!(
                "Invalid argument format {s}"
            )))
        }
    }
}
