use std::mem;

use vfile::util::buf::previewer::BufPreviewer;
use vfile::util::buf::reader::BufReader;

use crate::chopper::error::Result;
use crate::chopper::types::{FieldType, FieldValue, Header, Row};
use crate::cli::util::YesNoAuto;
use crate::source::csv_input_config::CSVInputConfig;
use crate::source::csv_timestamp_util::{self, TimestampCol, TimestampFmt};
use crate::source::source::Source;
use crate::util::csv::parser::CSVParser;
use crate::util::csv::reader::CSVReader;
use crate::util::csv::util::{guess_delimiter, guess_has_header};
use crate::util::tz::ChopperTz;

const DELIMITERS: &[u8] = b",\t ";

pub struct CSVSource {
    reader: CSVReader<BufReader>,
    header: Header,
    hide_timestamp_column: bool,
    timestamp_col: TimestampCol,
    timestamp_fmt: TimestampFmt,
    timezone: ChopperTz,
    next_row: Box<Row>,
    has_next_row: bool,
}

impl CSVSource {
    pub fn new(previewer: BufPreviewer, csv_input_config: &CSVInputConfig) -> Result<Self> {
        let (line1, line2) = match previewer.lines() {
            None => (None, None),
            Some(lines) => (lines.get(0), lines.get(1)),
        };

        let delimiter = match csv_input_config.delimiter {
            None => {
                match line1 {
                    Some(line) => guess_delimiter(line.as_str(), DELIMITERS),
                    None => DELIMITERS[0], // doesn't really matter, since file is empty, just give something back
                }
            }
            Some(d) => d,
        };

        let has_header = match csv_input_config.has_header {
            YesNoAuto::Yes => true,
            YesNoAuto::No => false,
            YesNoAuto::Auto => guess_has_header(line1, line2, delimiter),
        };

        let reader = previewer.into_reader();

        let parser = CSVParser::new(delimiter, false);
        let mut reader = CSVReader::with_parser(reader, parser);

        // get first row
        let first_row = match reader.read_line()? {
            None => Vec::new(),
            Some(line) => line,
        };
        let field_count = first_row.len();

        if csv_input_config.merge_extra_column_data {
            reader.set_max_columns(field_count);
        }
        if csv_input_config.fill_missing_columns_with_empty {
            reader.set_min_columns(field_count);
        }

        let (first_row, field_names) = if has_header {
            // get field names and a new first row if the file has a header
            let field_names = first_row;

            let first_row = match reader.read_line()? {
                None => Vec::new(),
                Some(line) => line,
            };

            (first_row, field_names)
        } else {
            // otherwise generate default field names
            (first_row, Header::generate_default_field_names(field_count))
        };

        let field_types: Vec<FieldType> = vec![FieldType::String; field_count];
        let header: Header = Header::new(field_names, field_types);

        let timestamp_config = &csv_input_config.timestamp_config;
        let timezone = timestamp_config.timezone();
        let (timestamp_col, timestamp_fmt) = csv_timestamp_util::get_timestamp_col_and_fmt(
            &header,
            &first_row,
            timestamp_config.timestamp_col(),
            timestamp_config.timestamp_fmt(),
            timezone,
        )?;

        let mut header = header;
        if csv_input_config.hide_timestamp_column {
            match timestamp_col {
                TimestampCol::Index(i) => {
                    header.field_types_mut().remove(i);
                    header.field_names_mut().remove(i);
                }
                TimestampCol::DateTimeIndex(d, t) => {
                    header.field_types_mut().remove(d);
                    header.field_names_mut().remove(d);
                    header.field_types_mut().remove(t);
                    header.field_names_mut().remove(t);
                }
            };
        };
        let header = header;

        let mut csv_reader = CSVSource {
            reader,
            header,
            hide_timestamp_column: csv_input_config.hide_timestamp_column,
            timestamp_col,
            timestamp_fmt,
            timezone: timezone.clone(),
            next_row: Box::new(Row::empty()),
            has_next_row: true,
        };

        // update next_row with first row
        csv_reader.update_row(first_row)?;

        Ok(csv_reader)
    }

    fn update_row(&mut self, next_record: Vec<String>) -> Result<()> {
        if self.hide_timestamp_column {
            match self.timestamp_col {
                TimestampCol::Index(ts) => {
                    for i in 0..next_record.len() {
                        if i == ts {
                            continue;
                        }

                        self.next_row
                            .field_values
                            .push(FieldValue::String(next_record.get(i).unwrap().to_string()));
                    }
                }
                TimestampCol::DateTimeIndex(d, t) => {
                    for i in 0..next_record.len() {
                        if i == d || i == t {
                            continue;
                        }

                        self.next_row
                            .field_values
                            .push(FieldValue::String(next_record.get(i).unwrap().to_string()));
                    }
                }
            };
        } else {
            for i in 0..next_record.len() {
                self.next_row
                    .field_values
                    .push(FieldValue::String(next_record.get(i).unwrap().to_string()));
            }
        };

        self.next_row.timestamp = csv_timestamp_util::get_timestamp(
            &next_record,
            &self.timestamp_col,
            &self.timestamp_fmt,
            &self.timezone,
        )?;
        Ok(())
    }

    fn next_row(&mut self) -> Option<Result<Box<Row>>> {
        if !self.has_next_row {
            return None;
        }

        let current_row = mem::replace(&mut self.next_row, Box::new(Row::empty()));
        if let Err(e) = self.update_next_row() {
            return Some(Err(e));
        }
        Some(Ok(current_row))
    }

    fn update_next_row(&mut self) -> Result<()> {
        match self.reader.read_line()? {
            Some(r) => self.update_row(r)?,
            None => self.has_next_row = false,
        }
        Ok(())
    }
}

impl Source for CSVSource {
    fn header(&self) -> &Header {
        &self.header
    }
}

impl Iterator for CSVSource {
    type Item = Result<Box<Row>>;

    fn next(&mut self) -> Option<Self::Item> {
        self.next_row()
    }
}
