// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    cm_types::ParseError,
    std::path::Path,
    std::str::Utf8Error,
    std::{error, fmt, io},
};

/// The location in the file where an error was detected.
#[derive(PartialEq, Clone, Debug)]
pub struct Location {
    /// One-based line number of the error.
    pub line: usize,

    /// One-based column number of the error.
    pub column: usize,
}

/// Enum type that can represent any error encountered by a cmx operation.
#[derive(Debug)]
pub enum Error {
    DuplicateRights(String),
    InvalidArgs(String),
    Io(io::Error),
    FidlEncoding(fidl::Error),
    MissingRights(String),
    Parse { err: String, location: Option<Location>, filename: Option<String> },
    Validate { schema_name: Option<String>, err: String, filename: Option<String> },
    Internal(String),
    Utf8(Utf8Error),
}

impl error::Error for Error {}

impl Error {
    pub fn invalid_args(err: impl Into<String>) -> Self {
        Self::InvalidArgs(err.into())
    }

    pub fn parse(
        err: impl fmt::Display,
        location: Option<Location>,
        filename: Option<&Path>,
    ) -> Self {
        Self::Parse {
            err: err.to_string(),
            location,
            filename: filename.map(|f| f.to_string_lossy().into_owned()),
        }
    }

    pub fn validate(err: impl fmt::Display) -> Self {
        Self::Validate { schema_name: None, err: err.to_string(), filename: None }
    }

    pub fn validate_schema(schema_name: &str, err: impl Into<String>) -> Self {
        Self::Validate {
            schema_name: Some(schema_name.to_string()),
            err: err.into(),
            filename: None,
        }
    }

    pub fn duplicate_rights(err: impl Into<String>) -> Self {
        Self::DuplicateRights(err.into())
    }

    pub fn missing_rights(err: impl Into<String>) -> Self {
        Self::MissingRights(err.into())
    }

    pub fn internal(err: impl Into<String>) -> Self {
        Self::Internal(err.into())
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self {
            Error::DuplicateRights(err) => write!(f, "Duplicate rights: {}", err),
            Error::InvalidArgs(err) => write!(f, "Invalid args: {}", err),
            Error::Io(err) => write!(f, "IO error: {}", err),
            Error::FidlEncoding(err) => write!(f, "Fidl encoding error: {}", err),
            Error::MissingRights(err) => write!(f, "Missing rights: {}", err),
            Error::Parse { err, location, filename } => {
                let mut prefix = String::new();
                if let Some(filename) = filename {
                    prefix.push_str(&format!("{}:", filename));
                }
                if let Some(location) = location {
                    // Check for a syntax error generated by pest. These error messages have
                    // the line and column number embedded in them, so we don't want to
                    // duplicate that.
                    //
                    // TODO: If serde_json5 had an error type for json5 syntax errors, we wouldn't
                    // need to parse the string like this.
                    if !err.starts_with(" -->") {
                        prefix.push_str(&format!("{}:{}:", location.line, location.column));
                    }
                }
                if !prefix.is_empty() {
                    write!(f, "Error at {} {}", prefix, err)
                } else {
                    write!(f, "{}", err)
                }
            }
            Error::Validate { schema_name: _, err, filename } => {
                let mut prefix = String::new();
                if let Some(filename) = filename {
                    prefix.push_str(&format!("{}:", filename));
                }
                if !prefix.is_empty() {
                    write!(f, "Error at {} {}", prefix, err)
                } else {
                    write!(f, "{}", err)
                }
            }
            Error::Internal(err) => write!(f, "Internal error: {}", err),
            Error::Utf8(err) => write!(f, "UTF8 error: {}", err),
        }
    }
}

impl From<io::Error> for Error {
    fn from(err: io::Error) -> Self {
        Error::Io(err)
    }
}

impl From<Utf8Error> for Error {
    fn from(err: Utf8Error) -> Self {
        Error::Utf8(err)
    }
}

impl From<serde_json::error::Error> for Error {
    fn from(err: serde_json::error::Error) -> Self {
        use serde_json::error::Category;
        match err.classify() {
            Category::Io | Category::Eof => Error::Io(err.into()),
            Category::Syntax => {
                let line = err.line();
                let column = err.column();
                Error::parse(err, Some(Location { line, column }), None)
            }
            Category::Data => Error::validate(err),
        }
    }
}

impl From<fidl::Error> for Error {
    fn from(err: fidl::Error) -> Self {
        Error::FidlEncoding(err)
    }
}

impl From<cm_json::Error> for Error {
    fn from(err: cm_json::Error) -> Self {
        match err {
            cm_json::Error::Io(err) => Error::Io(err),
            cm_json::Error::Parse { err, location, filename } => {
                Error::Parse { err, location: location.map(|loc| loc.into()), filename }
            }
            cm_json::Error::Validate { schema_name, err, filename } => {
                Error::Validate { schema_name, err, filename }
            }
        }
    }
}

impl From<ParseError> for Error {
    fn from(err: ParseError) -> Self {
        match err {
            ParseError::InvalidValue => Self::internal("invalid value"),
            ParseError::InvalidLength => Self::internal("invalid length"),
            ParseError::NotAName => Self::internal("not a name"),
            ParseError::NotAPath => Self::internal("not a path"),
        }
    }
}

impl From<cm_json::Location> for Location {
    fn from(location: cm_json::Location) -> Self {
        Location { line: location.line, column: location.column }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use cm_types as cm;
    use matches::assert_matches;

    #[test]
    fn test_syntax_error_message() {
        let result = serde_json::from_str::<cm::Name>("foo").map_err(Error::from);
        assert_matches!(result, Err(Error::Parse { .. }));
    }

    #[test]
    fn test_validation_error_message() {
        let result = serde_json::from_str::<cm::Name>("\"foo$\"").map_err(Error::from);
        assert_matches!(result, Err(Error::Validate { .. }));
    }
}
