#![crate_name = "uu_od"]

/*
 * This file is part of the uutils coreutils package.
 *
 * (c) Ben Hirsch <benhirsch24@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

extern crate getopts;
extern crate byteorder;

#[macro_use]
extern crate uucore;

mod multifilereader;
mod partialreader;
mod peekreader;
mod byteorder_io;
mod formatteriteminfo;
mod prn_int;
mod prn_char;
mod prn_float;
mod parse_nrofbytes;
mod parse_formats;
mod parse_inputs;
mod inputoffset;
mod inputdecoder;
#[cfg(test)]
mod mockstream;

use std::cmp;
use std::io::Write;
use byteorder_io::*;
use multifilereader::*;
use partialreader::*;
use peekreader::*;
use formatteriteminfo::*;
use parse_nrofbytes::parse_number_of_bytes;
use parse_formats::{parse_format_flags, ParsedFormatterItemInfo};
use prn_char::format_ascii_dump;
use parse_inputs::{parse_inputs, CommandLineInputs};
use inputoffset::{InputOffset, Radix};
use inputdecoder::{InputDecoder,MemoryDecoder};

static VERSION: &'static str = env!("CARGO_PKG_VERSION");
const MAX_BYTES_PER_UNIT: usize = 8;
const PEEK_BUFFER_SIZE: usize = 4; // utf-8 can be 4 bytes

static USAGE: &'static str =
r#"Usage:
    od [OPTION]... [--] [FILENAME]...
    od [-abcdDefFhHiIlLoOsxX] [FILENAME] [[+][0x]OFFSET[.][b]]
    od --traditional [OPTION]... [FILENAME] [[+][0x]OFFSET[.][b] [[+][0x]LABEL[.][b]]]

Displays data in various human-readable formats. If multiple formats are
specified, the output will contain all formats in the order they appear on the
commandline. Each format will be printed on a new line. Only the line
containing the first format will be prefixed with the offset.

If no filename is specified, or it is "-", stdin will be used. After a "--", no
more options will be recognised. This allows for filenames starting with a "-".

If a filename is a valid number which can be used as an offset in the second
form, you can force it to be recognised as a filename if you include an option
like "-j0", which is only valid in the first form.

RADIX is one of o,d,x,n for octal, decimal, hexadecimal or none.

BYTES is decimal by default, octal if prefixed with a "0", or hexadecimal if
prefixed with "0x". The suffixes b, KB, K, MB, M, GB, G, will multiply the
number with 512, 1000, 1024, 1000^2, 1024^2, 1000^3, 1024^3, 1000^2, 1024^2.

OFFSET and LABEL are octal by default, hexadecimal if prefixed with "0x" or
decimal if a "." suffix is added. The "b" suffix will multiply with 512.

TYPE contains one or more format specifications consisting of:
    a       for printable 7-bits ASCII
    c       for utf-8 characters or octal for undefined characters
    d[SIZE] for signed decimal
    f[SIZE] for floating point
    o[SIZE] for octal
    u[SIZE] for unsigned decimal
    x[SIZE] for hexadecimal
SIZE is the number of bytes which can be the number 1, 2, 4, 8 or 16,
    or C, I, S, L for 1, 2, 4, 8 bytes for integer types,
    or F, D, L for 4, 8, 16 bytes for floating point.
Any type specification can have a "z" suffic, which will add a ASCII dump at
    the end of the line.

If an error occurred, a diagnostic message will be printed to stderr, and the
exitcode will be non-zero."#;

pub fn uumain(args: Vec<String>) -> i32 {
    let mut opts = getopts::Options::new();

    opts.optopt("A", "address-radix",
                "Select the base in which file offsets are printed.", "RADIX");
    opts.optopt("j", "skip-bytes",
                "Skip bytes input bytes before formatting and writing.", "BYTES");
    opts.optopt("N", "read-bytes",
                "limit dump to BYTES input bytes", "BYTES");
    opts.optopt("", "endian", "byte order to use for multi-byte formats", "big|little");
    opts.optopt("S", "strings",
                ("output strings of at least BYTES graphic chars. 3 is assumed when \
                 BYTES is not specified."),
                "BYTES");
    opts.optflagmulti("a", "", "named characters, ignoring high-order bit");
    opts.optflagmulti("b", "", "octal bytes");
    opts.optflagmulti("c", "", "ASCII characters or backslash escapes");
    opts.optflagmulti("d", "", "unsigned decimal 2-byte units");
    opts.optflagmulti("D", "", "unsigned decimal 4-byte units");
    opts.optflagmulti("o", "", "octal 2-byte units");

    opts.optflagmulti("I", "", "decimal 8-byte units");
    opts.optflagmulti("L", "", "decimal 8-byte units");
    opts.optflagmulti("i", "", "decimal 4-byte units");
    opts.optflagmulti("l", "", "decimal 8-byte units");
    opts.optflagmulti("x", "", "hexadecimal 2-byte units");
    opts.optflagmulti("h", "", "hexadecimal 2-byte units");

    opts.optflagmulti("O", "", "octal 4-byte units");
    opts.optflagmulti("s", "", "decimal 2-byte units");
    opts.optflagmulti("X", "", "hexadecimal 4-byte units");
    opts.optflagmulti("H", "", "hexadecimal 4-byte units");

    opts.optflagmulti("e", "", "floating point double precision (64-bit) units");
    opts.optflagmulti("f", "", "floating point single precision (32-bit) units");
    opts.optflagmulti("F", "", "floating point double precision (64-bit) units");

    opts.optmulti("t", "format", "select output format or formats", "TYPE");
    opts.optflag("v", "output-duplicates", "do not use * to mark line suppression");
    opts.optflagopt("w", "width",
                ("output BYTES bytes per output line. 32 is implied when BYTES is not \
                 specified."),
                "BYTES");
    opts.optflag("h", "help", "display this help and exit.");
    opts.optflag("", "version", "output version information and exit.");
    opts.optflag("", "traditional", "compatibility mode with one input, offset and label.");

    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            disp_err!("{}", f);
            return 1;
        }
    };

    if matches.opt_present("h") {
        println!("{}", opts.usage(&USAGE));
        return 0;
    }
    if matches.opt_present("version") {
        println!("{} {}", executable!(), VERSION);
        return 0;
    }

    let byte_order = match matches.opt_str("endian").as_ref().map(String::as_ref) {
        None => { ByteOrder::Native },
        Some("little") => { ByteOrder::Little },
        Some("big") => { ByteOrder::Big },
        Some(s) => {
            disp_err!("Invalid argument --endian={}", s);
            return 1;
        }
    };

    let mut skip_bytes = match matches.opt_default("skip-bytes", "0") {
        None => 0,
        Some(s) => {
            match parse_number_of_bytes(&s) {
                Ok(i) => { i }
                Err(_) => {
                    disp_err!("Invalid argument --skip-bytes={}", s);
                    return 1;
                }
            }
        }
    };

    let mut label: Option<usize> = None;

    let input_strings = match parse_inputs(&matches) {
        Ok(CommandLineInputs::FileNames(v)) => v,
        Ok(CommandLineInputs::FileAndOffset((f, s, l))) => {
            skip_bytes = s;
            label = l;
            vec!{f}
        },
        Err(e) => {
            disp_err!("Invalid inputs: {}", e);
            return 1;
        }
    };

    let formats = match parse_format_flags(&args) {
        Ok(f) => f,
        Err(e) => {
            disp_err!("{}", e);
            return 1;
        }
    };

    let mut line_bytes = match matches.opt_default("w", "32") {
        None => 16,
        Some(s) => {
            match s.parse::<usize>() {
                Ok(i) => { i }
                Err(_) => { 2 }
            }
        }
    };
    let min_bytes = formats.iter().fold(1, |max, next| cmp::max(max, next.formatter_item_info.byte_size));
    if line_bytes % min_bytes != 0 {
        show_warning!("invalid width {}; using {} instead", line_bytes, min_bytes);
        line_bytes = min_bytes;
    }

    let output_duplicates = matches.opt_present("v");

    let read_bytes = match matches.opt_str("read-bytes") {
        None => None,
        Some(s) => {
            match  parse_number_of_bytes(&s) {
                Ok(i) => { Some(i) }
                Err(_) => {
                    disp_err!("Invalid argument --read-bytes={}", s);
                    return 1;
                }
            }
        }
    };

    let mut input = open_input_peek_reader(&input_strings, skip_bytes, read_bytes);

    let mut input_decoder = InputDecoder::new(&mut input, line_bytes, PEEK_BUFFER_SIZE, byte_order);

    let mut input_offset = InputOffset::new(Radix::Octal, skip_bytes, label);
    if let Err(e) = input_offset.parse_radix_from_commandline(matches.opt_str("A")) {
        disp_err!("Invalid -A/--address-radix\n{}", e);
        return 1;
    }

    odfunc(&mut input_decoder, &mut input_offset, line_bytes, &formats[..],
            output_duplicates)
}

// TODO: refactor, too many arguments
fn odfunc<I>(input_decoder: &mut InputDecoder<I>, input_offset: &mut InputOffset, line_bytes: usize,
        formats: &[ParsedFormatterItemInfo], output_duplicates: bool) -> i32
        where I : PeekRead+HasError {

    let mut duplicate_line = false;
    let mut previous_bytes: Vec<u8> = Vec::new();

    let byte_size_block = formats.iter().fold(1, |max, next| cmp::max(max, next.formatter_item_info.byte_size));
    let print_width_block = formats
        .iter()
        .fold(1, |max, next| {
            cmp::max(max, next.formatter_item_info.print_width * (byte_size_block / next.formatter_item_info.byte_size))
        });
    let print_width_line = print_width_block * (line_bytes / byte_size_block);

    if byte_size_block > MAX_BYTES_PER_UNIT {
        panic!("{}-bits types are unsupported. Current max={}-bits.",
                8 * byte_size_block,
                8 * MAX_BYTES_PER_UNIT);
    }

    let mut spaced_formatters: Vec<SpacedFormatterItemInfo> = formats
        .iter()
        .map(|f| SpacedFormatterItemInfo { frm: *f, spacing: [0; MAX_BYTES_PER_UNIT] })
        .collect();

    // calculate proper alignment for each item
    for sf in &mut spaced_formatters {
        let mut byte_size = sf.frm.formatter_item_info.byte_size;
        let mut items_in_block = byte_size_block / byte_size;
        let thisblock_width = sf.frm.formatter_item_info.print_width * items_in_block;
        let mut missing_spacing = print_width_block - thisblock_width;

        while items_in_block > 0 {
            let avg_spacing: usize = missing_spacing / items_in_block;
            for i in 0..items_in_block {
                sf.spacing[i * byte_size] += avg_spacing;
                missing_spacing -= avg_spacing;
            }
            // this assumes the size of all types is a power of 2 (1, 2, 4, 8, 16, ...)
            items_in_block /= 2;
            byte_size *= 2;
        }
    }

    loop {
        // print each line data (or multi-format raster of several lines describing the same data).

        match input_decoder.peek_read() {
            Ok(mut memory_decoder) => {
                let length=memory_decoder.length();

                if length == 0 {
                    input_offset.print_final_offset();
                    break;
                }

                // not enough byte for a whole element, this should only happen on the last line.
                if length != line_bytes {
                    // set zero bytes in the part of the buffer that will be used, but is not filled.
                    let mut max_used = length + MAX_BYTES_PER_UNIT;
                    if max_used > line_bytes {
                        max_used = line_bytes;
                    }

                    memory_decoder.zero_out_buffer(length, max_used);
                }

                if !output_duplicates
                        && length == line_bytes
                        && memory_decoder.get_buffer(0) == &previous_bytes[..] {
                    if !duplicate_line {
                        duplicate_line = true;
                        println!("*");
                    }
                }
                else {
                    duplicate_line = false;
                    if length == line_bytes {
                        // save a copy of the input unless it is the last line
                        memory_decoder.clone_buffer(&mut previous_bytes);
                    }

                    print_bytes(&input_offset.format_byte_offset(), &memory_decoder,
                        &spaced_formatters, byte_size_block, print_width_line);
                }

                input_offset.increase_position(length);
            }
            Err(e) => {
                show_error!("{}", e);
                input_offset.print_final_offset();
                return 1;
            }
        };
    }

    if input_decoder.has_error() {
        1
    } else {
        0
    }
}

fn print_bytes(prefix: &str, input_decoder: &MemoryDecoder,
        formats: &[SpacedFormatterItemInfo], byte_size_block: usize, print_width_line: usize) {
    let mut first = true; // First line of a multi-format raster.
    for f in formats {
        let mut output_text = String::new();

        let mut b = 0;
        while b < input_decoder.length() {
            output_text.push_str(&format!("{:>width$}",
                    "",
                    width = f.spacing[b % byte_size_block]));

            match f.frm.formatter_item_info.formatter {
                FormatWriter::IntWriter(func) => {
                    let p = input_decoder.read_uint(b, f.frm.formatter_item_info.byte_size);
                    output_text.push_str(&func(p));
                }
                FormatWriter::FloatWriter(func) => {
                    let p = input_decoder.read_float(b, f.frm.formatter_item_info.byte_size);
                    output_text.push_str(&func(p));
                }
                FormatWriter::MultibyteWriter(func) => {
                    output_text.push_str(&func(input_decoder.get_full_buffer(b)));
                }
            }

            b += f.frm.formatter_item_info.byte_size;
        }

        if f.frm.add_ascii_dump {
            let missing_spacing = print_width_line.saturating_sub(output_text.chars().count());
            output_text.push_str(&format!("{:>width$}  {}",
                    "",
                    format_ascii_dump(input_decoder.get_buffer(0)),
                    width=missing_spacing));
        }

        if first {
            print!("{}", prefix); // print offset
            // if printing in multiple formats offset is printed only once
            first = false;
        }
        else {
            // this takes the space of the file offset on subsequent
            // lines of multi-format rasters.
            print!("{:>width$}", "", width=prefix.chars().count());
        }
        print!("{}\n", output_text);
    }
}

/// returns a reader implementing `PeekRead+Read+HasError` providing the combined input
///
/// `skip_bytes` is the number of bytes skipped from the input
/// `read_bytes` is an optinal limit to the number of bytes to read
fn open_input_peek_reader<'a>(input_strings: &'a Vec<String>, skip_bytes: usize,
        read_bytes: Option<usize>) -> PeekReader<PartialReader<MultifileReader<'a>>> {
    // should return  "impl PeekRead+Read+HasError" when supported in (stable) rust
    let inputs = input_strings
        .iter()
        .map(|w| match w as &str {
            "-" => InputSource::Stdin,
            x => InputSource::FileName(x),
        })
        .collect::<Vec<_>>();

    let mf = MultifileReader::new(inputs);
    let pr = PartialReader::new(mf, skip_bytes, read_bytes);
    let input = PeekReader::new(pr);
    input
}

struct SpacedFormatterItemInfo {
    frm: ParsedFormatterItemInfo,
    spacing: [usize; MAX_BYTES_PER_UNIT],
}