use std::fs::File;
use std::io::{self, BufWriter, IsTerminal, Read, Write};
use std::iter;
use std::path::Path;

use npyz::WriterBuilder;
use pad::PadStr;
use rust_xlsxwriter::Workbook;
use simd_csv::{ByteRecord, StringRecord, ZeroCopyReader};
use unicode_width::UnicodeWidthStr;

use crate::CliResult;
use crate::config::{Config, Delimiter};
use crate::json::{JSONEmptyMode, JSONTypeInferrenceBuffer, OmittableAttributes};
use crate::moonblade::DynamicNumber;
use crate::select::{SelectedColumns, Selection};
use crate::util;
use crate::xml::XMLWriter;

fn is_numeric_column(records: &[Vec<String>], column_index: usize) -> bool {
    let mut all_empty = true;

    records.iter().all(|record| {
        let cell = &record[column_index].trim();

        if !cell.is_empty() {
            all_empty = false;
        }

        cell.is_empty() || cell.parse::<DynamicNumber>().is_ok()
    }) && !all_empty
}

fn column_aligments(columns: usize, records: &[Vec<String>]) -> Vec<Alignment> {
    let mut result = Vec::with_capacity(columns);

    for i in 0..columns {
        result.push(if is_numeric_column(records, i) {
            Alignment::Right
        } else {
            Alignment::Center
        });
    }

    result
}

#[derive(Debug)]
enum Alignment {
    Center,
    Right,
}

impl Alignment {
    fn as_latex(&self) -> &str {
        match self {
            Self::Center => "c",
            Self::Right => "r",
        }
    }
}

static USAGE: &str = "
Convert a CSV file to a variety of data formats.

Usage:
    xan to <format> [options] [<input>]
    xan to --help

Supported formats:
    html    - HTML table
    json    - JSON array or object
    jsonl   - JSON lines (same as `ndjson`)
    latex   - LaTeX table
    md      - Markdown table
    ndjson  - Newline-delimited JSON (same as `jsonl`)
    npy     - Numpy arrayrconf.path.as_ref().unwrap();
    txt     - Text lines
    xlsx    - Excel spreadsheet

Some formats can be streamed, some others require the full CSV file to be loaded into
memory.

Streamable formats are `html`, `jsonl`, `ndjson`, `npy` and `txt`.

JSON options:
    --sample-size <size>  Number of CSV rows to sample to infer column types.
                          Set to -1 to sample whole CSV input.
                          [default: 512]
    --nulls               Convert empty string to a null value.
    --omit                Ignore the empty values.
    --strings <columns>   Force selected columns to be considered as raw strings
                          instead of integers, floats etc.

NPY options:
    --dtype <type>       Number type to use for the npy conversion. Must be one of \"f32\"
                         or \"f64\". [default: f64]
    -s, --select <cols>  Numerical columns of the file to emit.

TXT options:
    -s, --select <col>  Column of file to emit as text. Will error if file
                        to convert to text has multiple columns or if
                        selection yields more than a single column.

Markdown options:
    -l, --limit <n>  Maximum number of rows to emit. If the end of the table is not
                     reached within limit, a dummy row containing ellipsis characters
                     will be written in the end.

LateX options:
    --caption <caption>  Optional name of the caption set in the latex, will be empty if not specified.

Common options:
    -h, --help             Display this message
    -o, --output <file>    Write output to <file> instead of stdout.
    -n, --no-headers       When set, the first row will not be evaled
                           as headers.
    -d, --delimiter <arg>  The field delimiter for reading CSV data.
                           Must be a single character.
";

#[derive(Deserialize)]
struct Args {
    arg_format: String,
    arg_input: Option<String>,
    flag_output: Option<String>,
    flag_no_headers: bool,
    flag_select: SelectedColumns,
    flag_delimiter: Option<Delimiter>,
    flag_sample_size: isize,
    flag_nulls: bool,
    flag_omit: bool,
    flag_strings: Option<SelectedColumns>,
    flag_dtype: String,
    flag_caption: Option<String>,
    flag_limit: Option<usize>,
}

impl Args {
    fn is_writing_to_file(&self) -> bool {
        self.flag_output.is_some() || !io::stdout().is_terminal()
    }

    fn sample_size(&self) -> Option<usize> {
        if self.flag_sample_size <= 0 {
            None
        } else {
            Some(self.flag_sample_size as usize)
        }
    }

    fn limit(&self) -> Option<usize> {
        match self.flag_limit {
            Some(l) if l > 0 => Some(l),
            _ => None,
        }
    }

    fn json_empty_mode(&self) -> JSONEmptyMode {
        if self.flag_nulls {
            JSONEmptyMode::Null
        } else if self.flag_omit {
            JSONEmptyMode::Omit
        } else {
            JSONEmptyMode::Empty
        }
    }

    fn rconf(&self) -> Config {
        Config::new(&self.arg_input)
            .no_headers(self.flag_no_headers)
            .delimiter(self.flag_delimiter)
            .select(self.flag_select.clone())
    }

    fn wconf(&self) -> Config {
        Config::new(&self.flag_output)
    }

    fn convert_to_json(&self) -> CliResult<()> {
        let rconf = self.rconf();
        let mut rdr = rconf.simd_reader()?;
        let mut writer = self.wconf().buf_io_writer()?;

        let headers = rdr.byte_headers()?.clone();

        let mut inferrence_buffer = JSONTypeInferrenceBuffer::with_columns(
            headers.len(),
            self.sample_size(),
            self.json_empty_mode(),
        );

        if let Some(sel) = &self.flag_strings {
            let indices = sel.selection(&headers, !rconf.no_headers)?;

            for index in indices.iter() {
                inferrence_buffer.set_string(*index);
            }
        }

        inferrence_buffer.read(&mut rdr)?;

        let mut json_object =
            OmittableAttributes::from_headers(headers.into_string_record()?.iter());
        let mut json_array = Vec::new();

        for record in inferrence_buffer.records() {
            inferrence_buffer.mutate_attributes(&mut json_object, record);
            json_array.push(json_object.clone());
        }

        let mut record = StringRecord::new();

        while rdr.read_record(&mut record)? {
            inferrence_buffer.mutate_attributes(&mut json_object, &record);
            json_array.push(json_object.clone());
        }

        serde_json::to_writer_pretty(&mut writer, &json_array)?;
        writeln!(&mut writer)?;

        Ok(())
    }

    fn convert_to_ndjson(&self) -> CliResult<()> {
        let rconf = self.rconf();
        let mut rdr = rconf.simd_reader()?;
        let mut writer = self.wconf().buf_io_writer()?;

        let headers = rdr.byte_headers()?.clone();

        let mut inferrence_buffer = JSONTypeInferrenceBuffer::with_columns(
            headers.len(),
            self.sample_size(),
            self.json_empty_mode(),
        );

        if let Some(sel) = &self.flag_strings {
            let indices = sel.selection(&headers, !rconf.no_headers)?;

            for index in indices.iter() {
                inferrence_buffer.set_string(*index);
            }
        }

        inferrence_buffer.read(&mut rdr)?;

        let mut json_object =
            OmittableAttributes::from_headers(headers.into_string_record()?.iter());

        for record in inferrence_buffer.records() {
            inferrence_buffer.mutate_attributes(&mut json_object, record);
            writeln!(writer, "{}", serde_json::to_string(&json_object)?)?;
        }

        let mut record = StringRecord::new();

        while rdr.read_record(&mut record)? {
            inferrence_buffer.mutate_attributes(&mut json_object, &record);
            writeln!(writer, "{}", serde_json::to_string(&json_object)?)?;
        }

        Ok(())
    }

    fn convert_to_xlsx(&self) -> CliResult<()> {
        if !self.is_writing_to_file() {
            Err("cannot export in xlsx without a path.\nUse -o, --output or pipe the result!")?;
        }

        let mut rdr = self.rconf().simd_reader()?;
        let mut writer = self.wconf().io_writer()?;

        let mut workbook = Workbook::new();
        let headers = rdr.byte_headers()?.clone().into_string_record()?;
        let worksheet = workbook.add_worksheet();

        for (col, header) in headers.iter().enumerate() {
            worksheet.write_string(0, col as u16, header)?;
        }

        for (row, value) in rdr.records().enumerate() {
            let record = value?;
            for (col, field) in record.iter().enumerate() {
                worksheet.write_string((row + 1) as u32, col as u16, field)?;
            }
        }

        let mut cursor = io::Cursor::new(Vec::new());
        workbook.save_to_writer(&mut cursor)?;
        let buf = cursor.into_inner();
        writer.write_all(&buf)?;

        writer.flush()?;
        Ok(())
    }

    fn convert_to_html(&self) -> CliResult<()> {
        let rconf = self.rconf();
        let mut rdr = rconf.simd_reader()?;
        let writer = self.wconf().buf_io_writer()?;

        let mut xml_writer = XMLWriter::new(writer);
        let mut record = StringRecord::new();

        xml_writer.open_no_attributes("table")?;

        if !rconf.no_headers {
            xml_writer.open_no_attributes("thead")?;
            xml_writer.open_no_attributes("tr")?;

            for header in rdr.byte_headers()?.clone().into_string_record()?.iter() {
                xml_writer.open_no_attributes("th")?;
                xml_writer.write_text(header)?;
                xml_writer.close("th")?;
            }

            xml_writer.close("tr")?;
            xml_writer.close("thead")?;
        }

        xml_writer.open_no_attributes("tbody")?;

        while rdr.read_record(&mut record)? {
            xml_writer.open_no_attributes("tr")?;

            for cell in record.iter() {
                xml_writer.open_no_attributes("td")?;
                xml_writer.write_text(cell)?;
                xml_writer.close("td")?;
            }

            xml_writer.close("tr")?;
        }

        xml_writer.close("tbody")?;

        xml_writer.close("table")?;
        xml_writer.finish()?;

        Ok(())
    }

    fn convert_to_latex(&self) -> CliResult<()> {
        let rconf = self.rconf();
        let mut rdr = rconf.simd_reader()?;
        let mut writer = self.wconf().buf_io_writer()?;

        fn escape_latex_table_cell(cell: &str) -> String {
            let mut result = String::with_capacity(cell.len());

            for c in cell.chars() {
                match c {
                    '\\' => result.push_str("\\textbackslash{}"),
                    '&' => result.push_str("\\&"),
                    '%' => result.push_str("\\%"),
                    '$' => result.push_str("\\$"),
                    '#' => result.push_str("\\#"),
                    '_' => result.push_str("\\_"),
                    '{' => result.push_str("\\{"),
                    '}' => result.push_str("\\}"),
                    '~' => result.push_str("\\textasciitilde{}"),
                    '^' => result.push_str("\\textasciicircum{}"),
                    '<' => result.push_str("\\textless{}"),
                    '>' => result.push_str("\\textgreater{}"),
                    c => result.push(c),
                }
            }

            result
        }

        let headers = rdr.byte_headers()?.clone().into_string_record()?;
        let records = rdr
            .into_records()
            .map(|result| {
                result.map(|record| {
                    record
                        .into_iter()
                        .map(escape_latex_table_cell)
                        .collect::<Vec<_>>()
                })
            })
            .collect::<Result<Vec<_>, _>>()?;

        let widths = headers
            .iter()
            .enumerate()
            .map(|(i, h)| {
                let header_w = escape_latex_table_cell(h).width();
                let max_record_w = records.iter().map(|r| r[i].width()).max().unwrap_or(0);
                header_w.max(max_record_w)
            })
            .collect::<Vec<_>>();

        let col_spec = column_aligments(headers.len(), &records)
            .iter()
            .map(Alignment::as_latex)
            .collect::<Vec<_>>()
            .join("|");

        writeln!(&mut writer, "\\begin{{table}}[h]")?;
        writeln!(&mut writer, "\\centering")?;
        writeln!(
            &mut writer,
            "\\caption{{{}}}",
            self.flag_caption.as_deref().unwrap_or("")
        )?;
        writeln!(&mut writer, "\\begin{{tabular}}{{|{}|}}", col_spec)?;
        writeln!(&mut writer, "\\hline")?;

        if !rconf.no_headers {
            for (i, (header, _)) in headers.iter().zip(widths.iter()).enumerate() {
                let escaped_h = escape_latex_table_cell(header);
                if i > 0 {
                    write!(&mut writer, " & ")?;
                }
                write!(&mut writer, "\\textbf{{{}}}", escaped_h)?;
            }
            writeln!(&mut writer, " \\\\")?;
            writeln!(&mut writer, "\\hline")?;
        }

        for record in records.into_iter() {
            for (i, (cell, width)) in record.into_iter().zip(widths.iter()).enumerate() {
                if i > 0 {
                    write!(&mut writer, " & ")?;
                }
                write!(&mut writer, "{}", cell.pad_to_width(*width))?;
            }

            writeln!(&mut writer, " \\\\")?;
        }

        writeln!(&mut writer, "\\hline")?;
        writeln!(&mut writer, "\\end{{tabular}}")?;
        writeln!(&mut writer, "\\end{{table}}")?;

        Ok(())
    }

    fn convert_to_md(&self) -> CliResult<()> {
        let rconf = self.rconf();
        let mut rdr = rconf.simd_reader()?;
        let mut writer = self.wconf().buf_io_writer()?;

        fn escape_md_table_cell(cell: &str) -> String {
            cell.replace("|", "\\|")
                .replace("<", "\\<")
                .replace(">", "\\>")
        }

        let headers = rdr.byte_headers()?.clone().into_string_record()?;

        let mut records_iter: Box<dyn Iterator<Item = simd_csv::Result<StringRecord>>> =
            Box::new(rdr.records());

        if let Some(limit) = self.limit() {
            records_iter = Box::new(records_iter.take(limit));
        }

        let mut records = records_iter
            .map(|result| {
                result.map(|record| {
                    record
                        .into_iter()
                        .map(escape_md_table_cell)
                        .collect::<Vec<_>>()
                })
            })
            .collect::<Result<Vec<_>, _>>()?;

        // NOTE: this must be done before pushing ellipsis
        let alignments = column_aligments(headers.len(), &records);

        let everything_was_read = !rdr.read_byte_record(&mut ByteRecord::new())?;

        if !everything_was_read {
            records.push((0..headers.len()).map(|_| "...".to_string()).collect());
        }

        let widths = headers
            .iter()
            .enumerate()
            .map(|(i, h)| {
                iter::once(h.width())
                    .chain(records.iter().map(move |r| r[i].width()))
                    .max()
                    .unwrap()
                    .max(3)
            })
            .collect::<Vec<_>>();

        write!(&mut writer, "|")?;

        for (header, width) in headers.iter().zip(widths.iter()) {
            write!(
                &mut writer,
                " {} |",
                if rconf.no_headers {
                    " ".repeat(*width)
                } else {
                    header.pad_to_width(*width)
                }
            )?;
        }

        writeln!(&mut writer)?;

        write!(&mut writer, "|")?;

        for (width, alignment) in widths.iter().copied().zip(alignments.iter()) {
            match alignment {
                Alignment::Center => {
                    write!(&mut writer, " {} |", "-".repeat(width))?;
                }
                Alignment::Right => {
                    write!(&mut writer, " {}: |", "-".repeat(width.saturating_sub(1)))?;
                }
            }
        }

        writeln!(&mut writer)?;

        for record in records.into_iter() {
            write!(&mut writer, "|")?;

            for (cell, width) in record.into_iter().zip(widths.iter()) {
                write!(&mut writer, " {} |", cell.pad_to_width(*width))?;
            }

            writeln!(&mut writer)?;
        }

        Ok(())
    }

    fn convert_to_npy(&self) -> CliResult<()> {
        if !self.is_writing_to_file() {
            Err("cannot export in npy without a path.\nUse -o, --output or pipe the result!")?;
        }

        fn write_floats<T: npyz::AutoSerialize + fast_float::FastFloat>(
            output_path: impl AsRef<Path>,
            mut rdr: ZeroCopyReader<Box<dyn Read + Send>>,
            sel: &Selection,
        ) -> CliResult<()> {
            let output_file = File::create(output_path)?;
            let io_writer = BufWriter::with_capacity(32 * (1 << 10), output_file);

            let mut writer = npyz::WriteOptions::<T>::new()
                .default_dtype()
                .writer(io_writer)
                .begin_2d(sel.len() as u64)?;

            while let Some(record) = rdr.read_byte_record()? {
                for i in sel.iter().copied() {
                    let cell = record.unquote(i).unwrap();
                    let f = fast_float::parse::<T, &[u8]>(cell)?;
                    writer.push(&f)?;
                }
            }

            writer.finish()?;

            Ok(())
        }

        let rconf = self.rconf();
        let mut rdr = rconf.simd_zero_copy_reader()?;
        let headers = rdr.byte_headers()?;

        let sel = rconf.selection(headers)?;

        let output_path = self.flag_output.as_ref().unwrap();

        match self.flag_dtype.as_str() {
            "float64" | "f64" => write_floats::<f64>(output_path, rdr, &sel),
            "float32" | "f32" => write_floats::<f32>(output_path, rdr, &sel),
            _ => Err(format!("unknown --dtype {}", self.flag_dtype))?,
        }
    }

    fn convert_to_txt(&self) -> CliResult<()> {
        let mut rdr = self.rconf().simd_zero_copy_reader()?;
        let mut writer = self.wconf().buf_io_writer()?;

        let headers = rdr.byte_headers()?.clone();
        let column_index = self
            .flag_select
            .single_selection(&headers, rdr.has_headers()).map_err(|_| {
                "Trying to convert more than a single column to text!\nUse `xan select` upstream or use -s/--select flag to restrict column selection."
            })?;

        while let Some(record) = rdr.read_byte_record()? {
            let cell = record.unescape(column_index).unwrap();
            writer.write_all(&cell)?;
            writer.write_all(b"\n")?;
        }

        Ok(())
    }
}

pub fn run(argv: &[&str]) -> CliResult<()> {
    let args: Args = util::get_args(USAGE, argv)?;

    match args.arg_format.as_str() {
        "html" => args.convert_to_html(),
        "json" => args.convert_to_json(),
        "jsonl" | "ndjson" => args.convert_to_ndjson(),
        "latex" | "tex" => args.convert_to_latex(),
        "md" | "markdown" => args.convert_to_md(),
        "npy" => args.convert_to_npy(),
        "txt" | "text" => args.convert_to_txt(),
        "xlsx" => args.convert_to_xlsx(),
        _ => Err("could not export the file to this format!")?,
    }
}
