//! Types used to communicate parsing errors.

use console::{style, StyledObject};
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
    any::type_name,
    borrow::Cow,
    collections::VecDeque,
    fmt::{self, Debug},
    io,
    ops::Range,
};

#[derive(Clone, Debug)]
pub(crate) enum InptContext<'s> {
    Within(&'s str),
    InptType(&'static str),
    FromStrType(&'static str),
    Regex(&'static str),
    Literal(&'static dyn Debug),
    RegexGroup(usize),
    Message(String),
    Recursion(&'static str),
    AtStart,
    AtEnd,
}

impl<'s> InptContext<'s> {
    fn within(&self) -> Option<&'s str> {
        match self {
            InptContext::Within(text) => Some(text),
            _ => None,
        }
    }
}

/// Provides information about parsing errors.
#[derive(Clone, Debug)]
pub struct InptError<'s> {
    pub(crate) context: Vec<InptContext<'s>>,
}

impl std::error::Error for InptError<'static> {}

impl<'s> InptError<'s> {
    pub fn expected_regex(re: &'static str, found: &'s str) -> Self {
        InptError {
            context: vec![InptContext::Within(found), InptContext::Regex(re)],
        }
    }

    pub fn expected_regex_at_start(re: &'static str) -> Self {
        InptError {
            context: vec![InptContext::AtStart, InptContext::Regex(re)],
        }
    }

    pub fn expected_regex_at_end(re: &'static str) -> Self {
        InptError {
            context: vec![InptContext::AtEnd, InptContext::Regex(re)],
        }
    }

    pub fn expected_lit(lit: &'static impl Debug, found: &'s str) -> Self {
        InptError {
            context: vec![InptContext::Within(found), InptContext::Literal(lit)],
        }
    }

    pub fn expected_lit_at_start(lit: &'static impl Debug) -> Self {
        InptError {
            context: vec![InptContext::AtStart, InptContext::Literal(lit)],
        }
    }

    pub fn expected_lit_at_end(lit: &'static impl Debug) -> Self {
        InptError {
            context: vec![InptContext::AtEnd, InptContext::Literal(lit)],
        }
    }

    pub fn expected<T>(found: &'s str) -> Self {
        InptError {
            context: vec![
                InptContext::Within(found),
                InptContext::InptType(type_name::<T>()),
            ],
        }
    }

    pub fn expected_at_start<T>() -> Self {
        InptError {
            context: vec![
                InptContext::AtStart,
                InptContext::InptType(type_name::<T>()),
            ],
        }
    }

    pub fn expected_at_end<T>() -> Self {
        InptError {
            context: vec![InptContext::AtEnd, InptContext::InptType(type_name::<T>())],
        }
    }

    pub fn expected_regex_group(index: usize) -> Self {
        InptError {
            context: vec![InptContext::AtEnd, InptContext::RegexGroup(index)],
        }
    }

    pub fn expected_from_str<T>(found: &'s str) -> Self {
        InptError {
            context: vec![
                InptContext::Within(found),
                InptContext::FromStrType(type_name::<T>()),
            ],
        }
    }

    pub fn recursion_at_start<T>() -> Self {
        InptError {
            context: vec![
                InptContext::AtStart,
                InptContext::Recursion(type_name::<T>()),
            ],
        }
    }
}

/// The result type of parsing `T`.
///
/// The inpt implementation of this type is infalible, as any errors
/// occuring when parsing `T` are simply provided in the `Err` variant.
pub type InptResult<'s, T> = Result<T, InptError<'s>>;

fn context_outside<'s, T>(mut this: InptResult<'s, T>, ctx: InptContext<'s>) -> InptResult<'s, T> {
    if let Err(error) = &mut this {
        error.context.push(ctx);
    }
    this
}

fn context_inside<'s, T>(mut this: InptResult<'s, T>, ctx: InptContext<'s>) -> InptResult<'s, T> {
    if let Err(error) = &mut this {
        if let Some((idx, _)) = error.context.iter().enumerate().rfind(|(_, ctx)| {
            matches!(
                ctx,
                InptContext::Within(_) | InptContext::AtStart | InptContext::AtEnd
            )
        }) {
            error.context.insert(idx + 1, ctx);
        } else {
            error.context.push(ctx);
        }
    }
    this
}

/// An extension trait for `InptResult`, used to provide error context.
pub trait ResultExt<'s> {
    /// Provide a slice of the input text which is known to contain the error,
    /// and set that slice as the target of later context.
    fn within(self, text: &'s str) -> Self;
    /// Provide the regex which failed to match on the target slice.
    fn regex(self, re: &'static str) -> Self;
    /// Provide the capture group which contained the target slice.
    fn regex_group(self, group: usize) -> Self;
    /// Place an additional message outside of whatever context has already been provided.
    fn message_outside(self, msg: impl fmt::Display) -> Self;
    /// Place an additional message inside of whatever context has already been provided.
    fn message_inside(self, msg: impl fmt::Display) -> Self;
    /// Provide the type which failed to parse from the target slice.
    fn expected<T>(self) -> Self;
    /// Find the last occurance of type A provided by a call to [`ResultExt::expected`] and change it to B.
    fn replace_expected<A, B>(self) -> Self;
    /// Rather than giving an exact slice to [`ResultExt::within`], specify that the error
    /// is at the start of wathever slice is provided later.
    fn at_start(self) -> Self;
    /// Rather than giving an exact slice to [`ResultExt::within`], specify that the error
    /// is at the end of wathever slice is provided later.
    fn at_end(self) -> Self;
}

impl<'s, R> ResultExt<'s> for InptResult<'s, R> {
    fn within(self, text: &'s str) -> Self {
        context_outside(self, InptContext::Within(text))
    }

    fn regex(self, re: &'static str) -> Self {
        context_outside(self, InptContext::Regex(re))
    }

    fn regex_group(self, group: usize) -> Self {
        context_outside(self, InptContext::RegexGroup(group))
    }

    fn message_outside(self, msg: impl fmt::Display) -> Self {
        context_outside(self, InptContext::Message(msg.to_string()))
    }

    fn message_inside(self, msg: impl fmt::Display) -> Self {
        context_inside(self, InptContext::Message(msg.to_string()))
    }

    fn expected<T>(self) -> Self {
        context_outside(self, InptContext::InptType(type_name::<T>()))
    }

    fn replace_expected<A, B>(mut self) -> Self {
        if let Err(error) = &mut self {
            for ctx in error.context.iter_mut().rev() {
                if let InptContext::InptType(ty) = ctx {
                    if *ty == type_name::<A>() {
                        *ty = type_name::<B>();
                    }
                    break;
                }
            }
        }
        self
    }

    fn at_start(self) -> Self {
        context_outside(self, InptContext::AtStart)
    }

    fn at_end(self) -> Self {
        context_outside(self, InptContext::AtEnd)
    }
}

static SHORTEN_TYPES: Lazy<Regex> = Lazy::new(|| {
    Regex::new("(std|alloc|core)::(vec|collections|boxed|sync|slice|rc|str|string)::").unwrap()
});

fn shorten_type(name: &str) -> Cow<str> {
    SHORTEN_TYPES.replace_all(name, "")
}

impl<'s> InptError<'s> {
    /// Returns the range of zero-indexed `(row,column)` pairs indicating the location in the input
    /// where the error occured, if any could be determined.
    pub fn row_col(&self) -> Option<Range<(usize, usize)>> {
        let inner = self.context.iter().find_map(|c| c.within())?;
        let all = self.context.iter().rev().find_map(|c| c.within())?;
        let begin_loc = inner.as_ptr() as usize - all.as_ptr() as usize;
        let end_loc = begin_loc + inner.len();

        let mut begin_row = 0;
        let mut begin_col = 0;
        let mut end_row = 0;
        let mut end_col = 0;
        for (i, c) in all.char_indices() {
            if c == '\n' {
                if i < begin_loc {
                    begin_col = 0;
                    begin_row += 1;
                }
                if i < end_loc {
                    end_col = 0;
                    end_row += 1;
                }
            } else {
                if i < begin_loc {
                    begin_col += 1;
                }
                if i < end_loc {
                    end_col += 1;
                }
            }
        }

        Some((begin_row, begin_col)..(end_row, end_col))
    }

    /// Returns the portion of the input text which caused the error, if any could be determined.
    pub fn unparsable_text(&self) -> Option<&'s str> {
        self.context.iter().find_map(|c| c.within())
    }

    fn annotated_impl(&self) -> VecDeque<StyledObject<String>> {
        let mut out = VecDeque::new();
        let mut inner: Option<&str> = None;
        let mut at_end = false;
        let mut at_start = false;
        let mut sty: fn(String) -> StyledObject<String> = |s| style(s).bold().red();

        for ctx in &self.context {
            match ctx {
                InptContext::Within(outer) => {
                    let (split_loc, skip_len) = match (at_start, at_end, inner) {
                        (false, false, Some(inner)) => (
                            inner.as_ptr() as usize - outer.as_ptr() as usize,
                            inner.len(),
                        ),
                        (false, false, None) => (0, 0),
                        (true, false, _) => (0, 0),
                        (false, true, _) => (outer.len(), 0),
                        _ => unreachable!(),
                    };

                    if split_loc != 0 {
                        out.push_front(style(outer[..split_loc].to_string()));
                    }
                    if split_loc + skip_len != outer.len() {
                        out.push_back(style(outer[split_loc + skip_len..].to_string()));
                    }

                    inner = Some(outer);
                    at_end = false;
                    at_start = false;
                }
                InptContext::InptType(ty) => {
                    let ty = shorten_type(ty);
                    out.push_front(sty(format!("<{ty}>")));
                    out.push_back(sty(format!("</{ty}>")));
                    sty = |s| style(s).bold().yellow();
                }
                InptContext::FromStrType(ty) => {
                    let ty = shorten_type(ty);
                    out.push_front(sty(format!("<{ty}::from_str>")));
                    out.push_back(sty(format!("</{ty}::from_str>")));
                    sty = |s| style(s).bold().yellow();
                }
                InptContext::Regex(reg) => {
                    out.push_front(sty(format!(" >")));
                    out.push_front(style(format!("/{reg}/")).blue().italic());
                    out.push_front(sty(format!("< ")));
                    out.push_back(sty(format!("</regex>")));
                    sty = |s| style(s).bold().yellow();
                }
                InptContext::Literal(lit) => {
                    out.push_front(sty(format!(" >")));
                    out.push_front(style(format!("{lit:?}")).blue().italic());
                    out.push_front(sty(format!("< ")));
                    out.push_back(sty(format!("</ {lit:?} >")));
                    sty = |s| style(s).bold().yellow();
                }
                InptContext::RegexGroup(idx) => {
                    out.push_front(sty(format!("<${idx}>")));
                    out.push_back(sty(format!("</${idx}>")));
                    sty = |s| style(s).bold().yellow();
                }
                InptContext::Message(msg) => {
                    out.push_front(sty(format!("<! {msg} >")));
                }
                InptContext::Recursion(ty) => {
                    out.push_front(sty(format!("<^ {ty} />")));
                }
                InptContext::AtStart if !at_end => at_start = true,
                InptContext::AtEnd if !at_start => at_end = true,
                _ => (),
            }
        }
        out
    }

    /// Pretty-prints the error trace into the given writer.
    pub fn annotated(
        &self,
        mut output: impl io::Write,
        force_styling: Option<bool>,
    ) -> io::Result<()> {
        for mut chunk in self.annotated_impl() {
            if let Some(force_styling) = force_styling {
                chunk = chunk.force_styling(force_styling);
            }
            write!(output, "{chunk}")?;
        }
        Ok(())
    }

    /// Pretty-prints the error trace into stdout.
    /// If the filename or other input name is known, provide it as `source`.
    pub fn annotated_stdout(&self, source: &str) -> io::Result<()> {
        use io::Write;
        let mut stdout = console::Term::buffered_stdout();
        if let Some((row, col)) = self.row_col().map(|range| range.start) {
            writeln!(
                stdout,
                "{} in {}:{}:{}",
                style("INPT ERROR").red().for_stdout(),
                source,
                row + 1,
                col + 1,
            )?;
        }
        for mut chunk in self.annotated_impl() {
            chunk = chunk.for_stdout();
            write!(stdout, "{chunk}")?;
        }
        write!(stdout, "\n")?;
        stdout.flush()
    }

    /// Pretty-prints the error trace into stderr.
    /// If the filename or other input name is known, provide it as `source`.
    pub fn annotated_stderr(&self, source: &str) -> io::Result<()> {
        use io::Write;
        let mut stderr = console::Term::buffered_stderr();
        if let Some((row, col)) = self.row_col().map(|range| range.start) {
            writeln!(
                stderr,
                "{} in {}:{}:{}",
                style("INPT ERROR").red().for_stderr(),
                source,
                row + 1,
                col + 1,
            )?;
        }
        for mut chunk in self.annotated_impl() {
            chunk = chunk.for_stderr();
            write!(stderr, "{chunk}")?;
        }
        write!(stderr, "\n")?;
        stderr.flush()
    }
}

impl<'a> fmt::Display for InptError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for mut chunk in self.annotated_impl() {
            chunk = chunk.force_styling(false);
            write!(f, "{chunk}")?;
        }
        Ok(())
    }
}
