//! Type safe device naming.

use std::fmt;
use std::hash::Hash;
use std::num::NonZero;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use std::sync::Arc;


#[derive(thiserror::Error, Debug)]
pub enum ParseTrlError {
    #[error("Invalid protocol, expecting tango://")]
    InvalidProtocol,
    #[error("Invalid host, wrong characters or too long (> 255)")]
    InvalidHost,
    #[error("Invalid port, expecting non-null 16-bit integer")]
    InvalidPort,
    #[error("Invalid hash, expecting #dbase=yes/no")]
    InvalidHash,
    #[error("Invalid or missing path, device-name[/attribute] with device-name being an alias or domain/family/member")]
    InvalidPath,
    #[error("Invalid device name, wrong characters or too long (> 85 per domain/family/member)")]
    InvalidDeviceName,
    #[error("Invalid attribute name, wrong characters or too long (> 255)")]
    InvalidAttrName,
    #[error("Invalid device or attribute alias, wrong characters or too long (> 255)")]
    InvalidAlias,
    #[error("Invalid property name, wrong characters or too long (> 255)")]
    InvalidPropertyName,
}

/// Tango Resource Locator.
/// 
/// Cloning this structure is cheap because it is backed by a Arc.
/// 
/// RFC: 
/// - https://tango-controls.readthedocs.io/projects/rfc/en/latest/16/TangoResourceLocator.html
/// - https://tango-controls.readthedocs.io/projects/rfc/en/latest/2/Device.html
/// - https://tango-controls.readthedocs.io/projects/rfc/en/latest/4/Attribute.html
/// - https://tango-controls.readthedocs.io/projects/rfc/en/latest/5/Property.html
#[derive(Clone)]
pub struct Trl {
    /// The raw string.
    raw: Arc<str>,
    /// If the host and port are present, this is the length of it.
    host_len_and_port: Option<(NonZero<u8>, NonZero<u16>)>,
    /// Offset of where the object starts.
    object_index: u16,
    /// Inner description of the object.
    object: TrlObjectInner,
    /// From RFC 5/Property, maximum property name length is 255.
    property_len: Option<NonZero<u8>>,
    /// True if the tango:// protocol is present.
    protocol: bool,
    /// True if the host/port directly targets a database or not.
    db: bool,
}

/// The length of the device located, including the slashes. This can target either a
/// device or a device attribute, or be a simple alias without slashs. Both device
/// name and alias have max length of 255 if we don't count the slashes.
/// 
/// From RFC 4/Attribute, maximum attr name length is 255.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum TrlObjectInner {
    Alias(NonZero<u8>),
    Dev(NonZero<u8>),
    Attr(NonZero<u8>, NonZero<u8>),
}

impl FromStr for Trl {

    type Err = ParseTrlError;

    fn from_str(raw: &str) -> Result<Self, Self::Err> {

        let mut rem = raw;
        let mut object_offset = 0u16;

        // Check protocol
        let mut protocol = false;
        if let Some((proto, new_rem)) = rem.split_once("://") {
            if proto != "tango" {
                return Err(ParseTrlError::InvalidProtocol);
            }
            object_offset += "tango://".len() as u16;
            protocol = true;
            rem = new_rem;
        }

        // Parse the end hash first.
        let mut db = true;
        if let Some((new_rem, hash)) = rem.rsplit_once('#') {
            db = match hash {
                "dbase=yes" => true,
                "dbase=no" => false,
                _ => return Err(ParseTrlError::InvalidHash),
            };
            rem = new_rem;
        }

        // Then parse the property, we don't validate the property immediately because 
        // acceptable characters depends on if this is device property or attr property.
        let mut property_len = None;
        if let Some((new_rem, property_raw)) = rem.rsplit_once("->") {

            property_len = Some(u8::try_from(property_raw.len()).ok()
                .and_then(NonZero::new)
                .ok_or(ParseTrlError::InvalidPropertyName)?);

            if !property_raw.chars().enumerate().all(is_tango_name_char) {
                return Err(ParseTrlError::InvalidPropertyName);
            }

            rem = new_rem;

        }

        // Only 4 parts at most, construct an array on stack.
        let mut parts = [""; 5];
        let mut parts_count = 0;
        for part in rem.split('/') {
            // If there are more part it's not normal!
            if parts_count >= parts.len() {
                return Err(ParseTrlError::InvalidPath);
            }
            parts[parts_count] = part;
            parts_count += 1;
        }

        
        // Get a slice with the real number of parts.
        let mut parts = &mut parts[..parts_count];
        debug_assert!(!parts.is_empty(), "A split we should have at least one element");

        // Try to find host:port in the first part if possible.
        let mut host_len_and_port = None;
        if let Some((host_raw, port_raw)) = parts[0].split_once(':') {

            // Remove the host:port part from the parts, check if there is still parts.
            parts = &mut parts[1..];
            if parts.is_empty() {
                return Err(ParseTrlError::InvalidPath)
            }

            let host_len = u8::try_from(host_raw.len()).ok()
                .and_then(NonZero::new)
                .ok_or(ParseTrlError::InvalidHost)?;

            if !host_raw.chars().enumerate().all(is_hostname_char) {
                return Err(ParseTrlError::InvalidHost);
            }

            let port = port_raw.parse::<NonZero<u16>>()
                .map_err(|_| ParseTrlError::InvalidPort)?;

            host_len_and_port = Some((host_len, port));

            // +1 for ':', +1 for '/'
            object_offset += host_raw.len() as u16 + 1 + port_raw.len() as u16 + 1;

        }

        // If this is an alias, it can be of length 255.
        let object;
        if parts.len() == 1 {
            
            object = TrlObjectInner::Alias(u8::try_from(parts[0].len()).ok()
                .and_then(NonZero::new)
                .ok_or(ParseTrlError::InvalidAlias)?);

            if !parts[0].chars().enumerate().all(is_tango_name_char) {
                return Err(ParseTrlError::InvalidAlias)
            }

        } else if parts.len() == 3 || parts.len() == 4 {

            let mut dev_len_raw = 0; // Add the 2 slashes to len.
            for dev_part in &parts[..3] {
                
                if dev_part.len() == 0 || dev_part.len() > 85 {
                    return Err(ParseTrlError::InvalidDeviceName);
                }

                if !dev_part.chars().enumerate().all(is_tango_name_char) {
                    return Err(ParseTrlError::InvalidDeviceName);
                }

                dev_len_raw += dev_part.len();

            }

            // Unwrap because we check every part to not be empty.
            let dev_len = NonZero::new(dev_len_raw as u8).unwrap();

            if let Some(&attr_raw) = parts.get(3) {

                object = TrlObjectInner::Attr(dev_len, u8::try_from(attr_raw.len()).ok()
                    .and_then(NonZero::new)
                    .ok_or(ParseTrlError::InvalidAttrName)?);

                if !attr_raw.chars().enumerate().all(is_tango_name_char) {
                    return Err(ParseTrlError::InvalidAttrName);
                }

            } else {
                object = TrlObjectInner::Dev(dev_len);
            }

        } else {
            return Err(ParseTrlError::InvalidPath);
        }

        Ok(Self {
            raw: Arc::from(raw),
            host_len_and_port,
            object_index: object_offset,
            object,
            property_len,
            protocol,
            db,
        })

    }

}

impl Trl {

    /// Return the canonicalized raw string for this TRL.
    #[inline]
    pub fn as_str(&self) -> &str {
        &self.raw
    }

    #[inline]
    fn host_and_port_index(&self) -> usize {
        if self.protocol { "tango://".len() } else { 0 }
    }

    pub fn host_and_port(&self) -> Option<(&str, u16)> {
        let index = self.host_and_port_index();
        self.host_len_and_port.map(|(host_len, port)| {
            (&self.raw[index..][..host_len.get() as usize], port.get())
        })
    }

    #[inline]
    fn object_index(&self) -> usize {
        self.object_index as usize
    }

    pub fn object(&self) -> TrlObject<'_> {
        let index = self.object_index();
        match self.object {
            TrlObjectInner::Alias(alias_len) => {
                TrlObject::Alias(&self.raw[index..][..alias_len.get() as usize])
            }
            TrlObjectInner::Dev(dev_len) => {
                // +2 for slashes that were ignored in length.
                TrlObject::Dev(&self.raw[index..][..dev_len.get() as usize + 2])
            }
            TrlObjectInner::Attr(dev_len, attr_len) => {
                // +2 for slashes that were ignored in length.
                let (dev_name, raw) = self.raw[index..].split_at(dev_len.get() as usize + 2);
                // Start at 1 to ignore the slash between device and attribute.
                let attr_name = &raw[1..][..attr_len.get() as usize];
                TrlObject::Attr(dev_name, attr_name)
            }
        }
    }

    #[inline]
    fn property_index(&self) -> usize {
        self.object_index() + match self.object {
            TrlObjectInner::Alias(alias_len) => 
                alias_len.get() as usize,
            TrlObjectInner::Dev(dev_len) => 
                dev_len.get() as usize + 2,
            TrlObjectInner::Attr(dev_len, attr_len) => 
                dev_len.get() as usize + attr_len.get() as usize + 3,
        } + 2  // +2 for the "->"
    }

    pub fn property(&self) -> Option<&str> {
        let index = self.property_index();
        self.property_len.map(|len| {
            &self.raw[index..][..len.get() as usize]
        })
    }

    #[inline]
    pub fn db(&self) -> bool {
        self.db
    }

}

impl fmt::Debug for Trl {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("Trl").field(&&*self.raw).finish()
    }
}

impl fmt::Display for Trl {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.raw)
    }
}

/// The object pointed to by a Tango Resource Locator.
pub enum TrlObject<'a> {
    /// An alias for a device or attribute.
    Alias(&'a str),
    /// A device name.
    Dev(&'a str),
    /// An attribute name with its device name.
    Attr(&'a str, &'a str),
}

/// From the RFC.
fn is_hostname_char((i, c): (usize, char)) -> bool {
    c.is_ascii_alphanumeric() || (i != 0 && (c == '-' || c == '.'))
}

/// From the RFC. 
/// 
/// With added:
/// - Dash as non first char '-'
/// - Digits as first char
fn is_tango_name_char((i, c): (usize, char)) -> bool {
    c.is_ascii_alphanumeric() || (i != 0 && (c == '_' || c == '-'))
}

// fn is_tango_attr_property_name_char((i, c): (usize, char)) -> bool {
//     c.is_ascii_alphabetic() || c == '_' || (i != 0 && c.is_ascii_digit())
// }

pub trait IntoTrl {
    fn into_trl(self) -> Result<Trl, ParseTrlError>;
}

impl IntoTrl for Trl {
    fn into_trl(self) -> Result<Trl, ParseTrlError> {
        Ok(self)
    }
}

impl IntoTrl for &'_ str {
    fn into_trl(self) -> Result<Trl, ParseTrlError> {
        Trl::from_str(self)
    }
}

/// A type-safe representation of a Tango device name.
/// 
/// RFC: https://tango-controls.readthedocs.io/projects/rfc/en/latest/2/Device.html
#[repr(transparent)]
pub struct DevName(str);

impl DevName {

    pub unsafe fn new_unchecked<'a>(raw: &'a str) -> &'a Self {
        unsafe { &*(raw as *const str as *const Self) }
    }

    pub unsafe fn new_unchecked_mut<'a>(raw: &'a mut str) -> &'a mut Self {
        unsafe { &mut *(raw as *mut str as *mut Self) }
    }

    #[inline]
    pub fn domain(&self) -> &str {
        self.0.split('/').nth(0).unwrap()
    }

    #[inline]
    pub fn family(&self) -> &str {
        self.0.split('/').nth(1).unwrap()
    }

    #[inline]
    pub fn member(&self) -> &str {
        self.0.split('/').nth(2).unwrap()
    }

    #[inline]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    #[inline]
    pub fn to_buf(&self) -> DevNameBuf {
        DevNameBuf { inner: self.0.to_string() }
    }

}

/// A mutable type-safe representation of a Tango device name.
pub struct DevNameBuf {
    inner: String,
}

impl Deref for DevNameBuf {

    type Target = DevName;

    #[inline]
    fn deref(&self) -> &Self::Target {
        unsafe { &*(self.inner.as_str() as *const str as *const DevName) }
    }

}

impl DerefMut for DevNameBuf {

    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { &mut *(self.inner.as_mut_str() as *mut str as *mut DevName) }
    }

}


#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn parse_and_get() {

        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
        enum TrlObjectKind {
            Alias,
            Dev,
            Attr,
        }

        for protocol in [false, true] {
            for host_and_port in [false, true] {
                for object in [TrlObjectKind::Alias, TrlObjectKind::Dev, TrlObjectKind::Attr] {
                    for property in [false, true] {
                        for db in [None, Some(false), Some(true)] {
                        
                            let mut buf = String::new();
                            
                            if protocol {
                                buf.push_str("tango://");
                            }

                            if host_and_port {
                                buf.push_str("example.com:10000/");
                            }

                            let object_index = buf.len() as u16;
                            match object {
                                TrlObjectKind::Alias => buf.push_str("alias"),
                                TrlObjectKind::Dev => buf.push_str("domain/family/member"),
                                TrlObjectKind::Attr => buf.push_str("domain/family/member/attribute"),
                            }

                            if property {
                                buf.push_str("->property");
                            }

                            match db {
                                Some(false) => buf.push_str("#dbase=no"),
                                Some(true) => buf.push_str("#dbase=yes"),
                                None => (),
                            }

                            let trl = Trl::from_str(&buf).unwrap();

                            assert_eq!(trl.protocol, protocol);
                            assert_eq!(trl.host_len_and_port, host_and_port.then_some((NonZero::new(11u8).unwrap(), NonZero::new(10000u16).unwrap())));
                            assert_eq!(trl.object_index, object_index);
                            assert_eq!(trl.object, match object {
                                TrlObjectKind::Alias => TrlObjectInner::Alias(NonZero::new(5u8).unwrap()),
                                TrlObjectKind::Dev => TrlObjectInner::Dev(NonZero::new(18u8).unwrap()),
                                TrlObjectKind::Attr => TrlObjectInner::Attr(NonZero::new(18u8).unwrap(), NonZero::new(9u8).unwrap()),
                            });
                            assert_eq!(trl.property_len, property.then_some(NonZero::new(8u8).unwrap()));
                            assert_eq!(trl.db, db.unwrap_or(true));

                        }
                    }
                }
            }
        }

    }

}
