use colored::*;
use envmnt::{ExpandOptions, ExpansionType};
use std::{boxed::Box, fs::OpenOptions, io::{BufRead, BufReader, BufWriter, Read, Write}, path::PathBuf, str::FromStr};

pub mod bits;
pub mod compress;
pub mod raw;
pub mod vec_helper;

pub type BufferedInputFile = BufReader<std::fs::File>;
pub type Output            = Box<dyn Write>;
pub type Input             = Box<dyn BufRead>;

#[macro_export]
macro_rules! format_if_error {
    ($result:expr, $format_text:literal) => {
        tool_helper::callback_if_any_error($result, |error_text| {
            format!($format_text, error_text=error_text)
        })
    };
    ($result:expr, $format_text:literal, $($arg:expr)*) => {
        tool_helper::callback_if_any_error($result, |error_text| {
            format!($format_text, $($arg),*, error_text=error_text)
        })
    };
}

#[macro_export]
macro_rules! format_if_error_drop_cause {
    ($result:expr, $format_text:literal) => {
        tool_helper::callback_if_any_error($result, |error_text| {
            format!($format_text)
        })
    };
    ($result:expr, $format_text:literal, $($arg:expr)*) => {
        tool_helper::callback_if_any_error($result, |error_text| {
            format!($format_text, $($arg),*)
        })
    };
}

const DEFAULT_ENV_EXPAND_OPTIONS : ExpandOptions = ExpandOptions{expansion_type: Some(ExpansionType::All), default_to_empty: false};

pub struct Error {
    pub exit_code: i32,
    pub text:      String,
}

impl Error {
    const DEFAULT_EXITCODE:i32 = -1;

    pub fn from_str(str: &str) -> Error {
        Self::from_text(str.to_owned())
    }

    pub fn from_text(text: String) -> Error {
        Error{exit_code: Self::DEFAULT_EXITCODE, text}
    }

    pub fn from_error<T>(error: T) -> Error where T: std::fmt::Display {
        Error::from_text(error.to_string())
    }

    pub fn from_core_error<T>(error: T) -> Error where T: core::fmt::Display {
        Error::from_text(error.to_string())
    }

    pub fn from_callback<F>(callback: F) -> Error where F: Fn() -> String {
        Error::from_text(callback())
    }

    pub fn not_implemented(function: &str) -> Error {
        Error::from_text(format!("{} not implemented yet", function))
    }

    pub fn try_or_new<T, S>(result: std::result::Result<T, S>) -> Result<T, Error> where S: std::fmt::Display {
        match result {
            Ok(value)  => Ok(value),
            Err(error) => Err(Error{exit_code: Self::DEFAULT_EXITCODE, text: error.to_string()}),
        }
    }

    pub fn ok_or_new<T, F>(option: Option<T>, error_text: F) -> Result<T, Error> where F: Fn () -> String{
        Ok(option.ok_or(Error::from_callback(error_text))?)
    }

    pub fn print_generic_to_std_err<T: std::fmt::Display>(object: &T) {
        eprintln!("{}", format!("ERROR: {}", object).red());
    }

    pub fn print_to_std_err(&self) {
        Self::print_generic_to_std_err(self)
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.text)
    }
}

impl std::convert::From<std::io::Error> for Error {
    fn from(error: std::io::Error) -> Self {
        Error{exit_code: -1, text: error.to_string()}
    }
}

impl std::convert::From<cdtypes::Error> for Error {
    fn from(error: cdtypes::Error) -> Self {
        Error::from_error(error)
    }
}

impl std::convert::From<std::convert::Infallible> for Error {
    fn from(error: std::convert::Infallible) -> Self {
        Error::from_error(error)
    }
}

impl std::convert::From<std::sync::mpsc::RecvError> for Error {
    fn from(error: std::sync::mpsc::RecvError) -> Self {
        Error::from_error(error)
    }
}

impl std::convert::From<hound::Error> for Error {
    fn from(error: hound::Error) -> Self {
        Error::from_error(error)
    }
}

pub fn exit_with_error(error: Error) {
    error.print_to_std_err();
    std::process::exit(error.exit_code);
}

pub fn print_warning(str: String) {
    eprintln!("{}", str.yellow());
}

pub fn prefix_if_error<T>(prefix: &str, result: Result<T, Error>) -> Result<T, Error> {
    match result {
        Ok(value)      => Ok(value),
        Err(mut error) => {
            let mut new_text = String::from(prefix);

            new_text.push_str(error.text.as_str());
            error.text = new_text;
            Err(error)
        },
    }
}

pub fn callback_if_error<F: Fn(String) -> String, T>(result: Result<T, Error>, callback: F) -> Result<T, Error> {
    match result {
        Ok(value)      => Ok(value),
        Err(mut error) => {
            error.text = callback(error.text);

            Err(error)
        }
    }
}

pub fn callback_if_any_error<F: Fn(String) -> String, T, E: std::string::ToString>(result: Result<T, E>, callback: F) -> Result<T, Error> {
    match result {
        Ok(value)  => Ok(value),
        Err(error) => {
            Err(Error::from_text(callback(error.to_string())))
        }
    }
}

pub fn string_with_env_from(str: &str) -> String {
    String::from(envmnt::expand(str, Some(DEFAULT_ENV_EXPAND_OPTIONS)))
}

pub fn path_with_env_from(path: &str) -> PathBuf {
    PathBuf::from(envmnt::expand(path, Some(DEFAULT_ENV_EXPAND_OPTIONS)))
}

pub fn open_output_file(output_path: &PathBuf) -> Result<BufWriter<std::fs::File>, Error> {
    Ok(std::io::BufWriter::new(std::fs::File::create(win_or_wsl_path(output_path)?)?))
}

pub fn open_output(output_file: &Option<PathBuf>) -> Result<Output, Error> {
    match output_file {
        Some(output_path) => Ok(Box::new(open_output_file(&output_path)?)),
        None              => Ok(Box::new(BufWriter::new(std::io::stdout()))),
    }
}

pub fn open_input(input_file: &Option<PathBuf>) -> Result<Input, Error> {
    match input_file {
        Some(input_path) => Ok(Box::new(open_input_file_buffered(&input_path)?)),
        None             => Ok(Box::new(BufReader::new(std::io::stdin()))),
    }
}

pub fn open_input_file_buffered(input_path: &PathBuf) -> Result<BufferedInputFile, Error> {
    Ok(BufReader::new(std::fs::File::open(win_or_wsl_path(input_path)?)?))
}

pub fn os_str_to_string(input: &std::ffi::OsStr, name: &str) -> Result<String, Error> {
    Ok(Error::ok_or_new(input.to_str(), ||format!("Converting {} to UTF-8 failed", name))?.to_owned())
}

pub fn get_file_name_from_path_buf(input: &PathBuf, name: &str) -> Result<String, Error> {
    os_str_to_string(Error::ok_or_new(input.file_name(), ||format!("No {} file name found", name))?, name)
}

pub fn input_to_vec(input: Input) -> Result<Vec<u8>, Error> {
    let mut data = Vec::new();

    for byte in input.bytes() {
        data.push(byte?);
    }

    Ok(data)
}

pub fn read_file(file_path: &PathBuf) -> Result<Vec<u8>, Error> {
    if let Some(ext) = file_path.extension() {
        if ext == "subst" {
            // File needs substitution!
            let file_content = read_file_to_string(file_path)?;
            return Ok(string_with_env_from(&file_content).into_bytes());
        }
    }

    match std::fs::read(win_or_wsl_path(file_path)?) {
        Ok(data)   => Ok(data),
        Err(error) => create_file_read_error(file_path, error),
    }
}

pub fn read_file_to_string(file_path: &PathBuf) -> Result<String, Error> {
    match std::fs::read_to_string(win_or_wsl_path(file_path)?) {
        Ok(string) => Ok(string),
        Err(error) => create_file_read_error(file_path, error),
    }
}

pub fn write_file(file_path: &PathBuf, data: Vec<u8>) -> Result<(), Error> {
    let mut file = OpenOptions::new().read(true).write(true).truncate(true).create(true).open(file_path)?;

    file.write_all(data.as_slice())?;
    Ok(())
}

fn create_file_read_error<T>(file_path: &PathBuf, error: std::io::Error) -> Result<T, Error> {
    Err(Error::from_text(format!("Failed reading file {} with error: \"{}\"", file_path.display(), error)))
}

fn win_or_wsl_path(path: &PathBuf) -> Result<PathBuf, Error> {
    let path = path.clone();
    if path.exists() {
        Ok(path)
    }

    else {
        match path.into_os_string().into_string() {
            Ok(path)   => Ok(PathBuf::from_str(wslpath::force_convert(path).as_str())?),
            Err(error) => Err(Error::from_text(format!("Failed converting {:?} to string for win/wsl mapping", error)))
        }
    }
}