//! The standard Tango error type.

use std::error::{self, Error as _};
use std::fmt::{self, Display};
use std::panic::Location;
use std::borrow::Cow;


/// Type alias for a result with the standard [`Tango`] error type.
pub type Result<T> = std::result::Result<T, Error>;

/// A common error type for all Tango operations, which support metadata and easy to build
/// chaining of errors.
#[derive(Debug)]
#[repr(transparent)]
pub struct Error {
    inner: Box<ErrorInner>,
}

#[derive(Debug)]
struct ErrorInner {
    /// The standardized reason of the error.
    reason: Cow<'static, str>,
    /// The human readable description of the error.
    description: Cow<'static, str>,
    /// The origin, in file/code, of the error.
    origin: Cow<'static, str>,
    /// The severity of the error.
    severity: ErrorSeverity,
    /// If thre reason was corresponding to a non-raw kind, then it's cached here.
    kind: Option<ErrorKind<'static>>,
    /// The source of the error.
    source: Option<ErrorSource>,
}

#[derive(Debug)]
enum ErrorSource {
    /// The parent of the error is another Tango error.
    Parent(Error),
    /// A generic tango error.
    Generic(Box<dyn error::Error + Send + Sync>),
}

impl Error {

    /// Create a new error of the given kind, this will grab the origin of the error from
    /// the caller's location. A default description is written depending on it by default,
    /// you can append or replace this description later.
    #[track_caller]
    pub fn new(kind: ErrorKind<'_>) -> Self {
        let mut ret = Self {
            inner: Box::new(ErrorInner {
                reason: Cow::Borrowed(""),
                description: Cow::Borrowed(""),
                origin: Cow::Borrowed(""),
                severity: ErrorSeverity::Error,
                kind: None,
                source: None,
            })
        };
        ret.set_kind(kind);
        ret.set_origin_from_caller();
        ret
    }

    /// Return the standardized textual reason.
    pub fn reason(&self) -> &str {
        &self.inner.reason
    }

    /// Get the kind of error, returning the raw reason if not categorized.
    pub fn kind(&self) -> ErrorKind<'_> {
        if let Some(kind) = self.inner.kind {
            kind
        } else {
            ErrorKind::Reason(&self.inner.reason)
        }
    }

    /// Change the kind of this error, this will also ensures that the associated reason
    /// is correponding to the new error kind. If the description is empty at this point,
    /// the description os overriden with a default description.
    pub fn set_kind(&mut self, kind: ErrorKind<'_>) {
        
        let (reason, kind) = match kind.into_static() {
            Ok(kind) => (Cow::Borrowed(kind.reason()), Some(kind)),
            Err(kind) => (Cow::Owned(kind.reason().to_string()), None),
        };

        self.inner.kind = kind;
        self.inner.reason = reason;

    }

    /// Change the kind of this error, and return itself.
    pub fn with_kind(mut self, kind: ErrorKind<'_>) -> Self {
        self.set_kind(kind);
        self
    }

    /// Return the description of the error.
    pub fn description(&self) -> &str {
        &self.inner.description
    }

    /// Append the given additional description to the error's description, if the error
    /// is currently empty, then this replaces the previous description. If a description
    /// already exists, it appends the description on a new line.
    pub fn append_description(&mut self, additional: impl Display) {
        if self.inner.description.is_empty() {
            self.inner.description = Cow::Owned(additional.to_string());
        } else {
            self.inner.description = Cow::Owned(format!("{}\n{additional}", self.inner.description));
        }
    }

    /// Overwrite any existing description with the given one.
    pub fn set_description(&mut self, description: impl Into<Cow<'static, str>>) {
        self.inner.description = description.into();
    }

    /// Same as [`Self::append_description`], and return itself.
    pub fn with_append_description(mut self, additional: impl Display) -> Self {
        self.append_description(additional);
        self
    }

    /// Overwrite any existing description with the given one, and return itself.
    pub fn with_description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
        self.set_description(description);
        self
    }

    /// Return the severity of this error.
    pub fn severity(&self) -> ErrorSeverity {
        self.inner.severity
    }

    /// Set the severity of the error.
    pub fn set_severity(&mut self, severity: ErrorSeverity) {
        self.inner.severity = severity;
    }

    /// Set the severity of the error, and return itself.
    pub fn with_severity(mut self, severity: ErrorSeverity) -> Self {
        self.set_severity(severity);
        self
    }

    /// Return the origin of this error, set by default to the caller's code location.
    pub fn origin(&self) -> &str {
        &self.inner.origin
    }

    /// Append the given additional origin to the existing origin, in order to add 
    /// details, a space is added before insertion. If the origin has been replaced 
    /// and is empty, then no space is added.
    pub fn append_origin(&mut self, additional: impl Display) {
        if self.inner.origin.is_empty() {
            self.inner.origin = Cow::Owned(additional.to_string());
        } else {
            self.inner.origin = Cow::Owned(format!("{} {additional}", self.inner.origin));
        }
    }

    /// Overwrite any existing origin with the given one.
    pub fn set_origin(&mut self, origin: impl Into<Cow<'static, str>>) {
        self.inner.origin = origin.into();
    }

    /// Same as [`Self::append_origin`], and return itself.
    pub fn with_append_origin(mut self, additional: impl Display) -> Self {
        self.append_origin(additional);
        self
    }

    /// Overwrite any existing origin with the given one, and return itself.
    pub fn with_origin(mut self, origin: impl Into<Cow<'static, str>>) -> Self {
        self.set_origin(origin);
        self
    }

    /// Overwrite the existing origin with the current location of the caller.
    #[track_caller]
    pub fn set_origin_from_caller(&mut self) {
        let location = Location::caller();
        self.set_origin(format!("{location}"));
    }

    /// Overwrite the existing origin with the current location of the caller, and return 
    /// itself.
    #[track_caller]
    pub fn with_origin_from_caller(mut self) -> Self {
        self.set_origin_from_caller();
        self
    }

    /// If the source of this error is a parent Tango error, returns a shared ref to it.
    /// To get a more generic source for this error, use the standard 
    /// [`error::Error::source`] method.
    pub fn parent(&self) -> Option<&Error> {
        if let Some(ErrorSource::Parent(err)) = &self.inner.source {
            Some(err)
        } else {
            None
        }
    }

    /// Set the parent source of this error.
    pub fn set_parent(&mut self, parent: Error) {
        self.inner.source = Some(ErrorSource::Parent(parent));
    }

    /// Set the parent source of this error and return itself.
    pub fn with_parent(mut self, parent: Error) -> Self {
        self.set_parent(parent);
        self
    }

    /// Set the generic source of this error.
    pub fn set_source(&mut self, source: Box<dyn error::Error + Send + Sync>) {
        self.inner.source = Some(ErrorSource::Generic(source));
    }

    /// Set the generic source of this error and return itself.
    pub fn with_source(mut self, source: Box<dyn error::Error + Send + Sync>) -> Self {
        self.set_source(source);
        self
    }

}

impl error::Error for Error {

    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match &self.inner.source {
            Some(ErrorSource::Parent(err)) => Some(err),
            Some(ErrorSource::Generic(err)) => Some(&**err),
            None => None,
        }
    }

}

impl Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        
        let severity = match self.severity() {
            ErrorSeverity::Warning => "WARNING",
            ErrorSeverity::Error => "ERROR",
            ErrorSeverity::Panic => "PANIC",
        };
        write!(f, "{severity} {}", self.reason())?;
        for line in self.description().lines() {
            write!(f, "\n  {line}")?;
        }
        
        if let Some(parent) = self.parent() {
            write!(f, "\n{parent}")?;
        } else if let Some(source) = self.source() {
            write!(f, "\nRAW {source}")?;
        }
        
        Ok(())

    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind<'r> {
    NotHandled,
    DatabaseNotConnected,
    DatabaseNeeded,
    AliasNotFound,
    DevNotFound,
    ServerNotConnected,
    DevNotConnected,
    CommandNameInvalid,
    CommandNotFound,
    CommandArgumentKindInvalid,
    AttrNameInvalid,
    AttrNotFound,
    AttrNotWritable,
    AttrKindInvalid,
    EventNotConnected,
    InvalidIndex,
    InvalidData,
    TimedOut,
    Internal,
    Reason(&'r str),
}

impl<'r> ErrorKind<'r> {

    fn into_static(self) -> std::result::Result<ErrorKind<'static>, Self> {
        Ok(match self {
            ErrorKind::NotHandled => ErrorKind::NotHandled,
            ErrorKind::DatabaseNotConnected => ErrorKind::DatabaseNotConnected,
            ErrorKind::DatabaseNeeded => ErrorKind::DatabaseNeeded,
            ErrorKind::AliasNotFound => ErrorKind::AliasNotFound,
            ErrorKind::DevNotFound => ErrorKind::DevNotFound,
            ErrorKind::ServerNotConnected => ErrorKind::ServerNotConnected,
            ErrorKind::DevNotConnected => ErrorKind::DevNotConnected,
            ErrorKind::CommandNameInvalid => ErrorKind::CommandNameInvalid,
            ErrorKind::CommandNotFound => ErrorKind::CommandNotFound,
            ErrorKind::CommandArgumentKindInvalid => ErrorKind::CommandArgumentKindInvalid,
            ErrorKind::AttrNameInvalid => ErrorKind::AttrNameInvalid,
            ErrorKind::AttrNotFound => ErrorKind::AttrNotFound,
            ErrorKind::AttrNotWritable => ErrorKind::AttrNotWritable,
            ErrorKind::AttrKindInvalid => ErrorKind::AttrKindInvalid,
            ErrorKind::EventNotConnected => ErrorKind::EventNotConnected,
            ErrorKind::InvalidIndex => ErrorKind::InvalidIndex,
            ErrorKind::InvalidData => ErrorKind::InvalidData,
            ErrorKind::TimedOut => ErrorKind::TimedOut,
            ErrorKind::Internal => ErrorKind::Internal,
            ErrorKind::Reason(_) => return Err(self),
        })
    }

    /// Return the textual and standard reason for this 
    pub fn reason(&self) -> &'r str {
        match self {
            ErrorKind::NotHandled => reason::API_InternalError,
            ErrorKind::DatabaseNotConnected => reason::API_CantConnectToDatabase,
            ErrorKind::DatabaseNeeded => reason::API_InternalError,
            ErrorKind::AliasNotFound => reason::API_AliasNotDefined,
            ErrorKind::DevNotFound => reason::API_DeviceNotExported,
            ErrorKind::ServerNotConnected => reason::API_ServerNotRunning,
            ErrorKind::DevNotConnected => reason::API_DeviceNotDefined,
            ErrorKind::CommandNameInvalid => reason::API_InternalError,
            ErrorKind::CommandNotFound => reason::API_CommandNotFound,
            ErrorKind::CommandArgumentKindInvalid => reason::API_IncompatibleCmdArgumentType,
            ErrorKind::AttrNameInvalid => reason::API_InternalError,
            ErrorKind::AttrNotFound => reason::API_AttrNotFound,
            ErrorKind::AttrNotWritable => reason::API_AttrNotWritable,
            ErrorKind::AttrKindInvalid => reason::API_IncompatibleAttrDataType,
            ErrorKind::EventNotConnected => reason::API_EventNotFound,
            ErrorKind::InvalidIndex => reason::API_InternalError,
            ErrorKind::InvalidData => reason::API_InternalError,
            ErrorKind::TimedOut => reason::API_DeviceTimedOut,
            ErrorKind::Internal => reason::API_InternalError,
            ErrorKind::Reason(reason) => reason,
        }
    }

    /// Return a statically defined description of the error.
    pub fn description(&self) -> &'static str {
        match self {
            ErrorKind::NotHandled => "No backend is able to handle such request",
            ErrorKind::DatabaseNotConnected => "Not able to connect to any database in order to find the device",
            ErrorKind::DatabaseNeeded => "This operation is only allowed if the device is connected through a database",
            ErrorKind::AliasNotFound => "Alias was not found in the database",
            ErrorKind::DevNotFound => "Device was not found in the database",
            ErrorKind::ServerNotConnected => "Device server hosting the device is not connected",
            ErrorKind::DevNotConnected => "Device was not found on its device server",
            ErrorKind::CommandNameInvalid => "Command name may contain invalid characters or is too long",
            ErrorKind::CommandNotFound => "The command was not found on the device",
            ErrorKind::CommandArgumentKindInvalid => "Invalid command argument data kind",
            ErrorKind::AttrNameInvalid => "Attribute name may contain invalid characters or is too long",
            ErrorKind::AttrNotFound => "The attribute was not found on the device",
            ErrorKind::AttrNotWritable => "The attribute is not writable",
            ErrorKind::AttrKindInvalid => "Invalid attribute data kind",
            ErrorKind::EventNotConnected => "Not able to connect to an event for subscribing",
            ErrorKind::InvalidIndex => "Invalid index for data",
            ErrorKind::InvalidData => "Failed to handle data",
            ErrorKind::TimedOut => "The network operation timed out",
            ErrorKind::Internal => "",
            ErrorKind::Reason(_) => "",
        }
    }

}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorSeverity {
    Warning,
    Error,
    Panic,
}

/// Standard device failed reasons.
#[allow(non_upper_case_globals)]
pub mod reason {

    macro_rules! define_reasons {
        ( $($reason:ident),* $(,)? ) => {
            $( pub const $reason: &str = stringify!($reason); )*
        };
    }

    define_reasons![
        API_AliasNotDefined,
        API_AlreadyPolled,
        API_AsynReplyNotArrived,
        API_AttrConfig,
        API_AttrEventProp,
        API_AttributeFailed,
        API_AttributeNotDataReadyEnabled,
        API_AttributePollingNotStarted,
        API_AttrIncorrectDataNumber,
        API_AttrNoAlarm,
        API_AttrNotAllowed,
        API_AttrNotFound,
        API_AttrNotPolled,
        API_AttrNotWritable,
        API_AttrOptProp,
        API_AttrPropValueNotSet,
        API_AttrValueNotSet,
        API_AttrWrongDefined,
        API_AttrWrongMemValue,
        API_BadAsyn,
        API_BadAsynPollId,
        API_BadAsynReqType,
        API_BadConfigurationProperty,
        API_BlackBoxArgument,
        API_BlackBoxEmpty,
        API_CannotCheckAccessControl,
        API_CannotOpenFile,
        API_CantActivatePOAManager,
        API_CantConnectToDatabase,
        API_CantConnectToDevice,
        API_CantCreateClassPoa,
        API_CantCreateLockingThread,
        API_CantDestroyDevice,
        API_CantFindLockingThread,
        API_CantGetClientIdent,
        API_CantGetDevObjectId,
        API_CantInstallSignal,
        API_CantRetrieveClass,
        API_CantRetrieveClassList,
        API_CantStoreDeviceClass,
        API_ClassNotFound,
        API_ClientTooOld,
        API_CmdArgumentTypeNotSupported,
        API_CmdNotPolled,
        API_CommandFailed,
        API_CommandNotAllowed,
        API_CommandNotFound,
        API_CommandTimedOut,
        API_CommunicationFailed,
        API_ConnectionFailed,
        API_CorbaException,
        API_CorbaSysException,
        API_CorruptedDatabase,
        API_DatabaseAccess,
        API_DatabaseCacheAccess,
        API_DatabaseFileError,
        API_DecodeErr,
        API_EncodeErr,
        API_DeprecatedCommand,
        API_DeviceLocked,
        API_DeviceNotDefined,
        API_DeviceNotExported,
        API_DeviceNotFound,
        API_DeviceNotLocked,
        API_DeviceNotPolled,
        API_DeviceTimedOut,
        API_DeviceUnlockable,
        API_DeviceUnlocked,
        API_DServerClassNotInitialised,
        API_DSFailedRegisteringEvent,
        API_DummyException,
        API_EmptyDataElement,
        API_EmptyDbDatum,
        API_EmptyDbDbDatum,
        API_EmptyDeviceAttribute,
        API_EmptyDeviceData,
        API_EventChannelNotExported,
        API_EventConsumer,
        API_EventNotFound,
        API_EventPropertiesNotSet,
        API_EventQueues,
        API_EventSupplierNotConstructed,
        API_EventTimeout,
        API_FwdAttrInconsistency,
        API_FwdAttrNotConfigured,
        API_HistoryInvalid,
        API_IncoherentDbData,
        API_IncoherentDevData,
        API_IncoherentValues,
        API_IncompatibleArgumentType,
        API_IncompatibleAttrArgumentType,
        API_IncompatibleAttrDataType,
        API_IncompatibleCmdArgumentType,
        API_InitMethodNotFound,
        API_InitNotPublic,
        API_InitThrowsException,
        API_InternalError,
        API_InvalidArgs,
        API_InvalidCorbaAny,
        API_JavaRuntimeSecurityException,
        API_MemAttFailedDuringInit,
        API_MemoryAllocation,
        API_MethodArgument,
        API_MethodNotFound,
        API_MissedEvents,
        API_NoDataYet,
        API_NonDatabaseDevice,
        API_NoSetValueAvailable,
        API_NotificationServiceFailed,
        API_NotSupported,
        API_NotSupportedFeature,
        API_NotUpdatedAnyMore,
        API_NtDebugWindowError,
        API_OverloadingNotSupported,
        API_PipeDataEltNotFound,
        API_PipeDuplicateDEName,
        API_PipeFailed,
        API_PipeNoDataElement,
        API_PipeNotAllowed,
        API_PipeNotFound,
        API_PipeNotWritable,
        API_PipeOptProp,
        API_PipeValueNotSet,
        API_PipeWrongArg,
        API_PipeWrongArgNumber,
        API_PolledDeviceNotInPoolConf,
        API_PolledDeviceNotInPoolMap,
        API_PollingThreadNotFound,
        API_PollObjNotFound,
        API_PollRingBufferEmpty,
        API_PollThreadOutOfSync,
        API_ReadOnlyMode,
        API_RootAttrFailed,
        API_ServerNotRunning,
        API_ShutdownInProgress,
        API_SignalOutOfRange,
        API_StartupSequence,
        API_StdException,
        API_SystemCallFailed,
        API_TangoHostNotSet,
        API_ThrowException,
        API_UnsupportedAttribute,
        API_UnsupportedDBaseModifier,
        API_UnsupportedFeature,
        API_UnsupportedProtocol,
        API_UtilSingletonNotCreated,
        API_UtilSingletonPoisoned,
        API_WAttrOutsideLimit,
        API_WizardConfError,
        API_WrongAttributeNameSyntax,
        API_WrongCmdLineArgs,
        API_WrongDeviceNameSyntax,
        API_WrongEventData,
        API_WrongFormat,
        API_WrongHistoryDataBuffer,
        API_WrongLockingStatus,
        API_WrongNumberOfArgs,
        API_WrongWildcardUsage,
        API_ZmqFailed,
        API_ZmqInitFailed,
        DB_AliasNotDefined,
        DB_ClassNotFoundInCache,
        DB_DeviceNotDefined,
        DB_DeviceNotFoundInCache,
        DB_SQLError,
        DB_TooOldStoredProc,
    ];

}
