use std::{
    borrow::Cow,
    cmp::Ordering,
    collections::HashSet,
    fmt::{self, Display},
    path::PathBuf,
};
use crate::{rules::Rule, ParserError};
use super::{
    resources::ResourceError,
    work_item::{WorkData, WorkItem, WorkStatus},
};
#[derive(Debug, Clone)]
enum ErrorKind {
    Parser {
        path: PathBuf,
        error: ParserError,
    },
    ResourceNotFound {
        path: PathBuf,
    },
    InvalidConfiguration {
        path: PathBuf,
    },
    MultipleConfigurationFound {
        paths: Vec<PathBuf>,
    },
    IO {
        path: PathBuf,
        error: String,
    },
    UncachedWork {
        path: PathBuf,
    },
    RuleError {
        path: PathBuf,
        rule_name: String,
        rule_number: usize,
        error: String,
    },
    CyclicWork {
        work: Vec<(WorkData, Vec<PathBuf>)>,
    },
    Custom {
        message: Cow<'static, str>,
    },
}
pub type DarkluaResult<T> = Result<T, DarkluaError>;
#[derive(Debug, Clone)]
pub struct DarkluaError {
    kind: Box<ErrorKind>,
    context: Option<Cow<'static, str>>,
}
impl DarkluaError {
    fn new(kind: ErrorKind) -> Self {
        Self {
            kind: kind.into(),
            context: None,
        }
    }
    pub(crate) fn context(mut self, context: impl Into<Cow<'static, str>>) -> Self {
        self.context = Some(context.into());
        self
    }
    pub(crate) fn parser_error(path: impl Into<PathBuf>, error: ParserError) -> Self {
        Self::new(ErrorKind::Parser {
            path: path.into(),
            error,
        })
    }
    pub(crate) fn multiple_configuration_found(
        configuration_files: impl Iterator<Item = PathBuf>,
    ) -> Self {
        Self::new(ErrorKind::MultipleConfigurationFound {
            paths: configuration_files.collect(),
        })
    }
    pub(crate) fn io_error(path: impl Into<PathBuf>, error: impl Into<String>) -> Self {
        Self::new(ErrorKind::IO {
            path: path.into(),
            error: error.into(),
        })
    }
    pub(crate) fn resource_not_found(path: impl Into<PathBuf>) -> Self {
        Self::new(ErrorKind::ResourceNotFound { path: path.into() })
    }
    pub(crate) fn invalid_configuration_file(path: impl Into<PathBuf>) -> Self {
        Self::new(ErrorKind::InvalidConfiguration { path: path.into() })
    }
    pub(crate) fn uncached_work(path: impl Into<PathBuf>) -> Self {
        Self::new(ErrorKind::UncachedWork { path: path.into() })
    }
    pub(crate) fn rule_error(
        path: impl Into<PathBuf>,
        rule: &dyn Rule,
        rule_index: usize,
        rule_error: impl Into<String>,
    ) -> Self {
        Self::new(ErrorKind::RuleError {
            path: path.into(),
            rule_name: rule.get_name().to_owned(),
            rule_number: rule_index,
            error: rule_error.into(),
        })
    }
    pub(crate) fn cyclic_work(work_left: Vec<WorkItem>) -> Self {
        let source_left: HashSet<PathBuf> = work_left
            .iter()
            .map(|work| work.source().to_path_buf())
            .collect();
        let mut required_work: Vec<_> = work_left
            .into_iter()
            .filter_map(|work| {
                if work.total_required_content() == 0 {
                    None
                } else {
                    let (status, data) = work.extract();
                    match status {
                        WorkStatus::NotStarted => None,
                        WorkStatus::InProgress(progress) => {
                            let mut content: Vec<_> = progress
                                .required_content()
                                .filter(|path| source_left.contains(*path))
                                .map(PathBuf::from)
                                .collect();
                            if content.is_empty() {
                                None
                            } else {
                                content.sort();
                                Some((data, content))
                            }
                        }
                    }
                }
            })
            .collect();
        required_work.sort_by(|(a_data, a_content), (b_data, b_content)| {
            match a_content.len().cmp(&b_content.len()) {
                Ordering::Equal => a_data.source().cmp(b_data.source()),
                other => other,
            }
        });
        required_work.sort_by_key(|(_, content)| content.len());
        Self::new(ErrorKind::CyclicWork {
            work: required_work,
        })
    }
    pub(crate) fn custom(message: impl Into<Cow<'static, str>>) -> Self {
        Self::new(ErrorKind::Custom {
            message: message.into(),
        })
    }
}
impl From<ResourceError> for DarkluaError {
    fn from(err: ResourceError) -> Self {
        match err {
            ResourceError::NotFound(path) => DarkluaError::resource_not_found(path),
            ResourceError::IO { path, error } => DarkluaError::io_error(path, error),
        }
    }
}
impl Display for DarkluaError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &*self.kind {
            ErrorKind::Parser { path, error } => {
                write!(f, "unable to parse `{}`: {}", path.display(), error)?;
            }
            ErrorKind::ResourceNotFound { path } => {
                write!(f, "unable to find `{}`", path.display())?;
            }
            ErrorKind::InvalidConfiguration { path } => {
                write!(f, "invalid configuration file at `{}`", path.display())?;
            }
            ErrorKind::MultipleConfigurationFound { paths } => {
                write!(
                    f,
                    "multiple default configuration file found: {}",
                    paths
                        .iter()
                        .map(|path| format!("`{}`", path.display()))
                        .collect::<Vec<_>>()
                        .join(", ")
                )?;
            }
            ErrorKind::IO { path, error } => {
                write!(f, "IO error with `{}`: {}", path.display(), error)?;
            }
            ErrorKind::UncachedWork { path } => {
                write!(f, "attempt to obtain work at `{}`", path.display())?;
            }
            ErrorKind::RuleError {
                path,
                rule_name,
                rule_number,
                error,
            } => {
                write!(
                    f,
                    "error processing `{}` ({} [#{}]): {}",
                    path.display(),
                    rule_name,
                    rule_number,
                    error,
                )?;
            }
            ErrorKind::CyclicWork { work } => {
                const MAX_PRINTED_WORK: usize = 12;
                const MAX_REQUIRED_PATH: usize = 20;
                let total = work.len();
                let list: Vec<_> = work
                    .iter()
                    .take(MAX_PRINTED_WORK)
                    .map(|(data, required)| {
                        let required_list: Vec<_> = required
                            .iter()
                            .take(MAX_REQUIRED_PATH)
                            .map(|path| format!("      - {}", path.display()))
                            .collect();
                        format!(
                            "    `{}` needs:\n{}",
                            data.source().display(),
                            required_list.join("\n")
                        )
                    })
                    .collect();
                write!(
                    f,
                    "cyclic work detected:\n{}{}",
                    list.join("\n"),
                    if total <= MAX_PRINTED_WORK {
                        "".to_owned()
                    } else {
                        format!("\n    and {} more", total - MAX_PRINTED_WORK)
                    }
                )?;
            }
            ErrorKind::Custom { message } => {
                write!(f, "{}", message)?;
            }
        };
        if let Some(context) = &self.context {
            write!(f, " ({})", context)?;
        }
        Ok(())
    }
}