extern crate binjs;
extern crate clap;
extern crate env_logger;
use binjs::io::{CompressionTarget, Format};
use binjs::source::{Shift, SourceParser};
use binjs::specialized::es6::io::Encoder;
use binjs::specialized::es6::Enrich;
use std::fs;
use std::io::*;
use std::path::{Path, PathBuf};
use std::thread;
use clap::*;
fn export_section(
    dest_bin_path: &Option<PathBuf>,
    target: &mut CompressionTarget,
    extension: &str,
) {
    let path = dest_bin_path
        .as_ref()
        .expect("Cannot write partial file without a destination")
        .with_extension(extension);
    let (data, _) = target.done().expect("Could not finalize compression");
    fs::write(&path, data.as_ref())
        .unwrap_or_else(|e| panic!("Could not write destination file {:?}: {:?}", path, e));
    target.reset();
}
struct Options<'a> {
    parser: &'a Shift,
    format: Format,
    dest_dir: Option<PathBuf>,
    enricher: Enrich,
    show_ast: bool,
    quiet: bool,
}
macro_rules! progress {
    ($quiet:expr, $($args:tt)*) => {
        if !$quiet {
            println!($($args)*);
        }
    }
}
enum Source<'a> {
    FromFile { path: &'a Path },
    FromStdin { text: String },
}
struct EncodeParams<'a> {
    source: Source<'a>,
    dest_bin_path: Option<PathBuf>,
    dest_txt_path: Option<PathBuf>,
}
fn handle_path<'a>(options: &mut Options<'a>, source_path: &Path, sub_dir: &Path) {
    progress!(options.quiet, "Treating {:?} ({:?})", source_path, sub_dir);
    let is_dir = fs::metadata(source_path).unwrap().is_dir();
    if is_dir {
        let file_name = source_path
            .file_name()
            .unwrap_or_else(|| panic!("Invalid source path {:?}", source_path));
        let sub_dir = sub_dir.join(file_name);
        for entry in fs::read_dir(source_path)
            .expect("Could not open directory")
            .map(|dir| dir.unwrap())
        {
            handle_path(options, entry.path().as_path(), &sub_dir);
        }
        return;
    }
    if let Some(Some("js")) = source_path.extension().map(std::ffi::OsStr::to_str) {
        
    } else {
        progress!(options.quiet, "Skipping {:?}", source_path);
        return;
    }
    let (dest_txt_path, dest_bin_path) = match options.dest_dir {
        None => (None, None), 
        Some(ref d) => {
            let file_name = source_path
                .file_stem()
                .expect("Could not extract file name");
            fs::create_dir_all(d.join(sub_dir))
                .expect("Could not find or create destination directory");
            let mut bin_path = d.join(sub_dir);
            bin_path.push(format!("{}.binjs", file_name.to_str().unwrap()));
            let mut txt_path = d.join(sub_dir);
            txt_path.push(format!("{}.js", file_name.to_str().unwrap()));
            (Some(txt_path), Some(bin_path))
        }
    };
    if let Some(ref bin_path) = dest_bin_path {
        progress!(options.quiet, "Output: {}", bin_path.to_string_lossy());
    } else {
        progress!(options.quiet, "Compressing to memory");
    }
    progress!(options.quiet, "Parsing.");
    handle_path_or_text(
        options,
        EncodeParams {
            source: Source::FromFile { path: source_path },
            dest_bin_path,
            dest_txt_path,
        },
    );
}
fn handle_path_or_text<'a>(options: &mut Options<'a>, params: EncodeParams) {
    let (source_path, source_len, mut ast) = match params.source {
        Source::FromFile { path } => (
            Some(path),
            fs::metadata(path).expect("Could not open source").len(),
            options
                .parser
                .parse_file(path)
                .expect("Could not parse source"),
        ),
        Source::FromStdin { text } => (
            None,
            text.len() as u64,
            options
                .parser
                .parse_str(text.as_str())
                .expect("Could not parse source"),
        ),
    };
    let dest_bin_path = params.dest_bin_path;
    let dest_txt_path = params.dest_txt_path;
    options
        .enricher
        .enrich(&mut ast)
        .expect("Could not enrich AST");
    if options.show_ast {
        serde_json::to_writer_pretty(std::io::stdout(), &ast).unwrap();
        println!();
    }
    progress!(options.quiet, "Encoding.");
    let encoder = Encoder::new();
    let encoding_path = match dest_bin_path {
        None => None,
        Some(ref buf) => Some(buf.as_path()),
    };
    let data = encoder
        .encode(encoding_path, &mut options.format, &ast)
        .expect("Could not encode");
    if dest_txt_path.is_some() {
        options
            .format
            .with_sections::<_, ()>(|contents, name| {
                export_section(&dest_bin_path, contents, name);
                Ok(())
            })
            .expect("Could not write sections");
    };
    let dest_len = data.as_ref().as_ref().len();
    if let Some(ref bin_path) = dest_bin_path {
        progress!(options.quiet, "Writing binary file.");
        fs::write(bin_path, data.as_ref())
            .unwrap_or_else(|e| panic!("Could not write destination file {:?}: {:?}", bin_path, e));
    } else {
        stdout()
            .write((*data).as_ref())
            .expect("Could not write to stdout");
    }
    if let Some(ref txt_path) = dest_txt_path {
        if txt_path.exists() {
            progress!(
                options.quiet,
                "A file with name {:?} already exists, skipping copy.",
                txt_path
            );
        } else {
            progress!(options.quiet, "Copying source file.");
            fs::copy(source_path.unwrap(), txt_path).expect("Could not copy source file");
        }
    }
    progress!(
        options.quiet,
        "Successfully compressed {} bytes => {} bytes",
        source_len,
        dest_len
    );
}
fn main() {
    thread::Builder::new()
        .name("large stack dedicated thread".to_string())
        .stack_size(20 * 1024 * 1024)
        .spawn(|| {
            main_aux();
        })
        .expect("Could not launch dedicated thread")
        .join()
        .expect("Error in dedicated thread");
}
fn main_aux() {
    env_logger::init();
    let matches = App::new("BinJS encoder")
        .author("David Teller, <dteller@mozilla.com>")
        .about("Encode a JavaScript text source to a JavaScript binary source in the BinJS format.")
        .args(&[
            Arg::with_name("in")
                .long("in")
                .short("i")
                .multiple(true)
                .takes_value(true)
                .number_of_values(1)
                .help("Input files to use. Must be JS source file. May be specified multiple times. If not specified, stdin is used."),
            Arg::with_name("out")
                .long("out")
                .short("o")
                .takes_value(true)
                .help("Output directory to use. Files in this directory may be overwritten. Requires --in. If not specified, stdout is used"),
            Arg::with_name("statistics")
                .long("show-stats")
                .help("Show statistics."),
            Arg::with_name("show-ast")
                .long("show-ast")
                .help("Show pos-processed ast"),
            Arg::with_name("quiet")
                .long("quiet")
                .short("q")
                .help("Do not print progress"),
        ])
        .args(Enrich::default().args().as_slice())
        .subcommand(binjs::io::Format::subcommand())
        .get_matches();
    
    let spec = binjs::generic::es6::Library::spec();
    
    let sources: Vec<_> = matches
        .values_of("in")
        .map_or_else(|| Vec::new(), |input| input.map(Path::new).collect());
    let dest_dir = if sources.len() == 0 {
        
        
        None
    } else {
        match matches.value_of("out") {
            None => None,
            Some(path) => Some(Path::new(path).to_path_buf()),
        }
    };
    let quiet = matches.is_present("quiet") || dest_dir.is_none();
    
    let format =
        binjs::io::Format::from_matches(&spec, &matches).expect("Could not parse encoding format");
    progress!(quiet, "Using format: {}", format.name());
    let enricher = Enrich::from_matches(&matches);
    let show_stats = matches.is_present("statistics");
    
    let parser = Shift::try_new().expect("Could not launch Shift");
    let mut options = Options {
        parser: &parser,
        format,
        dest_dir,
        enricher,
        show_ast: matches.is_present("show-ast"),
        quiet,
    };
    if sources.len() == 0 {
        
        let mut buffer = String::new();
        stdin()
            .read_to_string(&mut buffer)
            .expect("Failed to read from stdin");
        handle_path_or_text(
            &mut options,
            EncodeParams {
                source: Source::FromStdin { text: buffer },
                dest_bin_path: None,
                dest_txt_path: None,
            },
        );
    } else {
        for source_path in sources {
            handle_path(&mut options, source_path, PathBuf::new().as_path());
        }
    }
    if show_stats {
        match options.format {
            Format::Multipart { ref stats, .. } => {
                progress!(options.quiet, "Statistics: {}", stats.borrow());
            }
            Format::Entropy {
                options: ref entropy,
            } => {
                progress!(
                    options.quiet,
                    "Statistics: {}",
                    entropy.statistics_for_write()
                );
            }
            _ => {
                progress!(options.quiet, "No stats available for this format");
            }
        }
    }
}