use std::convert::From;
use std::env;
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};

use clap::builder::PossibleValue;
use clap::{command, value_parser, Arg, ArgAction, ValueEnum};

extern crate minifier;
use minifier::{css, js, json};

pub struct Cli;

impl Cli {
    pub fn init() {
        let matches = command!()
            .arg(
                Arg::new("FileType")
                    .short('t')
                    .long("type")
                    .help(
                        "File Extention without dot. This option is optional.
If you don't provide this option, all input files
type will detect via extension of input file.
",
                    )
                    .required(false)
                    .value_parser(value_parser!(FileType)),
            )
            .arg(
                Arg::new("output")
                    .short('o')
                    .long("out")
                    .help("Output file or directory (Default is parent dir of input files)")
                    .required(false)
                    .value_parser(value_parser!(PathBuf)),
            )
            .arg(
                Arg::new("FILE")
                    .help("Input Files...")
                    .num_args(1..)
                    .value_parser(value_parser!(PathBuf))
                    .action(ArgAction::Append),
            )
            .get_matches();
        let args: Vec<&PathBuf> = matches
            .get_many::<PathBuf>("FILE")
            .unwrap_or_default()
            .collect::<Vec<_>>();
        let ext: Option<&FileType> = matches.get_one::<FileType>("FileType");
        let out: Option<&PathBuf> = matches.get_one::<PathBuf>("output");
        for path in args.into_iter() {
            write_out_file(path, out, ext);
        }
    }
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum FileType {
    // Html,
    Css,
    Js,
    Json,
    Unknown,
}

impl FileType {
    fn as_str(&self) -> &str {
        match self {
            Self::Css => "css",
            Self::Js => "js",
            Self::Json => "json",
            Self::Unknown => "unknown",
        }
    }
}

impl ValueEnum for FileType {
    fn value_variants<'a>() -> &'a [Self] {
        &[FileType::Css, FileType::Js, FileType::Json]
    }
    fn to_possible_value(&self) -> Option<PossibleValue> {
        Some(match *self {
            FileType::Css => PossibleValue::new("css")
                .help("All the files will be consider as CSS, regardless of their extension."),
            FileType::Js => PossibleValue::new("js").help(
                "All the files will be consider as JavaScript, regardless of their extension.",
            ),
            FileType::Json => PossibleValue::new("json")
                .help("All the files will be consider as JSON, regardless of their extension."),
            FileType::Unknown => panic!("unknow file"),
        })
    }
}
impl std::str::FromStr for FileType {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        for variant in Self::value_variants() {
            if variant.to_possible_value().unwrap().matches(s, false) {
                return Ok(*variant);
            };
        }
        Err(format!("Invalid variant: {s}"))
    }
}

impl From<&PathBuf> for FileType {
    fn from(value: &PathBuf) -> Self {
        let ext = value.extension();
        if ext.is_none() {
            return Self::Unknown;
        };
        match ext.unwrap().to_ascii_lowercase().to_str().unwrap() {
            "css" => Self::Css,
            "js" => Self::Js,
            "json" => Self::Json,
            _ => Self::Unknown,
        }
    }
}

pub fn get_all_data<T: AsRef<Path>>(file_path: T) -> io::Result<String> {
    let mut file = File::open(file_path)?;
    let mut data = String::new();
    file.read_to_string(&mut data).unwrap();
    Ok(data)
}

fn write_out_file(file_path: &PathBuf, out_path: Option<&PathBuf>, ext: Option<&FileType>) {
    let file_ext = if let Some(v) = ext {
        v
    } else {
        &FileType::from(file_path)
    };
    if file_ext == &FileType::Unknown {
        eprintln!("{file_path:?}: unknow file extension...");
        return;
    };
    match get_all_data(file_path) {
        Ok(content) => {
            let out = match out_path {
                Some(op) => {
                    let mut op = op.clone();
                    if op.is_dir() {
                        op.push(file_path);
                        op.set_extension(format!("min.{}", file_ext.as_str()));
                    };
                    if op.parent().is_some() && !op.parent().unwrap().is_dir() {
                        std::fs::create_dir_all(op.parent().unwrap()).unwrap();
                    };
                    op
                }
                None => {
                    let mut p = file_path.clone();
                    p.set_extension(format!("min.{}", file_ext.as_str()));
                    p
                }
            };
            if let Ok(mut file) = OpenOptions::new()
                .truncate(true)
                .write(true)
                .create(true)
                .open(&out)
            {
                let func = |s: &str| -> String {
                    match file_ext {
                        FileType::Css => {
                            css::minify(s).expect("css minification failed").to_string()
                        }
                        FileType::Js => js::minify(s).to_string(),
                        FileType::Json => json::minify(s).to_string(),
                        FileType::Unknown => panic!("{file_path:?}: unknow file extension..."),
                    }
                };
                if let Err(e) = write!(file, "{}", func(&content)) {
                    eprintln!("Impossible to write into {out:?}: {e}");
                } else {
                    println!("{file_path:?}: done -> generated into {out:?}");
                }
            } else {
                eprintln!("Impossible to create new file: {out:?}");
            }
        }
        Err(e) => eprintln!("{file_path:?}: {e}"),
    }
}
