use std::collections::HashMap;
use std::io::Write;

use encode_unicode::CharExt;

use crate::chopper::error::{Error, Result};
use crate::chopper::types::{FieldType, FieldValue};

pub fn parse_into_delimiter(str: &str) -> Result<u8> {
    match &*str {
        r"\t" => Ok(b'\t'),
        s => {
            if s.len() != 1 {
                return Err(Error::from(format!(
                    "Error: specified delimiter '{}' is not a single ASCII character.",
                    s
                )));
            }
            let c = s.chars().next().unwrap();
            if c.is_ascii() {
                Ok(c as u8)
            } else {
                Err(Error::from(format!(
                    "Error: specified delimiter '{}' is not an ASCII character.",
                    c
                )))
            }
        }
    }
}

pub fn guess_delimiter(row: &str, possible_delimiters: &[u8]) -> u8 {
    assert_ne!(possible_delimiters.len(), 0);

    let mut counts: HashMap<u8, u32> = HashMap::new();

    for d in possible_delimiters {
        counts.insert(*d, 0);
    }

    for c in row.chars() {
        counts.entry(c as u8).and_modify(|count| *count += 1);
    }

    let mut sorted = possible_delimiters.to_vec();
    sorted.sort_by(|a, b| counts[b].cmp(&counts[a]));

    *sorted.get(0).unwrap()
}

/// we want to be on the conservative side, since we'd rather not silently lose data by
/// skipping the first data row by mistaking it for header;
/// returning false when confused is safest thing - in worst case parsing will fail
/// later and user will have to manually configure the csv file format
pub fn guess_has_header(line1: Option<&String>, line2: Option<&String>, delimiter: u8) -> bool {
    let line1 = match line1 {
        None => return false,
        Some(line) => line,
    };
    let line2 = match line2 {
        None => return false,
        Some(line) => line,
    };

    let line1: Vec<FieldType> = line1
        .split(delimiter as char)
        .map(|s| guess_type(s.trim()))
        .collect();
    let line2: Vec<FieldType> = line2
        .split(delimiter as char)
        .map(|s| guess_type(s.trim()))
        .collect();

    line1 != line2
}

pub fn guess_common_type(field_types: &[FieldType]) -> FieldType {
    if field_types.is_empty() {
        return FieldType::String;
    }

    let mut guess = type_upconvert(field_types[0]);

    for &field_type in field_types {
        if guess == FieldType::String {
            return guess;
        }

        let field_type = type_upconvert(field_type);
        guess = match field_type {
            FieldType::Double => FieldType::Double,
            FieldType::Long => guess, // at this point guess can be only long or double, and we want double to take precedence over long
            _ => return FieldType::String,
        }
    }

    guess
}

fn type_upconvert(field_type: FieldType) -> FieldType {
    match field_type {
        FieldType::Boolean => FieldType::String,
        FieldType::ByteBuf => FieldType::String,
        FieldType::String => FieldType::String,
        FieldType::Byte | FieldType::Char | FieldType::Short | FieldType::Int | FieldType::Long => {
            FieldType::Long
        }
        FieldType::Float | FieldType::Double => FieldType::Double,
        FieldType::MultiDimDoubleArray => FieldType::String,
    }
}

/// current limitations: only long, double, and string; rust format only
pub fn guess_type(str: &str) -> FieldType {
    if str.parse::<i64>().is_ok() {
        return FieldType::Long;
    };
    if str.parse::<f64>().is_ok() {
        return FieldType::Double;
    }
    FieldType::String
}

pub fn write_quoted<W: Write>(writer: &mut W, value: &FieldValue, delimiter: char) -> Result<()> {
    match value {
        FieldValue::Boolean(x) => write!(writer, "{}", if *x { "true" } else { "false" })?,
        FieldValue::Byte(x) => write!(writer, "{}", x)?,
        FieldValue::ByteBuf(x) => write!(writer, "u8[len={}]", x.len())?,
        FieldValue::Char(x) => {
            let c = char::from_utf16_tuple((*x, None))?;
            if c == '"' {
                write!(writer, "\"\"\"\"")?
            } else if delimiter == c {
                write!(writer, "\"{}\"", c)?
            } else {
                write!(writer, "{}", c.escape_default())?
            }
        }
        FieldValue::Double(x) => write!(writer, "{}", dtoa::Buffer::new().format(*x))?,
        FieldValue::Float(x) => write!(writer, "{}", dtoa::Buffer::new().format(*x))?,
        FieldValue::Int(x) => write!(writer, "{}", x)?,
        FieldValue::Long(x) => write!(writer, "{}", x)?,
        FieldValue::Short(x) => write!(writer, "{}", x)?,
        FieldValue::String(x) => {
            let has_quotable = x.contains(&['"', '\r', '\n'][..]);
            if has_quotable || x.contains(delimiter as char) {
                if has_quotable {
                    write!(
                        writer,
                        "\"{}\"",
                        x.replace('"', "\"\"")
                            .replace('\r', "\\r")
                            .replace('\n', "\\n")
                    )?
                } else {
                    write!(writer, "\"{}\"", x)?
                }
            } else {
                write!(writer, "{}", x)?
            }
        }
        FieldValue::MultiDimDoubleArray(x) => {
            let dim_str = x
                .shape()
                .iter()
                .map(|d| d.to_string())
                .collect::<Vec<String>>()
                .join("x");
            write!(writer, "f64[shape=({})]", dim_str)?
        }
        FieldValue::None => (),
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::chopper::types::{FieldType, FieldValue};
    use crate::util::csv::util::{guess_has_header, guess_type, write_quoted};

    #[test]
    fn test_guess_has_header() {
        assert!(guess_has_header(
            Some(&"zzz".to_string()),
            Some(&"123".to_string()),
            b','
        ));
        assert!(!guess_has_header(
            Some(&"zzz".to_string()),
            Some(&"zzz".to_string()),
            b','
        ));
        assert!(!guess_has_header(
            Some(&"123".to_string()),
            Some(&"123".to_string()),
            b','
        ));
        assert!(guess_has_header(
            Some(&"zzz".to_string()),
            Some(&"123".to_string()),
            b','
        ));
        assert!(guess_has_header(
            Some(&"zzz,zzz,zzz,zzz".to_string()),
            Some(&"zzz,zzz,123,zzz".to_string()),
            b','
        ));
        assert!(!guess_has_header(
            Some(&"zzz,zzz,123,zzz".to_string()),
            Some(&"zzz,zzz, 123 ,zzz".to_string()),
            b','
        ));
        assert!(guess_has_header(
            Some(&"123".to_string()),
            Some(&"123.4".to_string()),
            b','
        ));
    }

    #[test]
    fn test_guess_type() {
        assert_eq!(guess_type("zzz"), FieldType::String);
        assert_eq!(guess_type("123"), FieldType::Long);
        assert_eq!(guess_type("123.4"), FieldType::Double);
    }

    #[test]
    fn test_write_quoted() {
        test_write_quoted_string("zzz", "zzz");
        test_write_quoted_string("z,z,z", "\"z,z,z\"");
        test_write_quoted_string("\"zzz", "\"\"\"zzz\"");
        test_write_quoted_string("zz\"z", "\"zz\"\"z\"");
        test_write_quoted_string("z,\"z\",z", "\"z,\"\"z\"\",z\"");
        test_write_quoted_string("zz\nz", "\"zz\\nz\"");
        test_write_quoted_string("\"z\"z\n\"z\"", "\"\"\"z\"\"z\\n\"\"z\"\"\"");

        test_write_quoted_char(b'z', "z");
        test_write_quoted_char(b',', "\",\"");
        test_write_quoted_char(b'\n', "\\n");
        test_write_quoted_char(b'"', "\"\"\"\"");
    }

    fn test_write_quoted_string(value: &str, expected: &str) {
        let mut out = Vec::new();
        write_quoted(&mut out, &FieldValue::String(value.to_string()), ',').unwrap();
        assert_eq!(String::from_utf8(out).unwrap(), expected);
    }

    fn test_write_quoted_char(value: u8, expected: &str) {
        let mut out = Vec::new();
        write_quoted(&mut out, &FieldValue::Char(value as u16), ',').unwrap();
        assert_eq!(String::from_utf8(out).unwrap(), expected);
    }
}
