use std::fmt::Write;

use hwcalc_lib as hc;

const PROMPT_STRING: &str = "> ";

fn main() {
    let expr = std::env::args().skip(1).collect::<Vec<_>>().join(" ");

    let success = if expr.is_empty() {
        #[allow(unused)]
        let interactive = false;
        #[cfg(unix)]
        let interactive = {
            use std::os::unix::io::AsRawFd;
            nix::unistd::isatty(std::io::stdin().as_raw_fd()).unwrap_or(false)
        };

        repl(interactive)
            .map_err(|e| eprintln!("error: {}", e))
            .unwrap_or(false)
    } else {
        eval_print(&expr, false)
    };
    std::process::exit(if success { 0 } else { 1 });
}

fn eval_print(line: &str, interactive: bool) -> bool {
    if matches!(line, "\n" | "") {
        return true;
    }

    let num = match hc::eval(line) {
        Ok(e) => e,
        Err(hc::Error { kind, span }) => {
            if interactive {
                eprintln!(
                    "{}\x1b[1;31m{}\x1b[0m\n\x1b[1;31merror: \x1b[0m{}",
                    " ".repeat(span.start() + PROMPT_STRING.chars().count()),
                    "^".repeat(span.of(line).chars().count()),
                    kind,
                );
            } else {
                eprintln!(
                    "error: {} -- col {}: '{}'",
                    kind,
                    span.start(),
                    span.of(line),
                );
            }
            return false;
        }
    };

    let mut res = format!("{}", num);
    if !num.integer() {
        write!(res, " (= {})", num.value()).unwrap();
    }
    if num.to_signed().map_or(false, |n| n.negative()) {
        let other = if num.signed() {
            Some(num.to_unsigned())
        } else {
            num.to_signed()
        };
        if let Some(other) = other {
            if other.signed() || !other.negative() {
                write!(res, " (= {})", other).unwrap();
            }
        }
    }
    {
        const P: &str = "\n = ";
        write!(res, "{}{:b}", P, num).unwrap();
        write!(res, "{}{:o}", P, num).unwrap();
        write!(res, "{}{:x}", P, num).unwrap();
    }
    println!("{}", color(&res));

    true
}

fn color(src: &str) -> String {
    const DARK: u8 = 3;
    const BRIGHT: u8 = 9;
    const GREEN: u8 = 2;
    const BLUE: u8 = 4;
    const MAGENTA: u8 = 5;
    const CYAN: u8 = 6;

    const COL_RESET: &str = "\x1b[0m";
    const COL_SYM: &str = "\x1b[33m";
    const COL_TYPE: &str = "\x1b[0;93m";
    const COL_INVALID: &str = "\x1b[91m";

    hc::lex::tokens(src)
        .map(|(kind, span)| {
            let token = span.of(src);
            if let hc::lex::TokenKind::Num(num) = kind {
                let num_start = if num.prefixed { 2 } else { 0 };
                let prefix = &token[0..num_start];
                let mut numeral = token[num_start..].to_string();

                let color_digit = match num.base {
                    hc::Base::Bin => CYAN,
                    hc::Base::Oct => GREEN,
                    hc::Base::Dec => MAGENTA,
                    hc::Base::Hex => BLUE,
                };
                let col_dark = format!("\x1b[{}{}m", DARK, color_digit);
                let col_bright = format!("\x1b[{}{}m", BRIGHT, color_digit);

                for c in ['_', '(', ')', '.'] {
                    numeral = numeral.replace(c, &format!("{}{}{}", col_dark, c, col_bright));
                }

                let mut res = String::new();
                res.push_str(&col_dark);
                res.push_str(prefix);
                res.push_str(&col_bright);
                res.push_str(&numeral);
                res.push_str(COL_RESET);
                res
            } else {
                let color = match kind {
                    hc::lex::TokenKind::Sym(..) => COL_SYM,
                    hc::lex::TokenKind::Ty(..) => COL_TYPE,
                    hc::lex::TokenKind::Invalid => COL_INVALID,
                    _ => "",
                };
                format!("{}{}{}", color, token, COL_RESET)
            }
        })
        .collect::<String>()
}

#[cfg(not(feature = "readline"))]
fn repl(interactive: bool) -> std::io::Result<bool> {
    use std::io::Write;
    let mut success = true;
    loop {
        let mut line = String::new();
        if interactive {
            std::io::stdout().write_all(PROMPT_STRING.as_bytes())?;
            std::io::stdout().flush()?;
        }
        std::io::stdin().read_line(&mut line)?;
        if line.is_empty() {
            break;
        }
        success = eval_print(&line, interactive);
    }
    Ok(success)
}

#[cfg(feature = "readline")]
use readline::repl;

#[cfg(feature = "readline")]
mod readline {
    const PROGRAM_NAME: &str = "hwcalc";

    use rustyline_derive::Completer;
    use rustyline_derive::Helper;
    use rustyline_derive::Hinter;
    use rustyline_derive::Validator;

    #[derive(Helper, Completer, Hinter, Validator)]
    struct Helper;

    impl rustyline::highlight::Highlighter for Helper {
        fn highlight<'l>(&self, line: &'l str, _pos: usize) -> std::borrow::Cow<'l, str> {
            super::color(line).into()
        }

        fn highlight_char(&self, _line: &str, _pos: usize) -> bool {
            true
        }
    }

    pub fn repl(interactive: bool) -> rustyline::Result<bool> {
        let mut rl = rustyline::Editor::<Helper>::new()?;
        rl.set_helper(Some(Helper));
        let cache_dir = dirs::cache_dir().map(|d| d.join(PROGRAM_NAME));
        let path_hist = cache_dir.as_ref().map(|d| d.join("history"));
        path_hist.as_ref().and_then(|p| rl.load_history(p).ok());

        let mut success = true;
        loop {
            match rl.readline(super::PROMPT_STRING) {
                Ok(line) => {
                    rl.add_history_entry(line.as_str());
                    let nowrap = rl.dimensions().map_or(false, |(w, _)| {
                        line.chars().count() + super::PROMPT_STRING.chars().count() < w
                    });
                    success = super::eval_print(&line, interactive && nowrap);
                }
                Err(rustyline::error::ReadlineError::Interrupted)
                | Err(rustyline::error::ReadlineError::Eof) => break,
                Err(e) => return Err(e),
            }
        }

        if let Some(d) = &cache_dir {
            if !d.is_dir() {
                std::fs::DirBuilder::new()
                    .recursive(true)
                    .create(d)
                    .unwrap_or_else(|e| {
                        eprintln!("warning: failed to create dir '{}' -- {}", d.display(), e);
                    });
            }
            if let Some(p) = &path_hist {
                rl.save_history(p).unwrap_or_else(|e| {
                    eprintln!(
                        "warning: failed to save history to '{}' -- {}",
                        p.display(),
                        e
                    );
                });
            }
        }

        Ok(success)
    }
}
