//! Module containing the error type returned by TinyTemplate if an error occurs.

use instruction::{path_to_str, PathSlice};
use serde_json::Error as SerdeJsonError;
use serde_json::Value;
use std::error::Error as StdError;
use std::fmt;

/// Enum representing the potential errors that TinyTemplate can encounter.
#[derive(Debug)]
pub enum Error {
    ParseError {
        msg: String,
        line: usize,
        column: usize,
    },
    RenderError {
        msg: String,
        line: usize,
        column: usize,
    },
    SerdeError {
        err: SerdeJsonError,
    },
    GenericError {
        msg: String,
    },
    StdFormatError {
        err: fmt::Error,
    },
    CalledTemplateError {
        name: String,
        err: Box<Error>,
        line: usize,
        column: usize,
    },
    CalledFormatterError {
        name: String,
        err: Box<Error>,
        line: usize,
        column: usize,
    },

    #[doc(hidden)]
    __NonExhaustive,
}
impl From<SerdeJsonError> for Error {
    fn from(err: SerdeJsonError) -> Error {
        Error::SerdeError { err }
    }
}
impl From<fmt::Error> for Error {
    fn from(err: fmt::Error) -> Error {
        Error::StdFormatError { err }
    }
}
impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::ParseError { msg, line, column } => write!(
                f,
                "Failed to parse the template (line {}, column {}). Reason: {}",
                line, column, msg
            ),
            Error::RenderError { msg, line, column } => {
                write!(
                    f,
                    "Encountered rendering error on line {}, column {}. Reason: {}",
                    line, column, msg
                )
            }
            Error::SerdeError { err } => {
                write!(f, "Unexpected serde error while converting the context to a serde_json::Value. Error: {}", err)
            }
            Error::GenericError { msg } => {
                write!(f, "{}", msg)
            }
            Error::StdFormatError { err } => {
                write!(f, "Unexpected formatting error: {}", err)
            }
            Error::CalledTemplateError {
                name,
                err,
                line,
                column,
            } => {
                write!(
                    f,
                    "Call to sub-template \"{}\" on line {}, column {} failed. Reason: {}",
                    name, line, column, err
                )
            }
            Error::CalledFormatterError {
                name,
                err,
                line,
                column,
            } => {
                write!(
                    f,
                    "Call to value formatter \"{}\" on line {}, column {} failed. Reason: {}",
                    name, line, column, err
                )
            }
            Error::__NonExhaustive => unreachable!(),
        }
    }
}
impl StdError for Error {
    fn description(&self) -> &str {
        match self {
            Error::ParseError { .. } => "ParseError",
            Error::RenderError { .. } => "RenderError",
            Error::SerdeError { .. } => "SerdeError",
            Error::GenericError { msg } => &msg,
            Error::StdFormatError { .. } => "StdFormatError",
            Error::CalledTemplateError { .. } => "CalledTemplateError",
            Error::CalledFormatterError { .. } => "CalledFormatterError",
            Error::__NonExhaustive => unreachable!(),
        }
    }
}

pub type Result<T> = ::std::result::Result<T, Error>;

pub(crate) fn lookup_error(source: &str, step: &str, path: PathSlice, current: &Value) -> Error {
    let avail_str = if let Value::Object(object_map) = current {
        let mut avail_str = " Available values at this level are ".to_string();
        for (i, key) in object_map.keys().enumerate() {
            if i > 0 {
                avail_str.push_str(", ");
            }
            avail_str.push('\'');
            avail_str.push_str(key);
            avail_str.push('\'');
        }
        avail_str
    } else {
        "".to_string()
    };

    let (line, column) = get_offset(source, step);

    Error::RenderError {
        msg: format!(
            "Failed to find value '{}' from path '{}'.{}",
            step,
            path_to_str(path),
            avail_str
        ),
        line,
        column,
    }
}

pub(crate) fn truthiness_error(source: &str, path: PathSlice) -> Error {
    let (line, column) = get_offset(source, path.last().unwrap());
    Error::RenderError {
        msg: format!(
            "Path '{}' produced a value which could not be checked for truthiness.",
            path_to_str(path)
        ),
        line,
        column,
    }
}

pub(crate) fn unprintable_error() -> Error {
    Error::GenericError {
        msg: "Expected a printable value but found array or object.".to_string(),
    }
}

pub(crate) fn not_iterable_error(source: &str, path: PathSlice) -> Error {
    let (line, column) = get_offset(source, path.last().unwrap());
    Error::RenderError {
        msg: format!(
            "Expected an array for path '{}' but found a non-iterable value.",
            path_to_str(path)
        ),
        line,
        column,
    }
}

pub(crate) fn unknown_template(source: &str, name: &str) -> Error {
    let (line, column) = get_offset(source, name);
    Error::RenderError {
        msg: format!("Tried to call an unknown template '{}'", name),
        line,
        column,
    }
}

pub(crate) fn unknown_formatter(source: &str, name: &str) -> Error {
    let (line, column) = get_offset(source, name);
    Error::RenderError {
        msg: format!("Tried to call an unknown formatter '{}'", name),
        line,
        column,
    }
}

pub(crate) fn called_template_error(source: &str, template_name: &str, err: Error) -> Error {
    let (line, column) = get_offset(source, template_name);
    Error::CalledTemplateError {
        name: template_name.to_string(),
        err: Box::new(err),
        line,
        column,
    }
}

pub(crate) fn called_formatter_error(source: &str, formatter_name: &str, err: Error) -> Error {
    let (line, column) = get_offset(source, formatter_name);
    Error::CalledFormatterError {
        name: formatter_name.to_string(),
        err: Box::new(err),
        line,
        column,
    }
}

/// Find the line number and column of the target string within the source string. Will panic if
/// target is not a substring of source.
pub(crate) fn get_offset(source: &str, target: &str) -> (usize, usize) {
    let offset = target.as_ptr() as isize - source.as_ptr() as isize;
    let to_scan = &source[0..(offset as usize)];

    let mut line = 1;
    let mut column = 0;

    for byte in to_scan.bytes() {
        match byte as char {
            '\n' => {
                line += 1;
                column = 0;
            }
            _ => {
                column += 1;
            }
        }
    }

    (line, column)
}
