// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

use std::fs;
use std::path;

use clap::Parser;
use merge::Merge;
use serde::Deserialize;

use crate::doc;
use crate::viewer;

/// Command-line viewer for rustdoc documentation
///
/// Opens the documentation for a given keyword. If no keyword is given, shows an interactive
/// crate picker for the current Cargo project (running `cargo doc` automatically if needed).
///
/// Per default, manrs looks up documentation in the ./target/doc directory (resolved to the
/// workspace root) and in the system documentation directories.
#[derive(Debug, Default, Deserialize, Merge, Parser)]
#[command(version)]
#[serde(default)]
pub struct Args {
    /// The item to open, e.g. `HashMap`, `serde::Serialize`, `Vec::push`.
    /// If omitted, shows an interactive crate picker.
    #[merge(skip)]
    #[serde(skip)]
    #[arg(required = false)]
    pub keyword: Option<doc::Name>,

    /// The sources to check for documentation generated by rustdoc
    ///
    /// Typically, this is the path of a directory containing the documentation for one or more
    /// crates in subdirectories.
    #[merge(strategy = merge::vec::prepend)]
    #[arg(name = "source", short, long)]
    pub source_paths: Vec<String>,

    /// The viewer for the rustdoc documentation (one of: plain, rich, tui)
    #[arg(long)]
    #[serde(deserialize_with = "deserialize_viewer", default)]
    pub viewer: Option<ViewerName>,

    /// Do not search the default documentation sources
    ///
    /// If this option is not set, rusty-man appends `$sysroot/share/doc/rust{,-doc}/html` and
    /// `$target/doc` to the list of sources if they exist.  `$sysroot` is the output of `rustc
    /// --print sysroot` or `/usr` if that command does not output a valid path.  `$target` is
    /// `$CARGO_TARGET_DIR`, `$CARGO_BUILD_TARGET_DIR` or `./target`.
    #[merge(strategy = merge::bool::overwrite_false)]
    #[arg(long)]
    pub no_default_sources: bool,

    /// Do not read the search index if there is no exact match
    ///
    /// Per default, rusty-man reads the search indexes of all sources and tries to find matching
    /// items if there is no exact match for the keyword.  If this option is set, the search
    /// indexes are not read.
    #[merge(strategy = merge::bool::overwrite_false)]
    #[arg(long)]
    pub no_search: bool,

    /// Show all examples for the item instead of opening the full documentation.
    #[merge(strategy = merge::bool::overwrite_false)]
    #[arg(short, long)]
    pub examples: bool,

    /// The path to the configuration file to read
    ///
    /// Per default, rusty-man tries to read defaults for the command-line arguments from the
    /// config.toml file in the user configuration directory according to the XDG Base Directory
    /// Specification, i. e. ${XDG_USER_CONFIG}/manrs/config.toml, where ${XDG_USER_CONFIG}
    /// defaults to ${HOME}/.config.
    ///
    /// If this option is set, rusty-man reads the given configuration file instead.  If this
    /// option is set to "-", rusty-man does not read any configuration files.
    #[merge(skip)]
    #[arg(short, long)]
    #[serde(skip)]
    pub config_file: Option<String>,

    #[command(flatten)]
    #[serde(flatten)]
    pub viewer_args: ViewerArgs,
}

/// A wrapper for the viewer name, so it can be used with clap's value_parser.
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(transparent)]
pub struct ViewerName(pub String);

impl std::str::FromStr for ViewerName {
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(ViewerName(s.to_owned()))
    }
}

impl ViewerName {
    pub fn to_viewer(&self) -> anyhow::Result<Box<dyn viewer::Viewer>> {
        viewer::get_viewer(&self.0)
    }
}

impl merge::Merge for ViewerName {
    fn merge(&mut self, other: Self) {
        if self.0.is_empty() {
            *self = other;
        }
    }
}

#[derive(Debug, Default, Deserialize, Merge, Parser)]
#[serde(default)]
pub struct ViewerArgs {
    /// Disable syntax highlighting.
    ///
    /// Per default, rusty-man tries to highlight Rust code snippets in its output if the rich or
    /// tui viewer is selected.  If this option is set, it renders the HTML representation instead.
    #[merge(strategy = merge::bool::overwrite_false)]
    #[arg(long)]
    pub no_syntax_highlight: bool,

    /// The color theme for syntax highlighting
    ///
    /// rusty-man includes these color themes: base16-ocean.dark, base16-eighties.dark,
    /// base16-mocha.dark, base16-ocean.light, InspiredGitHub, Solarized (dark), Solarized (light).
    /// Default value: base16-eighties.dark.
    #[arg(long)]
    pub theme: Option<String>,

    /// The width of the text output
    ///
    /// Per default, rusty-man sets the width of the text output based on the width of the terminal
    /// with the maximum width given by --max-width.  If this option is set, it uses the given
    /// width instead.
    #[arg(long)]
    pub width: Option<usize>,

    /// The maximum width of the text output
    ///
    /// Unless the --width option is set, rusty-man sets the width of the text output based on the
    /// width of the terminal with the maximum width set with this option.
    #[arg(long)]
    pub max_width: Option<usize>,

    /// The pager to use for the plain and rich viewers.
    ///
    /// Per default, rusty-man uses the pager set in the PAGER environment variable, or less if
    /// this environment variable is not set.
    #[arg(long)]
    pub pager: Option<String>,
}

impl Args {
    pub fn load() -> anyhow::Result<Args> {
        let mut args = Args::parse();

        if let Some(config) = Args::load_config(args.config_file.as_deref())? {
            args.merge(config);
        }

        Ok(args)
    }

    pub fn get_viewer(&self) -> anyhow::Result<Option<Box<dyn viewer::Viewer>>> {
        self.viewer.as_ref().map(|v| v.to_viewer()).transpose()
    }

    fn load_config(file: Option<&str>) -> anyhow::Result<Option<Args>> {
        let path = if let Some(file) = file {
            if file == "-" {
                None
            } else {
                Some(path::PathBuf::from(file))
            }
        } else {
            let dirs = xdg::BaseDirectories::with_prefix("manrs");
            dirs.find_config_file("config.toml")
        };
        if let Some(path) = path {
            log::info!("Loading configuration file '{}'", path.display());
            let s = fs::read_to_string(path)?;
            toml::from_str(&s).map(Some).map_err(From::from)
        } else {
            Ok(None)
        }
    }
}

fn deserialize_viewer<'de, D>(d: D) -> Result<Option<ViewerName>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s: Option<String> = Deserialize::deserialize(d)?;
    Ok(s.map(ViewerName))
}
