use tool_helper::{Input, Output};
use std::io::Read;
pub use tool_helper::{Error, format_if_error};

const EMPTY_CONTENT_ERROR_STRING:&'static str = "Content is not allowed to be empty";

pub enum LineFeed {
    Unix,
    Windows
}

pub enum FileType {
    CHeader,
    CSource,
    CPPHeader,
    CPPSource,
}

pub struct Configuration {
    pub file_name:     String,
    pub data_name:     String,
    pub line_feed:     LineFeed,
    pub file_type:     FileType,
}

struct FileDeclarations<'a> {
    include:  &'a str,
    var_type: &'a str,
}

const UNIX_LINEFEED:    &'static str = "\n";
const WINDOWS_LINEFEED: &'static str = "\r\n";

const   C_DECLARATIONS: FileDeclarations = FileDeclarations{include: "", var_type: "char"};
const CPP_DECLARATIONS: FileDeclarations = FileDeclarations{include: "#include <stdint.hpp>", var_type: "uint8_t"};

fn output_bytes(input: Input, output: &mut Output, line_feed: &str) -> Result<usize, std::io::Error> {
    let mut byte_line_count = 0;
    let mut bytes_written   = 0;

    write!(output, "\t")?;
    for byte in input.bytes() {
        if byte_line_count > 0 {
            write!(output, ", ")?;
        }

        write!(output, "{:#04X}", byte?)?;
        byte_line_count += 1;
        bytes_written   += 1;

        if byte_line_count >= 16 {
            write!(output, ",{}\t", line_feed)?;
            byte_line_count = 0;
        }
    }
    
    Ok(bytes_written)
}

fn write_header_file(input: Input, output: &mut Output, file_name: String, declarations: &FileDeclarations, data_name: String, line_feed: &str) -> Result<(), Error> {
    let file_name = file_name.to_uppercase().replace('.', "_");

    write!(output, "#ifndef __{}__{}", file_name, line_feed)?;
    write!(output, "#define __{}__{}", file_name, line_feed)?;
    write!(output, "{}{}{}", declarations.include, line_feed, line_feed)?;
    write!(output, "static const {} {}[] = {{{}", declarations.var_type, data_name, line_feed)?;

    if output_bytes(input, output, line_feed)? == 0 {
        return Err(Error::from_str(EMPTY_CONTENT_ERROR_STRING));
    }

    write!(output, "{}}};{}", line_feed, line_feed)?;
    write!(output, "#endif // !__{}__{}", file_name, line_feed)?;
    Ok(())
}

fn write_source_file(input: Input, output: &mut Output, declarations: &FileDeclarations, data_name: String, line_feed: &str) -> Result<(), Error> {
    write!(output, "{}{}{}", declarations.include, line_feed, line_feed)?;
    write!(output, "const {} {}[] = {{{}", declarations.var_type, data_name, line_feed)?;

    if output_bytes(input, output, line_feed)? == 0 {
        return Err(Error::from_str(EMPTY_CONTENT_ERROR_STRING));
    }

    write!(output, "{}}};", line_feed)?;
    Ok(())
}

pub fn convert(cfg: Configuration, input: Input, mut output: Output) -> Result<(), Error> {
    let line_feed = {
        match cfg.line_feed {
            LineFeed::Windows => WINDOWS_LINEFEED,
            LineFeed::Unix    => UNIX_LINEFEED
        }
    };
    let (declarations, is_source_file) = {
        match cfg.file_type {
            FileType::CHeader   => (&C_DECLARATIONS,   false),
            FileType::CSource   => (&C_DECLARATIONS,   true),
            FileType::CPPHeader => (&CPP_DECLARATIONS, false),
            FileType::CPPSource => (&CPP_DECLARATIONS, true),
        }
    };

    if is_source_file {
        format_if_error!(write_source_file(input, &mut output, declarations, cfg.data_name, line_feed), "Writing source file failed with: \"{error_text}\"")
    }

    else {
        format_if_error!(write_header_file(input, &mut output, cfg.file_name, declarations, cfg.data_name, line_feed), "Writing header file failed with: \"{error_text}\"")
    }
}