mod basename;
mod command;
mod dirname;
mod exists;
mod expand;
mod join;
mod parse;
mod relative_to;
mod split;
mod r#type;

use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::{
    ColumnPath, Dictionary, MaybeOwned, Primitive, ShellTypeName, UntaggedValue, Value,
};
use nu_source::Span;
use std::path::{Path, PathBuf};
use std::sync::Arc;

pub use basename::PathBasename;
pub use command::Path as PathCommand;
pub use dirname::PathDirname;
pub use exists::PathExists;
pub use expand::PathExpand;
pub use join::PathJoin;
pub use parse::PathParse;
pub use r#type::PathType;
pub use relative_to::PathRelativeTo;
pub use split::PathSplit;

#[cfg(windows)]
const ALLOWED_COLUMNS: [&str; 4] = ["prefix", "parent", "stem", "extension"];
#[cfg(not(windows))]
const ALLOWED_COLUMNS: [&str; 3] = ["parent", "stem", "extension"];

trait PathSubcommandArguments {
    fn get_column_paths(&self) -> &Vec<ColumnPath>;
}

fn encode_path(
    entries: &Dictionary,
    orig_span: Span,
    new_span: Span,
) -> Result<PathBuf, ShellError> {
    if entries.length() == 0 {
        return Err(ShellError::labeled_error_with_secondary(
            "Empty table cannot be encoded as a path",
            "got empty table",
            new_span,
            "originates from here",
            orig_span,
        ));
    }

    for col in entries.keys() {
        if !ALLOWED_COLUMNS.contains(&col.as_str()) {
            let msg = format!(
                "Column '{}' is not valid for a structured path. Allowed columns are: {}",
                col,
                ALLOWED_COLUMNS.join(", ")
            );
            return Err(ShellError::labeled_error_with_secondary(
                "Invalid column name",
                msg,
                new_span,
                "originates from here",
                orig_span,
            ));
        }
    }

    // At this point, the row is known to have >0 columns, all of them allowed
    let mut result = PathBuf::new();

    #[cfg(windows)]
    if let MaybeOwned::Borrowed(val) = entries.get_data("prefix") {
        let s = val.as_string()?;
        if !s.is_empty() {
            result.push(&s);
        }
    };

    if let MaybeOwned::Borrowed(val) = entries.get_data("parent") {
        let p = val.as_string()?;
        if !p.is_empty() {
            result.push(p);
        }
    };

    let mut basename = String::new();

    if let MaybeOwned::Borrowed(val) = entries.get_data("stem") {
        let s = val.as_string()?;
        if !s.is_empty() {
            basename.push_str(&s);
        }
    };

    if let MaybeOwned::Borrowed(val) = entries.get_data("extension") {
        let s = val.as_string()?;
        if !s.is_empty() {
            basename.push('.');
            basename.push_str(&s);
        }
    };

    if !basename.is_empty() {
        result.push(basename);
    }

    Ok(result)
}

fn join_path(parts: &[Value], new_span: &Span) -> Result<PathBuf, ShellError> {
    parts
        .iter()
        .map(|part| match &part.value {
            UntaggedValue::Primitive(Primitive::String(s)) => Ok(Path::new(s)),
            UntaggedValue::Primitive(Primitive::FilePath(pb)) => Ok(pb.as_path()),
            _ => {
                let got = format!("got {}", part.type_name());
                Err(ShellError::labeled_error_with_secondary(
                    "Cannot join values that are not paths or strings.",
                    got,
                    new_span,
                    "originates from here",
                    part.tag.span,
                ))
            }
        })
        .collect()
}

fn handle_value<F, T>(action: &F, v: &Value, span: Span, args: Arc<T>) -> Result<Value, ShellError>
where
    T: PathSubcommandArguments,
    F: Fn(&Path, Tag, &T) -> Value,
{
    match &v.value {
        UntaggedValue::Primitive(Primitive::FilePath(buf)) => Ok(action(buf, v.tag(), &args)),
        UntaggedValue::Primitive(Primitive::String(s)) => Ok(action(s.as_ref(), v.tag(), &args)),
        UntaggedValue::Row(entries) => {
            // implicit path join makes all subcommands understand the structured path
            let path_buf = encode_path(entries, v.tag().span, span)?;
            Ok(action(&path_buf, v.tag(), &args))
        }
        UntaggedValue::Table(parts) => {
            // implicit path join makes all subcommands understand path split into parts
            let path_buf = join_path(parts, &span)?;
            Ok(action(&path_buf, v.tag(), &args))
        }
        other => {
            let got = format!("got {}", other.type_name());
            Err(ShellError::labeled_error_with_secondary(
                "Value is a not string, path, row, or table",
                got,
                span,
                "originates from here",
                v.tag().span,
            ))
        }
    }
}

fn operate_column_paths<F, T>(
    input: crate::InputStream,
    action: &'static F,
    span: Span,
    args: Arc<T>,
) -> OutputStream
where
    T: PathSubcommandArguments + Send + Sync + 'static,
    F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static,
{
    input
        .map(move |v| {
            let mut ret = v;

            for path in args.get_column_paths() {
                let cloned_args = Arc::clone(&args);
                ret = match ret.swap_data_by_column_path(
                    path,
                    Box::new(move |old| handle_value(&action, old, span, cloned_args)),
                ) {
                    Ok(v) => v,
                    Err(e) => Value::error(e),
                };
            }

            ret
        })
        .to_output_stream()
}

fn operate<F, T>(
    input: crate::InputStream,
    action: &'static F,
    span: Span,
    args: Arc<T>,
) -> OutputStream
where
    T: PathSubcommandArguments + Send + Sync + 'static,
    F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static,
{
    if args.get_column_paths().is_empty() {
        input
            .map(
                move |v| match handle_value(&action, &v, span, Arc::clone(&args)) {
                    Ok(v) => v,
                    Err(e) => Value::error(e),
                },
            )
            .to_output_stream()
    } else {
        operate_column_paths(input, action, span, args)
    }
}
