use clap::builder::{RangedI64ValueParser, RangedU64ValueParser, ValueParser};
use clap::parser::ValueSource;
use clap::{Arg, ArgMatches, Command, ValueHint};
use xilem_html::{elements as el, Adapt, MessageResult, View, ViewExt, ViewMarker, ViewSequence};

use crate::AppState;

enum ValueType {
    Path,
    Command,
    Name,
    Url,
    Email,
    String,
    Range,
    Bool,
    Enum,
    Map,
    Unknown,
}

impl ValueType {
    fn name(&self) -> &str {
        match self {
            Self::Path => "path",
            Self::Command => "command",
            Self::Name => "name",
            Self::Url => "url",
            Self::Email => "email",
            Self::String => "string",
            Self::Range => "range",
            Self::Bool => "bool",
            Self::Enum => "enum",
            Self::Map => "map",
            Self::Unknown => "unknown",
        }
    }
}

impl From<&Arg> for ValueType {
    fn from(value: &Arg) -> Self {
        match value.get_value_hint() {
            ValueHint::AnyPath => Self::Path,
            ValueHint::FilePath => Self::Path,
            ValueHint::DirPath => Self::Path,
            ValueHint::ExecutablePath => Self::Path,
            ValueHint::CommandName => Self::Command,
            ValueHint::CommandString => Self::Command,
            ValueHint::CommandWithArguments => Self::Command,
            ValueHint::Username => Self::Name,
            ValueHint::Hostname => Self::Name,
            ValueHint::Url => Self::Url,
            ValueHint::EmailAddress => Self::Email,
            _ => {
                let type_id = value.get_value_parser().type_id();
                if type_id == ValueParser::new(RangedU64ValueParser::<u8>::new()).type_id() {
                    Self::Range
                } else if type_id == ValueParser::new(RangedU64ValueParser::<u16>::new()).type_id()
                {
                    Self::Range
                } else if type_id == ValueParser::new(RangedU64ValueParser::<u32>::new()).type_id()
                {
                    Self::Range
                } else if type_id == ValueParser::new(RangedU64ValueParser::<u64>::new()).type_id()
                {
                    Self::Range
                } else if type_id == ValueParser::new(RangedI64ValueParser::<i8>::new()).type_id() {
                    Self::Range
                } else if type_id == ValueParser::new(RangedI64ValueParser::<i16>::new()).type_id()
                {
                    Self::Range
                } else if type_id == ValueParser::new(RangedI64ValueParser::<i32>::new()).type_id()
                {
                    Self::Range
                } else if type_id == ValueParser::new(RangedI64ValueParser::<i64>::new()).type_id()
                {
                    Self::Range
                } else if type_id == ValueParser::string().type_id() {
                    Self::String
                } else {
                    Self::Unknown
                }
            }
        }
    }
}

fn type_description(arg: &Arg) -> impl View<Arg> + ViewMarker {
    let value_type = ValueType::from(arg);
    el::details(el::summary(value_type.name().to_string()))
}

fn aliases(arg: &Arg) -> impl ViewSequence<Arg> {
    let mut aliases = Vec::new();

    // Handle short aliases (-a, -b etc)
    if let Some(long_aliases) = arg.get_short_and_visible_aliases() {
        aliases.extend(
            long_aliases
                .iter()
                .map(|alias| alias.to_string())
                .filter(|alias| alias != arg.get_id()),
        );
    }

    // Handle long aliases (--alias1, --alias2 etc)
    if let Some(long_aliases) = arg.get_long_and_visible_aliases() {
        aliases.extend(
            long_aliases
                .iter()
                .map(|alias| alias.to_string())
                .filter(|alias| alias != arg.get_id()),
        );
    }

    // Handle environment variables
    if let Some(env_var) = arg.get_env() {
        aliases.push(env_var.to_string_lossy().to_string())
    }

    // Ignore if all aliases have been filtered out.
    // This will usually happen when there is 1 alias returned by get_all_aliases,
    // which is the long argument version, which is equivalent to the argument id
    // and is therefore redundant
    if aliases.is_empty() {
        return None;
    }

    Some(el::section((
        el::h4("Also known as:"),
        el::ul(
            aliases
                .into_iter()
                .map(el::samp)
                .map(el::li)
                .collect::<Vec<_>>(),
        ),
    )))
}

fn source(arg_matches: Option<&ArgMatches>, arg: &Arg) -> impl View<Arg> + ViewMarker {
    el::span(if let Some(arg_matches) = arg_matches.as_ref() {
        if let Some(source) = arg_matches.value_source(arg.get_id().as_str()) {
            match source {
                ValueSource::DefaultValue => "default",
                ValueSource::EnvVariable => "environment",
                ValueSource::CommandLine => "cmdline",
                _ => "todo",
            }
        } else {
            "none"
        }
    } else if !arg.get_default_values().is_empty() {
        "Error, potential defaults:"
    } else {
        "Error parsing argument"
    })
}

fn matched_values(arg_matches: Option<&ArgMatches>, arg: &Arg) -> impl View<Arg> + ViewMarker {
    let default_matches = ArgMatches::default();
    let arg_matches = arg_matches.unwrap_or(&default_matches);

    el::ul(
        if let Ok(raw_occurences) = arg_matches.try_get_raw_occurrences(arg.get_id().as_str()) {
            raw_occurences
                .unwrap_or_default()
                .map(|os_strings| {
                    el::li(el::kbd(
                        os_strings
                            .map(|s| s.to_string_lossy().to_string())
                            .collect::<Vec<String>>()
                            .join(" "),
                    ))
                })
                .collect::<Vec<_>>()
        } else {
            arg.get_default_values()
                .into_iter()
                .map(|os_str| os_str.to_string_lossy().to_string())
                .map(el::kbd)
                .map(el::li)
                .collect::<Vec<_>>()
        },
    )
}

fn item(arg_matches: Option<&ArgMatches>, arg: &Arg) -> impl View<Arg> + ViewMarker {
    el::section((
        // Name of the argument
        el::h3(arg.get_id().to_string().to_uppercase()),
        // Hand-written documentation
        crate::util::collapsible_docs(arg.get_help(), arg.get_long_help()),
        // The argument's type info
        type_description(arg),
        // Number of allowed arguments
        arg.get_num_args().map(|arg_range| arg_range.to_string()),
        // Source location (defaults, command line etc)
        source(arg_matches, arg),
        // Matched values
        matched_values(arg_matches, arg),
        // Alternate sources
        aliases(arg),
    ))
    .class("arg_item")
}

pub fn args_view(
    command: &Command,
    arg_matches: Option<&ArgMatches>,
) -> impl ViewSequence<AppState> {
    command
        .get_opts()
        .map(|arg| {
            Adapt::new(
                |_state, _thunk| {
                    // TODO: actually re-compute this value
                    MessageResult::Nop
                },
                item(arg_matches, arg),
            )
        })
        .collect::<Vec<_>>()
}