//! Various conversions functions and implementation.

use std::borrow::Cow;
use std::time::{Duration, SystemTime};

use crate::dev::{AttrQuality, DevState, DisplayLevel};
use crate::data::{DataKind, DataLayoutKind};
use crate::error::{Error, ErrorKind, ErrorSeverity};

use super::orb;


/// Internal conversion from CORBA exception to DevError.
/// This from implementation is tracking caller in order to have a better idea of the \
/// location the error was created.
impl From<orb::CORBA_Exception> for Error {
    #[track_caller]
    fn from(err: orb::CORBA_Exception) -> Self {

        let repo_id = err.repo_id();

        let err = match orb::CORBA_SystemException::downcast(err) {
            Ok(err) => {

                // Once this is a system exception, this is now containing the minor code
                // appended to the major code!
                let name = err.name();

                if repo_id == c"IDL:omg.org/CORBA/OBJECT_NOT_EXIST:1.0"
                || repo_id == c"IDL:omg.org/CORBA/TRANSIENT:1.0"
                || repo_id == c"IDL:omg.org/CORBA/COMM_FAILURE:1.0" {
                    if name == c"OBJECT_NOT_EXIST_NoMatch" {
                        return Error::new(ErrorKind::DevNotConnected);
                    } else if name == c"TRANSIENT_NoUsableProfile" 
                           || name == c"TRANSIENT_ConnectFailed"
                           || name == c"COMM_FAILURE_WaitingForReply" {
                        return Error::new(ErrorKind::ServerNotConnected);
                    } else if name == c"TRANSIENT_CallTimedout" {
                        return Error::new(ErrorKind::TimedOut);
                    } else {
                        return Error::new(ErrorKind::Internal)
                            .with_description(format!("{err:?}"))
                            .with_source(Box::new(err));
                    }
                } else if repo_id == c"IDL:omg.org/CORBA/TIMEOUT:1.0" {
                    return Error::new(ErrorKind::TimedOut);
                } else {
                    return Error::new(ErrorKind::Internal)
                        .with_description(format!("{err:?}"))
                        .with_source(Box::new(err));
                }

            }
            Err(err) => err,
        };

        let err = match orb::Tango_DevFailed::downcast(err) {
            Ok(err) if !err.data().errors.is_empty() => {
                return Error::from(&err.data().errors);
            }
            Ok(err) => err.into_base(),
            Err(err) => err,
        };

        Error::new(ErrorKind::Internal)
            .with_description(format!("{err:?}"))
            .with_source(Box::new(err))

    }
}

impl<'a> From<&'a orb::Tango_DevErrorList> for Error {
    fn from(value: &'a orb::Tango_DevErrorList) -> Self {
        
        let mut top_err = None;
        for raw_err in value.iter().rev() {

            let mut err = Error::new(ErrorKind::Reason(&raw_err.reason.to_string_lossy()));
            err.set_description(Cow::Owned(raw_err.desc.to_string_lossy().into_owned()));
            err.set_origin(Cow::Owned(raw_err.origin.to_string_lossy().into_owned()));
            err.set_severity(raw_err.severity.into());
            if let Some(parent_err) = top_err.take() {
                err.set_parent(parent_err);
            }

            top_err = Some(err);

        }

        top_err.expect("The given Tango::DevErrorList should have at least one error")

    }
}

// From/To for device state. Note that these mappings should be optimized, because both 
// enums have the same discriminants.
impl Into<DevState> for orb::Tango_DevState {
    fn into(self) -> DevState {
        match self {
            orb::Tango_DevState::ON => DevState::On,
            orb::Tango_DevState::OFF => DevState::Off,
            orb::Tango_DevState::CLOSE => DevState::Close,
            orb::Tango_DevState::OPEN => DevState::Open,
            orb::Tango_DevState::INSERT => DevState::Insert,
            orb::Tango_DevState::EXTRACT => DevState::Extract,
            orb::Tango_DevState::MOVING => DevState::Moving,
            orb::Tango_DevState::STANDBY => DevState::Standby,
            orb::Tango_DevState::FAULT => DevState::Fault,
            orb::Tango_DevState::INIT => DevState::Init,
            orb::Tango_DevState::RUNNING => DevState::Running,
            orb::Tango_DevState::ALARM => DevState::Alarm,
            orb::Tango_DevState::DISABLE => DevState::Disable,
            orb::Tango_DevState::UNKNOWN => DevState::Unknown,
        }
    }
}

impl From<DevState> for orb::Tango_DevState {
    fn from(value: DevState) -> Self {
        match value {
            DevState::On => orb::Tango_DevState::ON,
            DevState::Off => orb::Tango_DevState::OFF,
            DevState::Close => orb::Tango_DevState::CLOSE,
            DevState::Open => orb::Tango_DevState::OPEN,
            DevState::Insert => orb::Tango_DevState::INSERT,
            DevState::Extract => orb::Tango_DevState::EXTRACT,
            DevState::Moving => orb::Tango_DevState::MOVING,
            DevState::Standby => orb::Tango_DevState::STANDBY,
            DevState::Fault => orb::Tango_DevState::FAULT,
            DevState::Init => orb::Tango_DevState::INIT,
            DevState::Running => orb::Tango_DevState::RUNNING,
            DevState::Alarm => orb::Tango_DevState::ALARM,
            DevState::Disable => orb::Tango_DevState::DISABLE,
            DevState::Unknown => orb::Tango_DevState::UNKNOWN,
        }
    }
}

// From/To for device severity. 
impl Into<ErrorSeverity> for orb::Tango_ErrSeverity {
    fn into(self) -> ErrorSeverity {
        match self {
            orb::Tango_ErrSeverity::WARN => ErrorSeverity::Warning,
            orb::Tango_ErrSeverity::ERR => ErrorSeverity::Error,
            orb::Tango_ErrSeverity::PANIC => ErrorSeverity::Panic,
        }
    }
}

impl From<ErrorSeverity> for orb::Tango_ErrSeverity {
    fn from(value: ErrorSeverity) -> Self {
        match value {
            ErrorSeverity::Warning => orb::Tango_ErrSeverity::WARN,
            ErrorSeverity::Error => orb::Tango_ErrSeverity::ERR,
            ErrorSeverity::Panic => orb::Tango_ErrSeverity::PANIC,
        }
    }
}

// From/To for attribute quality. 
impl Into<AttrQuality> for orb::Tango_AttrQuality {
    fn into(self) -> AttrQuality {
        match self {
            orb::Tango_AttrQuality::ATTR_VALID => AttrQuality::Valid,
            orb::Tango_AttrQuality::ATTR_INVALID => AttrQuality::Invalid,
            orb::Tango_AttrQuality::ATTR_ALARM => AttrQuality::Alarm,
            orb::Tango_AttrQuality::ATTR_CHANGING => AttrQuality::Changing,
            orb::Tango_AttrQuality::ATTR_WARNING => AttrQuality::Warning,
        }
    }
}

impl From<AttrQuality> for orb::Tango_AttrQuality {
    fn from(value: AttrQuality) -> Self {
        match value {
            AttrQuality::Valid => tango_omniorb::Tango_AttrQuality::ATTR_VALID,
            AttrQuality::Invalid => tango_omniorb::Tango_AttrQuality::ATTR_INVALID,
            AttrQuality::Alarm => tango_omniorb::Tango_AttrQuality::ATTR_ALARM,
            AttrQuality::Changing => tango_omniorb::Tango_AttrQuality::ATTR_CHANGING,
            AttrQuality::Warning => tango_omniorb::Tango_AttrQuality::ATTR_WARNING,
        }
    }
}

// From/To for display level. 
impl Into<DisplayLevel> for orb::Tango_DispLevel {
    fn into(self) -> DisplayLevel {
        match self {
            orb::Tango_DispLevel::OPERATOR => DisplayLevel::Operator,
            orb::Tango_DispLevel::EXPERT => DisplayLevel::Expert,
            orb::Tango_DispLevel::DL_UNKNOWN => DisplayLevel::Operator,
        }
    }
}

impl From<DisplayLevel> for orb::Tango_DispLevel {
    fn from(value: DisplayLevel) -> Self {
        match value {
            DisplayLevel::Operator => orb::Tango_DispLevel::OPERATOR,
            DisplayLevel::Expert => orb::Tango_DispLevel::EXPERT,
        }
    }
}

pub fn time_val_to_system(val: &orb::Tango_TimeVal) -> Option<SystemTime> {
    
    let nanos = 
        u64::try_from(val.tv_nsec).ok()? + 
        u64::try_from(val.tv_usec).ok()? * 1_000 +
        u64::try_from(val.tv_sec).ok()? * 1_000_000_000;

    SystemTime::UNIX_EPOCH
        .checked_add(Duration::from_nanos(nanos))
    
}

pub fn time_val_to_system_or_epoch(val: &orb::Tango_TimeVal) -> SystemTime {
    time_val_to_system(val).unwrap_or(SystemTime::UNIX_EPOCH)
}

pub fn time_val_from_system(time: SystemTime) -> Option<orb::Tango_TimeVal> {

    let duration = time.duration_since(SystemTime::UNIX_EPOCH).ok()?;
    let nanos = duration.subsec_nanos();
    let micros = nanos / 1_000;
    let nanos = nanos - micros * 1_000;

    // Cast not checked because a nano sec is 999_999_999.
    Some(orb::Tango_TimeVal {
        tv_sec: i32::try_from(duration.as_secs()).ok()?,
        tv_usec: micros as i32,
        tv_nsec: nanos as i32,
    })

}

pub fn time_val_from_system_or_zero(time: SystemTime) -> orb::Tango_TimeVal {
    time_val_from_system(time).unwrap_or_default()
}

pub fn data_kind_from_type(
    data_type: orb::CORBA_Long, 
    attr_layout: Option<DataLayoutKind>, 
    enum_labels: Option<&orb::Tango_DevVarStringArray>,
) -> Option<DataKind> {

    let attr = attr_layout.is_some();
    let layout = attr_layout.unwrap_or(DataLayoutKind::Scalar);

    Some(match data_type {
        0 => DataKind::Void,
        1 => DataKind::Bool(layout),
        2 => DataKind::Short(layout),
        3 => DataKind::Long(layout),
        4 => DataKind::Float(layout),
        5 => DataKind::Double(layout),
        6 => DataKind::UShort(layout),
        7 => DataKind::ULong(layout),
        8 => DataKind::String(layout),
        19 => DataKind::State(layout),
        20 => DataKind::String(layout),
        22 => DataKind::Octet(layout),
        23 => DataKind::Long64(layout),
        24 => DataKind::ULong64(layout),
        29 => {
            // For enumerations...
            if let Some(labels) = enum_labels {
                DataKind::Enum(Cow::Owned(labels.iter()
                    .map(|s| s.to_string_lossy().into_owned())
                    .collect::<Vec<_>>()))
            } else {
                DataKind::Short(layout)
            }
        }
        _ if !attr => {
            // For commands, we have the array kinds!
            const SPECTRUM_KIND: DataLayoutKind = DataLayoutKind::Spectrum(None);
            match data_type {
                9 => DataKind::Octet(SPECTRUM_KIND),
                10 => DataKind::Short(SPECTRUM_KIND),
                11 => DataKind::Long(SPECTRUM_KIND),
                12 => DataKind::Float(SPECTRUM_KIND),
                13 => DataKind::Double(SPECTRUM_KIND),
                14 => DataKind::UShort(SPECTRUM_KIND),
                15 => DataKind::ULong(SPECTRUM_KIND),
                16 => DataKind::String(SPECTRUM_KIND),
                17 => DataKind::Struct(Cow::Borrowed(&[
                    DataKind::Long(SPECTRUM_KIND),
                    DataKind::String(SPECTRUM_KIND),
                ])),
                18 => DataKind::Struct(Cow::Borrowed(&[
                    DataKind::Double(SPECTRUM_KIND),
                    DataKind::String(SPECTRUM_KIND),
                ])),
                21 => DataKind::Bool(SPECTRUM_KIND),
                25 => DataKind::Long64(SPECTRUM_KIND),
                26 => DataKind::ULong64(SPECTRUM_KIND),
                31 => DataKind::State(SPECTRUM_KIND),
                _ => return None,
            }
        }
        _ => return None,
    })

}

pub const S_NOT_SPECIFIED: &str = "Not specified";
pub const S_NO_DESCRIPTION: &str = "No description";
pub const S_NO_DISPLAY_UNIT: &str = "No display unit";
pub const S_UNINITIALIZED: &str = "Uninitialised";

pub fn opt_str_if(s: &orb::CORBA_String, is_not: &str) -> Option<String> {
    if s.to_bytes() == is_not.as_bytes() {
        None
    } else {
        Some(s.to_string_lossy().into_owned())
    }
}

pub fn opt_f32_empty(s: &orb::CORBA_String) -> Option<f32> {
    if s.is_empty() {
        None
    } else {
        s.to_string_lossy().parse::<f32>().ok()
    }
}

pub fn opt_duration_if(s: &orb::CORBA_String, is_not: &str) -> Option<Duration> {
    if s.to_bytes() == is_not.as_bytes() {
        None
    } else {
        let s = s.to_string_lossy();
        if let Some(s) = s.strip_suffix("ms") {
            Some(Duration::from_millis(s.parse::<u64>().ok()?))
        } else if let Some(s) = s.strip_suffix("s") {
            Some(Duration::from_secs(s.parse::<u64>().ok()?))
        } else {
            None
        }
    }
}
