//! Conversion of data from and into CORBA::Any.

use tango_omniorb as orb;

use crate::backend::{DevCommandBackend, DevGetCommandInfoBackend};
use crate::data::{Data, DataLayout, DataLayoutView, DataStruct, DataView};
use crate::dev::{CommandInfo, DevState, DisplayLevel};
use crate::error::{Error, ErrorKind, Result};
use crate::array::MdView;

use super::convert::{data_kind_from_type, opt_str_if, S_UNINITIALIZED};


/// Convert a device data into a `CORBA::Any` type by using tango types.
pub fn command_data_to_any(buf: Data<'_>) -> Result<orb::CORBA_Any> {

    let mut any = orb::CORBA_Any::default();

    match buf.as_view() {
        DataView::Void => (),
        DataView::Bool(DataLayoutView::Scalar(&val)) => any.insert_boolean(val),
        DataView::Octet(DataLayoutView::Scalar(&val)) => any.insert_octet(val),
        DataView::UShort(DataLayoutView::Scalar(&val)) => any.insert_ushort(val),
        DataView::Short(DataLayoutView::Scalar(&val)) => any.insert_short(val),
        DataView::ULong(DataLayoutView::Scalar(&val)) => any.insert_ulong(val),
        DataView::Long(DataLayoutView::Scalar(&val)) => any.insert_long(val),
        DataView::ULong64(DataLayoutView::Scalar(&val)) => any.insert_ulonglong(val),
        DataView::Long64(DataLayoutView::Scalar(&val)) => any.insert_longlong(val),
        DataView::Float(DataLayoutView::Scalar(&val)) => any.insert_float(val),
        DataView::Double(DataLayoutView::Scalar(&val)) => any.insert_double(val),
        DataView::State(DataLayoutView::Scalar(&val)) => any.insert(orb::Tango_DevState::from(val)),
        DataView::String(DataLayoutView::Scalar(val)) => {
            any.insert_string_move(orb::CORBA_String::try_from(&**val)
                .map_err(|()| Error::new(ErrorKind::InvalidData)
                    .with_append_description("String cannot be converted to CORBA::String")
                    .with_append_description(format_args!("{val:?}")))?);
        }
        DataView::Bool(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarBooleanArray>()));
        }
        DataView::Octet(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarCharArray>()));
        }
        DataView::UShort(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarUShortArray>()));
        }
        DataView::Short(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarShortArray>()));
        }
        DataView::ULong(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarULongArray>()));
        }
        DataView::Long(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarLongArray>()));
        }
        DataView::ULong64(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarULong64Array>()));
        }
        DataView::Long64(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarLong64Array>()));
        }
        DataView::Float(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarFloatArray>()));
        }
        DataView::Double(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter().copied()
                .collect::<orb::Tango_DevVarDoubleArray>()));
        }
        DataView::State(DataLayoutView::Spectrum(view)) => {
            any.insert_move(orb::CppBox::new(view.as_raw_slice().iter()
                .map(|&s| orb::Tango_DevState::from(s))
                .collect::<orb::Tango_DevVarStateArray>()));
        }
        DataView::String(DataLayoutView::Spectrum(view)) => {
            let mut arr = orb::CppBox::new(orb::Tango_DevVarStringArray(orb::CORBA_Sequence::with_capacity(view.raw_len())));
            for val in view.as_raw_slice() {
                match orb::CORBA_String::try_from(&**val) {
                    Ok(s) => arr.push(s),
                    Err(_) => return Err(Error::new(ErrorKind::InvalidData)
                        .with_append_description("String cannot be converted to CORBA::String")
                        .with_append_description(format_args!("{val:?}"))),
                };
            }
            any.insert_move(arr);
        }
        DataView::Struct(data) => {
            
            if data.len() != 2 {
                return Err(Error::new(ErrorKind::InvalidData)
                    .with_append_description("Struct cannot be converted to CORBA::Any")
                    .with_append_description(format_args!("{data:?}")));
            }

            let svalue = match data.get(1).unwrap() {
                DataView::String(DataLayoutView::Spectrum(view)) => {
                    let mut arr = orb::Tango_DevVarStringArray(orb::CORBA_Sequence::with_capacity(view.raw_len()));
                    for val in view.as_raw_slice() {
                        match orb::CORBA_String::try_from(&**val) {
                            Ok(s) => arr.push(s),
                            Err(_) => return Err(Error::new(ErrorKind::InvalidData)
                                .with_append_description("String cannot be converted to CORBA::String")
                                .with_append_description(format_args!("{val:?}"))),
                        };
                    }
                    arr
                }
                _ => return Err(Error::new(ErrorKind::InvalidData)
                        .with_append_description("Data struct cannot be converted to CORBA::Any because second field should be a string sequence")
                        .with_append_description(format_args!("{data:?}")))
            };

            match data.get(0).unwrap() {
                DataView::Long(DataLayoutView::Spectrum(view)) => {
                    any.insert_move(orb::CppBox::new(orb::Tango_DevVarLongStringArray {
                        lvalue: view.as_raw_slice().iter().copied()
                            .collect::<orb::Tango_DevVarLongArray>(),
                        svalue,
                    }));
                }
                DataView::Double(DataLayoutView::Spectrum(view)) => {
                    any.insert_move(orb::CppBox::new(orb::Tango_DevVarDoubleStringArray {
                        dvalue: view.as_raw_slice().iter().copied()
                            .collect::<orb::Tango_DevVarDoubleArray>(),
                        svalue,
                    }));
                }
                _ => return Err(Error::new(ErrorKind::InvalidData)
                    .with_append_description("Struct cannot be converted to CORBA::Any because first field should be a long/double sequence")
                    .with_append_description(format_args!("{data:?}")))
            }

        }
        _ => return Err(Error::new(ErrorKind::InvalidData)
            .with_append_description("Cannot be converted to CORBA::Any")
            .with_append_description(format_args!("{buf:?}")))
    }

    Ok(any)

}

/// A lazy abstract buffer that extract values from Any, and returns a Data borrowing its
/// inner structures.
#[derive(Debug)]
pub struct DevCommandFromAny {
    any: orb::CORBA_Any,
}

impl DevCommandFromAny {

    #[inline]
    pub fn new(any: orb::CORBA_Any) -> Self {
        Self {
            any,
        }
    }

    fn get_inner(&self) -> Option<Data<'_>> {

        let tc = self.any.type_code()?;

        let data = match tc.kind() {
            orb::CORBA_TypeCodeKind::Null => Data::Void,
            orb::CORBA_TypeCodeKind::Boolean =>
                Data::Bool(DataLayout::Scalar(self.any.extract_boolean()?)),
            orb::CORBA_TypeCodeKind::UShort =>
                Data::UShort(DataLayout::Scalar(self.any.extract_ushort()?)),
            orb::CORBA_TypeCodeKind::Short =>
                Data::Short(DataLayout::Scalar(self.any.extract_short()?)),
            orb::CORBA_TypeCodeKind::ULong =>
                Data::ULong(DataLayout::Scalar(self.any.extract_ulong()?)),
            orb::CORBA_TypeCodeKind::Long =>
                Data::Long(DataLayout::Scalar(self.any.extract_long()?)),
            orb::CORBA_TypeCodeKind::ULongLong =>
                Data::ULong64(DataLayout::Scalar(self.any.extract_ulonglong()?)),
            orb::CORBA_TypeCodeKind::LongLong =>
                Data::Long64(DataLayout::Scalar(self.any.extract_longlong()?)),
            orb::CORBA_TypeCodeKind::Float =>
                Data::Float(DataLayout::Scalar(self.any.extract_float()?)),
            orb::CORBA_TypeCodeKind::Double =>
                Data::Double(DataLayout::Scalar(self.any.extract_double()?)),
            orb::CORBA_TypeCodeKind::Enum => {
                Data::State(DataLayout::Scalar(self.any.extract::<orb::Tango_DevState>()?.into()))
            }
            orb::CORBA_TypeCodeKind::String => {
                Data::String(DataLayout::Scalar(self.any.extract_string_borrow()?.to_string_lossy()))
            }
            orb::CORBA_TypeCodeKind::Alias => {

                let tc_seq = tc.content_type()?;
                
                if tc_seq.kind() != orb::CORBA_TypeCodeKind::Sequence {
                    return None;
                }

                let tc_item = tc_seq.content_type()?;

                match tc_item.kind() {
                    orb::CORBA_TypeCodeKind::Boolean => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarBooleanArray>()?;
                        Data::Bool(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::UShort => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarUShortArray>()?;
                        Data::UShort(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::Short => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarShortArray>()?;
                        Data::Short(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::ULong => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarULongArray>()?;
                        Data::ULong(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::Long => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarLongArray>()?;
                        Data::Long(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::ULongLong => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarULong64Array>()?;
                        Data::ULong64(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::LongLong => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarLong64Array>()?;
                        Data::Long64(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::Float => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarFloatArray>()?;
                        Data::Float(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::Double => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarDoubleArray>()?;
                        Data::Double(DataLayout::SpectrumView(MdView::from(&arr[..])))
                    }
                    orb::CORBA_TypeCodeKind::Enum => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarStateArray>()?;
                        Data::State(DataLayout::Spectrum(arr.iter()
                            .map(|&s| Into::<DevState>::into(s))
                            .collect::<Vec<_>>()
                            .into()))
                    }
                    orb::CORBA_TypeCodeKind::String => {
                        let arr = self.any.extract_borrow::<orb::Tango_DevVarStringArray>()?;
                        Data::String(DataLayout::Spectrum(arr.iter()
                            .map(|s| s.to_string_lossy())
                            .collect::<Vec<_>>()
                            .into()))
                    }
                    _ => return None,
                }

            }
            orb::CORBA_TypeCodeKind::Struct => {
                
                let svalue;
                let data0 = if let Some(multi) = self.any.extract_borrow::<orb::Tango_DevVarLongStringArray>() {
                    svalue = &multi.svalue;
                    Data::Long(DataLayout::SpectrumView(MdView::from(&multi.lvalue[..])))
                } else if let Some(multi) = self.any.extract_borrow::<orb::Tango_DevVarDoubleStringArray>() {
                    svalue = &multi.svalue;
                    Data::Double(DataLayout::SpectrumView(MdView::from(&multi.dvalue[..])))
                } else {
                    return None;
                };

                let data1 = Data::String(DataLayout::Spectrum(svalue.iter()
                    .map(|s| s.to_string_lossy())
                    .collect::<Vec<_>>()
                    .into()));

                Data::Struct(DataStruct::Owned(Box::new([data0, data1])))

            }
            _ => return None,
        };

        Some(data)

    }

}

impl DevCommandBackend for DevCommandFromAny {

    fn get(&self) -> Result<Data<'_>> {
        self.get_inner().ok_or_else(|| Error::new(ErrorKind::InvalidData)
            .with_append_description("Internal CORBA::Any is not supported")
            .with_append_description(format_args!("{:?}", self.any)))
    }

}

/// The raw list that we are iterating over.
#[derive(Debug)]
pub struct DevGetCommandInfoMany<R> {
    raw_list: orb::CppBox<R>,
    mapping: Option<Vec<Option<usize>>>,
}

/// Internal extension to the `DevGetCommandInfoMany` that is implemented on top of
/// the `DevGetCommandInfoBackend` to initialize the names mapping from requested names.
pub trait DevGetCommandInfoBackendExt: DevGetCommandInfoBackend {
    fn init_mapping(&mut self, names: &[&str]);
}

impl<R> DevGetCommandInfoMany<R> {

    /// Create this structure with no mapping, thus returning all items by default. Use
    /// [`DevGetCommandInfoBackendExt::init_mapping`] to reduce the set of items returned.
    #[inline]
    pub fn new(raw_list: orb::CppBox<R>) -> Self {
        Self {
            raw_list,
            mapping: None,
        }
    }

}

impl DevGetCommandInfoBackend for DevGetCommandInfoMany<orb::Tango_DevCmdInfoList> {
    
    fn len(&self) -> usize {
        self.mapping.as_ref()
            .map(|mapping| mapping.len())
            .unwrap_or(self.raw_list.len())
    }

    fn get(&self, index: usize) -> Result<CommandInfo<'_>> {

        let index = match &self.mapping {
            Some(mapping) => mapping.get(index)
                .ok_or_else(|| Error::new(ErrorKind::InvalidIndex))?
                .ok_or_else(|| Error::new(ErrorKind::CommandNotFound))?,
            None if index < self.raw_list.len() => index,
            None => return Err(Error::new(ErrorKind::InvalidIndex)),
        };

        command_info_from_v1(&self.raw_list[index])

    }

}

impl DevGetCommandInfoBackendExt for DevGetCommandInfoMany<orb::Tango_DevCmdInfoList> {
    fn init_mapping(&mut self, names: &[&str]) {
        self.mapping = Some(names.iter()
            .map(|&name| self.raw_list.iter().position(|info| info.cmd_name.to_bytes() == name.as_bytes()))
            .collect())
    }
}

impl DevGetCommandInfoBackend for DevGetCommandInfoMany<orb::Tango_DevCmdInfoList_2> {
    
    fn len(&self) -> usize {
        self.mapping.as_ref()
            .map(|mapping| mapping.len())
            .unwrap_or(self.raw_list.len())
    }

    fn get(&self, index: usize) -> Result<CommandInfo<'_>> {
        
        let index = match &self.mapping {
            Some(mapping) => mapping.get(index)
                .ok_or_else(|| Error::new(ErrorKind::InvalidIndex))?
                .ok_or_else(|| Error::new(ErrorKind::CommandNotFound))?,
            None if index < self.raw_list.len() => index,
            None => return Err(Error::new(ErrorKind::InvalidIndex)),
        };

        command_info_from_v2(&self.raw_list[index])

    }

}

impl DevGetCommandInfoBackendExt for DevGetCommandInfoMany<orb::Tango_DevCmdInfoList_2> {
    fn init_mapping(&mut self, names: &[&str]) {
        self.mapping = Some(names.iter()
            .map(|&name| self.raw_list.iter().position(|info| info.cmd_name.to_bytes() == name.as_bytes()))
            .collect())
    }
}

/// The raw list that we are iterating over.
#[derive(Debug)]
pub struct DevGetCommandInfoSingle<R> {
    raw_info: orb::CppBox<R>,
}

impl<R> DevGetCommandInfoSingle<R> {

    #[inline]
    pub fn new(raw_info: orb::CppBox<R>) -> Self {
        Self {
            raw_info,
        }
    }

}

impl DevGetCommandInfoBackend for DevGetCommandInfoSingle<orb::Tango_DevCmdInfo> {

    fn len(&self) -> usize {
        1
    }

    fn get(&self, index: usize) -> Result<CommandInfo<'_>> {
        if index == 0 {
            command_info_from_v1(&self.raw_info)
        } else {
            Err(Error::new(ErrorKind::InvalidIndex))
        }
    }

}

impl DevGetCommandInfoBackend for DevGetCommandInfoSingle<orb::Tango_DevCmdInfo_2> {

    fn len(&self) -> usize {
        1
    }

    fn get(&self, index: usize) -> Result<CommandInfo<'_>> {
        if index == 0 {
            command_info_from_v2(&self.raw_info)
        } else {
            Err(Error::new(ErrorKind::InvalidIndex))
        }
    }

}

/// Attribute info from a raw `Tango::DevCmdInfo` (v1).
pub fn command_info_from_v1(raw_conf: &orb::Tango_DevCmdInfo) -> Result<CommandInfo<'_>> {
    Ok(CommandInfo {
        name: raw_conf.cmd_name.to_string_lossy(),
        display_level: match raw_conf.cmd_tag {
            0 => DisplayLevel::Operator,
            1 => DisplayLevel::Expert,
            _ => return Err(Error::new(ErrorKind::InvalidData)
                .with_append_description(format_args!("Invalid command tag: {:02X}", raw_conf.cmd_tag)))
        },
        in_kind: data_kind_from_type(raw_conf.in_type, None, None)
            .ok_or_else(|| Error::new(ErrorKind::InvalidData)
                .with_append_description(format_args!("Unexpected raw tango data type for input: {}", raw_conf.in_type)))?,
        out_kind: data_kind_from_type(raw_conf.in_type, None, None)
            .ok_or_else(|| Error::new(ErrorKind::InvalidData)
                .with_append_description(format_args!("Unexpected raw tango data type for output: {}", raw_conf.in_type)))?,
        in_description: opt_str_if(&raw_conf.in_type_desc, S_UNINITIALIZED),
        out_description: opt_str_if(&raw_conf.out_type_desc, S_UNINITIALIZED),
    })
}

/// Attribute info from a raw `Tango::DevCmdInfo` (v2).
pub fn command_info_from_v2(raw_conf: &orb::Tango_DevCmdInfo_2) -> Result<CommandInfo<'_>> {
    Ok(CommandInfo {
        name: raw_conf.cmd_name.to_string_lossy(),
        display_level: raw_conf.level.into(),
        in_kind: data_kind_from_type(raw_conf.in_type, None, None)
            .ok_or_else(|| Error::new(ErrorKind::InvalidData)
                .with_append_description(format_args!("Unexpected raw tango data type for input: {}", raw_conf.in_type)))?,
        out_kind: data_kind_from_type(raw_conf.out_type, None, None)
            .ok_or_else(|| Error::new(ErrorKind::InvalidData)
                .with_append_description(format_args!("Unexpected raw tango data type for output: {}", raw_conf.in_type)))?,
        in_description: opt_str_if(&raw_conf.in_type_desc, S_UNINITIALIZED),
        out_description: opt_str_if(&raw_conf.out_type_desc, S_UNINITIALIZED),
    })
}
