use crate::markdown::{Markdown, MarkdownError};
use cargo_metadata::{PackageName, TargetKind};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thiserror::Error;

mod extract_doc;
mod inject_doc;
mod markdown;
pub mod transform;
pub mod utils;

pub use extract_doc::{ExtractDocError, extract_doc_from_source_file};
pub use inject_doc::{InjectDocError, MARKER_RDME, inject_doc_in_readme};

#[derive(Error, Debug)]
pub enum ProjectError {
    #[error("failed to get cargo metadata: {0}")]
    CargoMetadataError(cargo_metadata::Error),
    #[error("project has no root package")]
    ProjectHasNoRootPackage,
    #[error("project has no package \"{0}\"")]
    ProjectHasNoPackage(String),
}

impl From<cargo_metadata::Error> for ProjectError {
    fn from(e: cargo_metadata::Error) -> ProjectError {
        ProjectError::CargoMetadataError(e)
    }
}

pub fn find_first_file_in_ancestors(dir_path: impl AsRef<Path>, filename: &str) -> Option<PathBuf> {
    for ancestor_dir in dir_path.as_ref().ancestors() {
        let file = ancestor_dir.join(filename);
        if file.is_file() {
            return Some(file);
        }
    }

    None
}

#[derive(Debug)]
pub enum PackageTarget {
    Bin { crate_name: String },
    Lib,
}

#[derive(PartialEq, Eq, Debug)]
pub struct Project {
    package_name: PackageName,
    manifest_path: PathBuf,
    readme_path: Option<PathBuf>,
    lib_path: Option<PathBuf>,
    bin_path: HashMap<String, PathBuf>,
    directory: PathBuf,
}

impl Project {
    /// Creates a [`Project`] the current directory.  It will search ancestor paths until it finds
    /// the root of the project.
    pub fn from_current_dir(manifest_path: Option<&Path>) -> Result<Project, ProjectError> {
        let metadata = Project::get_cargo_metadata(manifest_path)?;
        let package = metadata.root_package().ok_or(ProjectError::ProjectHasNoRootPackage)?;

        Ok(Project::from_package(package))
    }

    fn get_cargo_metadata(
        manifest_path: Option<&Path>,
    ) -> Result<cargo_metadata::Metadata, ProjectError> {
        let mut command = cargo_metadata::MetadataCommand::new();

        if let Some(manifest_path) = manifest_path {
            command.manifest_path(manifest_path);
        }

        Ok(command.exec()?)
    }

    fn select_package<'a>(
        metadata: &'a cargo_metadata::Metadata,
        package_name: &str,
    ) -> Option<&'a cargo_metadata::Package> {
        metadata.packages.iter().find(|package| {
            package.name == package_name && metadata.workspace_members.contains(&package.id)
        })
    }

    pub fn from_current_dir_workspace_project(
        manifest_path: Option<&Path>,
        project_name: &str,
    ) -> Result<Project, ProjectError> {
        let metadata = Project::get_cargo_metadata(manifest_path)?;

        let package = Project::select_package(&metadata, project_name)
            .ok_or_else(|| ProjectError::ProjectHasNoPackage(project_name.to_owned()))?;

        Ok(Project::from_package(package))
    }

    fn from_package(package: &cargo_metadata::Package) -> Project {
        const LIB_CRATE_KINDS: [TargetKind; 6] = [
            TargetKind::Lib,
            TargetKind::DyLib,
            TargetKind::StaticLib,
            TargetKind::CDyLib,
            TargetKind::RLib,
            TargetKind::ProcMacro,
        ];
        let lib_packages: Vec<&cargo_metadata::Target> = package
            .targets
            .iter()
            .filter(|target| target.kind.iter().any(|k| LIB_CRATE_KINDS.contains(k)))
            .collect();

        assert!(lib_packages.len() <= 1, "more than one lib target");

        let lib_package = lib_packages.first();

        let bin_packages =
            package.targets.iter().filter(|target| target.kind.contains(&TargetKind::Bin));

        let manifest_path = package.manifest_path.clone().into_std_path_buf();

        let directory = manifest_path
            .parent()
            .expect("error getting the parent path of the manifest file")
            .to_path_buf();

        Project {
            package_name: package.name.clone(),
            manifest_path,
            readme_path: package.readme.as_ref().map(|p| p.clone().into_std_path_buf()),
            lib_path: lib_package.map(|t| t.src_path.clone().into_std_path_buf()),
            bin_path: bin_packages
                .map(|t| (t.name.clone(), t.src_path.clone().into_std_path_buf()))
                .collect(),
            directory,
        }
    }

    #[must_use]
    pub fn get_lib_entryfile_path(&self) -> Option<&Path> {
        self.lib_path.as_ref().filter(|p| p.is_file()).map(PathBuf::as_path)
    }

    #[must_use]
    pub fn get_bin_default_crate_name(&self) -> Option<&str> {
        match self.bin_path.len() {
            1 => self.bin_path.keys().next().map(String::as_str),
            _ => None,
        }
    }

    #[must_use]
    pub fn get_bin_default_entryfile_path(&self) -> Option<&Path> {
        match self.bin_path.len() {
            1 => self
                .bin_path
                .keys()
                .next()
                .and_then(|bin_name| self.get_bin_entryfile_path(bin_name)),
            _ => None,
        }
    }

    #[must_use]
    pub fn get_bin_entryfile_path(&self, name: &str) -> Option<&Path> {
        self.bin_path.get(name).filter(|p| p.is_file()).map(PathBuf::as_path)
    }

    #[must_use]
    pub fn get_readme_path(&self) -> Option<PathBuf> {
        self.readme_path
            .clone()
            .or_else(|| Some(Path::new("README.md").to_path_buf()))
            .map(|p| self.directory.join(p))
            .filter(|p| p.is_file())
    }

    #[must_use]
    pub fn get_package_name(&self) -> &PackageName {
        &self.package_name
    }

    #[must_use]
    pub fn get_manifest_path(&self) -> &PathBuf {
        &self.manifest_path
    }
}

#[derive(Eq, PartialEq, Clone, Debug)]
pub struct Doc {
    pub markdown: Markdown,
}

impl Doc {
    #[must_use]
    pub fn from_markdown(markdown: Markdown) -> Doc {
        Doc { markdown }
    }

    // TODO implement FromStr when ! type is stable.
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(str: impl Into<String>) -> Doc {
        Doc { markdown: Markdown::from_str(str) }
    }

    fn is_toplevel_doc(attr: &syn::Attribute) -> bool {
        use syn::AttrStyle;
        use syn::token::Not;

        attr.style == AttrStyle::Inner(Not::default()) && attr.path().is_ident("doc")
    }

    pub fn lines(&self) -> impl Iterator<Item = &str> {
        self.markdown.lines()
    }

    // Return the markdown as a string.  Note that the line terminator will always be a line feed.
    #[must_use]
    pub fn as_string(&self) -> &str {
        self.markdown.as_string()
    }
}

#[derive(Error, Debug)]
pub enum ReadmeError {
    #[error("failed to read README file \"{0}\"")]
    ErrorReadingReadmeFromFile(PathBuf),
    #[error("failed to write README file \"{0}\"")]
    ErrorWritingMarkdownToFile(PathBuf),
    #[error("failed to write README")]
    ErrorWritingMarkdown,
}

impl From<MarkdownError> for ReadmeError {
    fn from(e: MarkdownError) -> ReadmeError {
        match e {
            MarkdownError::ErrorReadingMarkdownFromFile(p) => {
                ReadmeError::ErrorReadingReadmeFromFile(p)
            }
            MarkdownError::ErrorWritingMarkdownToFile(p) => {
                ReadmeError::ErrorWritingMarkdownToFile(p)
            }
            MarkdownError::ErrorWritingMarkdown => ReadmeError::ErrorWritingMarkdown,
        }
    }
}

#[derive(Eq, PartialEq, Debug, Copy, Clone)]
pub enum LineTerminator {
    Lf,
    CrLf,
}

pub struct Readme {
    pub markdown: Markdown,
}

impl Readme {
    pub fn from_file(file_path: impl AsRef<Path>) -> Result<Readme, ReadmeError> {
        Ok(Readme { markdown: Markdown::from_file(file_path)? })
    }

    // TODO implement FromStr when ! type is stable.
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(str: impl Into<String>) -> Readme {
        Readme { markdown: Markdown::from_str(str) }
    }

    pub fn from_lines(lines: &[impl AsRef<str>]) -> Readme {
        Readme { markdown: Markdown::from_lines(lines) }
    }

    pub fn lines(&self) -> impl Iterator<Item = &str> {
        self.markdown.lines()
    }

    pub fn write_to_file(
        &self,
        file: impl AsRef<Path>,
        line_terminator: LineTerminator,
    ) -> Result<(), ReadmeError> {
        Ok(self.markdown.write_to_file(file, line_terminator)?)
    }

    pub fn write(
        &self,
        writer: impl std::io::Write,
        line_terminator: LineTerminator,
    ) -> Result<(), ReadmeError> {
        Ok(self.markdown.write(writer, line_terminator)?)
    }

    // Return the markdown as a string.  Note that the line terminator will always be a line feed.
    #[must_use]
    pub fn as_string(&self) -> &str {
        self.markdown.as_string()
    }
}

pub fn infer_line_terminator(file_path: impl AsRef<Path>) -> std::io::Result<LineTerminator> {
    let content: String = std::fs::read_to_string(file_path.as_ref())?;

    let crlf_lines: usize = content.matches("\r\n").count();
    let lf_lines: usize = content.matches('\n').count() - crlf_lines;

    if crlf_lines > lf_lines { Ok(LineTerminator::CrLf) } else { Ok(LineTerminator::Lf) }
}
