//
// Copyright (C) 2024 Automated Design Corp.. All Rights Reserved.
// Created Date: 2024-04-17 10:44:29
// -----
// Last Modified: 2024-11-12 08:01:38
// -----
//
//

use anyhow::anyhow;
use crc32fast::Hasher;
use indexmap::IndexMap;
use log::debug;
use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
use std::fmt;

use crate::{
    command_arg_tuple::MechFsmCommandArgTuple,
    variant::VariantValue,
};

// Re-export CommandMessage types from ipc module for backward compatibility.
// The canonical location is now `mechutil::ipc::{CommandMessage, CommandMessageResult, Action}`.
pub use crate::ipc::{Action, CommandMessage, CommandMessageResult};

/// A list of control codes for interacting with a remote or external state machine
/// intended to receive and execute commands.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, ToPrimitive, FromPrimitive, Eq, PartialEq)]
#[repr(i16)]
#[serde(try_from = "i16", into = "i16")]
pub enum MechFsmControl {
    /// No operation
    NoOp = 0,
    /// Request remote state machine to restart/reset to its startup state
    /// This is a hard reset that can and should result in a break in communications. For simply resetting or stopping
    /// a process in the remote FSM, the Init command should be used instead.
    Restart = 1,
    /// Request the remote state machine to jump to its Init state, which is the
    /// entry state just before the process. Making this request is used to stop any commands in
    /// process or reset process values, or used as a recovery after an error.
    /// The FSM will jump to this state any time after an error occurs.
    Init = 5,
    /// Request remote state machine to enter Idle state.
    /// If the remote state machine is already in Idle, no action is taken.
    /// This is the control code
    Idle = 10,
    /// Execute the current command. The command is usually a command code or
    /// Index, Sub-Index combination.
    /// The remote state machine must be in Idle to receive commands.
    Exec = 11,
    /// A request to write a value to the register specified by index and sub-index.
    /// Registers can be of any value type, though that type must be defined and known.
    Write = 12,
    /// A request to read a value to the register specified by index and sub-index.
    /// Registers can be of any value type, though that type must be defined and known.
    Read = 15,
    /// Ack that the command has completed.
    /// This is in response to CmdDone from the remote state machine.
    AckDone = 101,
}

impl TryFrom<i16> for MechFsmControl {
    type Error = anyhow::Error;

    fn try_from(value: i16) -> Result<Self, Self::Error> {
        num_traits::FromPrimitive::from_i16(value)
            .ok_or_else(|| anyhow::anyhow!("Invalid MechFsmControl value: {}", value))
    }
}

impl Into<i16> for MechFsmControl {
    fn into(self) -> i16 {
        self as i16
    }
}

/// A list of status codes for returned by a remote or external state machine
/// intended to receive and execute commands.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, ToPrimitive, FromPrimitive, Eq, PartialEq)]
#[repr(i16)]
pub enum MechFsmState {
    /// Invalid. The Status of the FSM should never be 0. This just means communications
    /// hasn't updated the value.
    Invalid = 0,
    /// The state machine is in its StartUp state. The FSM will automatically enter this
    /// state when it starts running.
    ///
    /// In this state, the state machine handles its internal business and, if
    /// successful, increments to the Init state.
    /// This is a good state for the FSM to establish/re-establish any communications.
    /// This state is not intended to be part of the normally-executing process.
    StartUp = 1,
    /// The state machine is in the Init state. This is the state before Idle where
    /// any values are reset; it is the entrance point to the process of the state machine.
    /// The FSM will go to this state after an error, and can
    /// be sent here by request from the FSM client, usually to stop a command in process.
    Init = 5,
    /// The state machine is Idle. It ready to accept and execute commands.
    Idle = 10,
    /// The state machine is executing a command, read or write request.
    /// Theoretically, an Init control code would stop execution of the command
    /// and reset the state machine, although that is not always practical.
    Executing = 20,
    /// The state machine has completed the command, read or write request.
    /// It will wait for an AckDone before returning to Idle, or an Init to
    /// reset.
    CmdDone = 100,
    /// The state machine has encountered an error.
    /// It will wait for an AckDone before returning to Idle, or an Init to
    /// reset.
    Error = 900,
}

impl fmt::Display for MechFsmState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let description = match self {
            MechFsmState::Invalid => "Invalid",
            MechFsmState::StartUp => "StartUp",
            MechFsmState::Init => "Init",
            MechFsmState::Idle => "Idle",
            MechFsmState::Executing => "Executing",
            MechFsmState::CmdDone => "CmdDone",
            MechFsmState::Error => "Error",
        };
        write!(f, "{}", description)
    }
}

/// We use this command structure for command interfaces with industrial controllers.
/// The command tuple identifies a transaction ID and a code, used for either a command request
/// or command status response.
/// The transaction ID allows arguments to be related to the requested command, allowing both
/// the server and client to identify stale data. The command code identifies the command to the remote state machine, using
/// a defined set of enumerations.
///
/// While this is a simple struct, it's important that the values be stored, read and written as
/// as a structure and not individually. Not all industrial protocols update values in the order one might expect, and just because
/// multiple values got updated in the same PLC scan doesn't mean they will be updated for communications
/// after the same PLC scan.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct MechFsmCommandTuple {
    /// Transaction ID. Should be non-zero. Generally, this transaction ID is used
    /// to match a command to its arguments or status response. A transaction ID
    /// should not be re-used sequentially.
    pub transaction_id: u32,
    /// Control code. Requests a state of execution of the state machine in the remote server or device.
    pub control: MechFsmControl,
    /// Index, or command code, of the command.
    /// Similar to CANOpen or CoE
    /// The index is used when the control code is Exec, Write or Read and, depending upon implementation,
    /// identifies the command to be executed, or the group where the command sub-index to be
    /// executed is located. Some command servers or devices are simple and only need a single
    /// command code,
    pub index: u16,
    /// Sub-index of a command code. Used when a state machine is
    /// grouping its commands and registers.
    /// Similar to CANOpen or CoE.
    /// The sub_index is used when the control code is Exec, Write or Read and, depending upon implementation,
    /// identifies the command to be executed or register to read/write.
    /// Some command servers or devices are simple and only need a single
    /// command code, in which case this field is ignored.
    pub sub_index: u16,
    /// Cyclical Redundancy Checksum. 32-bit. Optional.
    /// Don't set manually. Unless a custom CRC value is required, use the calculate_crc32
    /// function.
    pub crc: u32,

    // /// Data value that can be sent along with the command as an argument.
    // /// Usually used with the Write Register command.
    // pub data: MechCommandRegisterValue,
    /// Arguments and data values that can be sent along with the command, designed to be
    /// compatible with fixed-memory industrial controllers.
    /// The command arg tuple can contain up to 255 columns arguments, depending upon
    /// the size of the individual arguments. In that way, a command can be executed
    /// in less time because there needs be only one transmission: the command code and its
    /// arguments all in one package.
    pub data: MechFsmCommandArgTuple,
}

impl MechFsmCommandTuple {
    /// Constructor
    pub fn new() -> Self {
        Self {
            transaction_id: 0,
            control: MechFsmControl::NoOp,
            index: 0,
            sub_index: 0,
            crc: 0,
            data: MechFsmCommandArgTuple::new(),
        }
    }

    /// Construct an instance with the specified control code.
    pub fn from_control_code(ctrl: MechFsmControl) -> Self {
        Self {
            transaction_id: 0,
            control: ctrl,
            index: 0,
            sub_index: 0,
            crc: 0,
            data: MechFsmCommandArgTuple::new(),
        }
    }

    /// Convert this instance to a standardized, little-endian byte array.
    /// Convert this instance to a standardized, little-endian byte array.
    pub fn to_bytes(&self) -> [u8; 1036] {
        let mut ret = [0 as u8; 1036];
        ret[0..4].copy_from_slice(&self.transaction_id.to_le_bytes());
        ret[4..6].copy_from_slice(&(self.control as i16).to_le_bytes());
        ret[6..8].copy_from_slice(&self.index.to_le_bytes());
        ret[8..10].copy_from_slice(&self.sub_index.to_le_bytes());
        ret[10..14].copy_from_slice(&self.crc.to_le_bytes());
        ret[14..1036].copy_from_slice(&self.data.to_bytes());

        return ret;
    }

    /// Calculate the CRC32 checksum for the provided command tuple.
    pub fn calculate_crc32(command: &MechFsmCommandTuple) -> u32 {
        // Serialize the struct without the crc field
        let mut hasher = Hasher::new();

        // Exclude the CRC field during serialization by zeroing it
        let mut crc_excluded_tuple = command.clone();
        crc_excluded_tuple.crc = 0;

        let bytes = crc_excluded_tuple.to_bytes();
        hasher.update(&bytes);
        let crc32 = hasher.finalize();
        return crc32;
    }

    /// Calculate and store the CRC32 checksum in this command tuple.
    pub fn update_crc32(&mut self) {
        self.crc = MechFsmCommandTuple::calculate_crc32(self);
    }

    /// Returns true if this command tuple is valid for processing. A command tuple that does not return
    /// true cannot be trusted.
    ///
    /// ### Remarks
    /// Being invalid isn't necessarily an error. It could simply mean the control code is NoOp, or the
    /// transaction ID is 0.
    pub fn is_valid(&self) -> bool {
        if self.control == MechFsmControl::NoOp {
            return false;
        }

        if self.transaction_id == 0 {
            return false;
        }

        let crc_res = MechFsmCommandTuple::calculate_crc32(self);
        if crc_res != self.crc {
            debug!("CRC Check failed! {} != {}", crc_res, self.crc);
            debug!("{:?}", self);
            return false;
        } else {
            return true;
        }
    }
}

// For convenience, implement converting the TuplMechFsmCommandTuple from a VariantValue.
// Most of our communications libraries use VariantValue as the data type.
impl TryFrom<VariantValue> for MechFsmCommandTuple {
    type Error = anyhow::Error;

    /// Convert the VariantValue to a MechFsmCommandTuple
    fn try_from(value: VariantValue) -> Result<Self, anyhow::Error> {
        match value.clone() {
            VariantValue::Object(map) => {
                let control = map
                    .get("control")
                    .and_then(|v| v.to_int16().ok())
                    .and_then(MechFsmControl::from_i16)
                    .unwrap_or(MechFsmControl::NoOp);

                let crc = map.get("crc").and_then(|v| v.to_uint32().ok()).unwrap_or(0);

                let data = map
                    .get("data")
                    .and_then(|v| v.clone().try_into().ok())
                    .unwrap_or(MechFsmCommandArgTuple::new());

                let index = map
                    .get("index")
                    .and_then(|v| v.to_uint16().ok())
                    .unwrap_or(0);

                let sub_index = map
                    .get("sub_index")
                    .and_then(|v| v.to_uint16().ok())
                    .unwrap_or(0);

                let transaction_id = map
                    .get("transaction_id")
                    .and_then(|v| v.to_uint32().ok())
                    .unwrap_or(0);

                let ret = MechFsmCommandTuple {
                    control,
                    crc,
                    data,
                    index,
                    sub_index,
                    transaction_id,
                };

                return Ok(ret);
            }
            _ => {
                return Err(anyhow::anyhow!("VariantValue is not an Object"));
            }
        }
    }
}

// For convenience, implement converting the MechFsmCommandTuple to a VariantValue.
// Most of our communications libraries use VariantValue as the data type.
impl TryInto<VariantValue> for MechFsmCommandTuple {
    type Error = anyhow::Error;

    /// Convert the MechFsmCommandTuple to a VariantValue.
    fn try_into(self) -> Result<VariantValue, anyhow::Error> {
        let mut map = IndexMap::new();

        // Convert the data field to a variant
        let data_var: VariantValue = self.data.try_into().unwrap();

        // Manually insert each field as a VariantValue
        map.insert(
            "control".to_string(),
            VariantValue::Int16(self.control as i16),
        );
        map.insert("crc".to_string(), VariantValue::UInt32(self.crc));
        map.insert("data".to_string(), data_var);
        map.insert("index".to_string(), VariantValue::UInt16(self.index));
        map.insert(
            "sub_index".to_string(),
            VariantValue::UInt16(self.sub_index),
        );
        map.insert(
            "transaction_id".to_string(),
            VariantValue::UInt32(self.transaction_id),
        );

        let ret = VariantValue::Object(Box::new(map));
        return Ok(ret);
    }
}

/// Represents the status response from a state machine in a remote server or device.
#[derive(Deserialize, Debug, Clone, Copy)]
pub struct MechFsmStatusTuple {
    /// Transaction ID. Should be non-zero. The transaction ID should match the ID
    /// from the command.
    pub transaction_id: u32,
    /// Status code. Represents a state of execution of the state machine in the remote server or device.
    pub state: MechFsmState,
    /// Cyclical Redundancy Checksum. 32-bit. Optional.
    /// Don't set manually. Unless a custom CRC value is required, use the calculate_crc32
    /// function.
    pub crc: u32,

    // /// Data value that can be sent along with the status as a data payload.
    // /// Usually used for the result of the Read Register command.
    // pub data: MechCommandRegisterValue,
    /// Arguments and data values that can be sent along with the command, designed to be
    /// compatible with fixed-memory industrial controllers.
    /// The command arg tuple can contain up to 255 columns arguments, depending upon
    /// the size of the individual arguments. In that way, a command can be executed
    /// in less time because there needs be only one transmission: the command code and its
    /// arguments all in one package.
    pub data: MechFsmCommandArgTuple,
}

impl MechFsmStatusTuple {
    /// Constructor new status tuple.
    pub fn new() -> Self {
        Self {
            transaction_id: 0,
            state: MechFsmState::Invalid,
            crc: 0,
            data: MechFsmCommandArgTuple::new(),
        }
    }

    // /// Serialize the struct to a byte array as it would be in C.
    pub fn to_bytes(&self) -> [u8; 1032] {
        let mut ret = [0 as u8; 1032];
        ret[0..4].copy_from_slice(&self.transaction_id.to_le_bytes());
        ret[4..6].copy_from_slice(&(self.state as i16).to_le_bytes());
        ret[6..10].copy_from_slice(&(self.crc as u32).to_le_bytes());
        ret[10..1032].copy_from_slice(&self.data.to_bytes());
        return ret;
    }

    pub fn update_crc32(&mut self) {
        // Get the byte array representation of the struct without the crc field
        let mut crc_excluded_tuple = self.clone();
        crc_excluded_tuple.crc = 0;

        // Serialize the struct to bytes
        let bytes = crc_excluded_tuple.to_bytes();

        // Calculate CRC32 on the byte array
        let mut hasher = crc32fast::Hasher::new();
        hasher.update(&bytes);
        self.crc = hasher.finalize();
    }
}

// For convenience, implement converting the MechFsmStatusTuple from a VariantValue.
// Most of our communications libraries use VariantValue as the data type.
impl TryFrom<VariantValue> for MechFsmStatusTuple {
    type Error = anyhow::Error;

    /// Convert a VariantValue to MechFsmStatusTuple.
    fn try_from(value: VariantValue) -> Result<Self, anyhow::Error> {
        match value.to_json() {
            Ok(json) => match serde_json::from_value(json) {
                Ok(ret) => {
                    return Ok(ret);
                }
                Err(err) => {
                    return Err(anyhow!(
                        "Failed to deserialize MechFsmStatusTuple from json: {}",
                        err
                    ));
                }
            },
            Err(err) => {
                return Err(anyhow!(
                    "Failed to deserialize MechFsmStatusTuple to json: {}",
                    err
                ));
            }
        }
    }
}

// For convenience, implement converting the MechFsmStatusTuple to a VariantValue.
// Most of our communications libraries use VariantValue as the data type.
impl TryInto<VariantValue> for MechFsmStatusTuple {
    type Error = anyhow::Error;

    /// Convert the MechFsmStatusTuple to a VariantValue.
    fn try_into(self) -> Result<VariantValue, anyhow::Error> {
        match serde_json::to_value(self) {
            Ok(json) => match VariantValue::from_json(&json) {
                Ok(ret) => {
                    return Ok(ret);
                }
                Err(err) => {
                    return Err(anyhow!(
                        "Failed to serialize MechFsmCommandTuple to VariantValue: {}",
                        err
                    ));
                }
            },
            Err(err) => {
                return Err(anyhow!(
                    "Failed to serialize MechFsmCommandTuple to json: {}",
                    err
                ));
            }
        }
    }
}

impl Serialize for MechFsmStatusTuple {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let data = serde_json::to_value(self.data).unwrap();

        let mut state = serializer.serialize_struct("MechFsmStatusTuple", 4)?;
        state.serialize_field("transaction_id", &self.transaction_id)?;
        state.serialize_field("state", &(self.state as i16))?;
        state.serialize_field("crc", &self.crc)?;
        state.serialize_field("data", &data)?;
        return state.end();
    }
}
