use std::collections::HashMap;
use std::error::Error as StdError;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::{cmp, process};
use structopt::StructOpt;
use sv_parser::{parse_sv, Define, DefineText};
use sv_parser_error::Error;
use sv_parser_pp::preprocess::preprocess;

#[derive(StructOpt)]
struct Opt {
    pub files: Vec<PathBuf>,

    /// Include path
    #[structopt(short = "i", long = "include", multiple = true, number_of_values = 1)]
    pub includes: Vec<PathBuf>,

    /// Show syntax tree
    #[structopt(short = "t", long = "tree")]
    pub tree: bool,

    /// Show preprocesed text
    #[structopt(short = "p", long = "pp")]
    pub pp: bool,

    /// Allow incomplete source code
    #[structopt(long = "incomplete")]
    pub incomplete: bool,

    /// Define
    #[structopt(short = "d", long = "define", multiple = true, number_of_values = 1)]
    pub defines: Vec<String>,

    /// Quiet
    #[structopt(short = "q", long = "quiet")]
    pub quiet: bool,
}

fn main() {
    let opt = Opt::from_args();

    let mut defines = HashMap::new();
    for define in &opt.defines {
        let mut define = define.splitn(2, '=');
        let ident = String::from(define.next().unwrap());
        let text = if let Some(x) = define.next() {
            let x = enquote::unescape(x, None).unwrap();
            Some(DefineText::new(x, None))
        } else {
            None
        };
        let define = Define::new(ident.clone(), vec![], text);
        defines.insert(ident, Some(define));
    }

    let builder = std::thread::Builder::new().stack_size(20 * 1024 * 1024);

    let child = builder
        .spawn(move || {
            let mut exit = 0;
            for path in &opt.files {
                if opt.pp {
                    match preprocess(&path, &defines, &opt.includes, false, false) {
                        Ok((preprocessed_text, new_defines)) => {
                            println!("{}", preprocessed_text.text());
                            defines = new_defines;
                        }
                        _ => (),
                    }
                } else {
                    match parse_sv(&path, &defines, &opt.includes, false, opt.incomplete) {
                        Ok((syntax_tree, new_defines)) => {
                            if opt.tree {
                                println!("{}", syntax_tree);
                            }
                            defines = new_defines;
                            if !opt.quiet {
                                println!("parse succeeded: {:?}", path);
                            }
                        }
                        Err(x) => {
                            match x {
                                Error::Parse(Some((origin_path, origin_pos))) => {
                                    println!("parse failed: {:?}", path);
                                    print_parse_error(&origin_path, &origin_pos);
                                }
                                x => {
                                    println!("parse failed: {:?} ({})", path, x);
                                    let mut err = x.source();
                                    while let Some(x) = err {
                                        println!("  Caused by {}", x);
                                        err = x.source();
                                    }
                                }
                            }
                            exit = 1;
                        }
                    }
                }
            }
            process::exit(exit);
        })
        .expect("thread spawn failure");

    let _ = child.join();
}

static CHAR_CR: u8 = 0x0d;
static CHAR_LF: u8 = 0x0a;

fn print_parse_error(origin_path: &PathBuf, origin_pos: &usize) {
    let mut f = File::open(&origin_path).unwrap();
    let mut s = String::new();
    let _ = f.read_to_string(&mut s);

    let mut pos = 0;
    let mut column = 1;
    let mut last_lf = None;
    while pos < s.len() {
        if s.as_bytes()[pos] == CHAR_LF {
            column += 1;
            last_lf = Some(pos);
        }
        pos += 1;

        if *origin_pos == pos {
            let row = if let Some(last_lf) = last_lf {
                pos - last_lf
            } else {
                pos + 1
            };
            let mut next_crlf = pos;
            while next_crlf < s.len() {
                if s.as_bytes()[next_crlf] == CHAR_CR || s.as_bytes()[next_crlf] == CHAR_LF {
                    break;
                }
                next_crlf += 1;
            }

            let column_len = format!("{}", column).len();

            print!(" {}:{}:{}\n", origin_path.to_string_lossy(), column, row);

            print!("{}|\n", " ".repeat(column_len + 1));

            print!("{} |", column);

            let beg = if let Some(last_lf) = last_lf {
                last_lf + 1
            } else {
                0
            };
            print!(
                " {}\n",
                String::from_utf8_lossy(&s.as_bytes()[beg..next_crlf])
            );

            print!("{}|", " ".repeat(column_len + 1));

            print!(
                " {}{}\n",
                " ".repeat(pos - beg),
                "^".repeat(cmp::min(origin_pos + 1, next_crlf) - origin_pos)
            );
        }
    }
}
