use std::cell::RefCell;
use std::io::{BufWriter, Write, stdout};
use std::path::Path;
use umya_spreadsheet::{Cell, Range, Spreadsheet, Worksheet, reader};

use crate::arguments::{Arguments, IntoArgs, NumberRows, TrimSpaces};
use crate::error::Error;

/// A reader for .xlsx files with extended options.
#[derive(Debug, Clone)]
pub struct XlsxReader {
    pub(crate) args: Arguments,
    pub(crate) book: Option<Spreadsheet>,
    pub(crate) sheet_index: Option<usize>,
    pub(crate) worksheet_dimensions: RefCell<Option<(u32, u32)>>,
    pub(crate) current_row: u32,
}

impl XlsxReader {
    /// Create an XlsxReader from a path or clap arguments
    pub fn new(args: impl IntoArgs) -> Self {
        let args = args.into_args();

        XlsxReader {
            args,
            book: None,
            sheet_index: None,
            worksheet_dimensions: RefCell::new(None),
            current_row: 0u32,
        }
    }

    pub fn list_worksheets(&self) -> Result<Vec<(usize, String)>, Error> {
        let book = match &self.book {
            Some(book) => book,
            None => return Err("Call finish before list_worksheets.".into()),
        };
        let sheets = book.get_sheet_collection();
        let mut res = vec![];

        for (i, sheet) in sheets.iter().enumerate() {
            res.push((i, String::from(sheet.get_name())));
        }
        Ok(res)
    }

    pub(crate) fn finish(&mut self) -> Result<(), Error> {
        let book = reader::xlsx::read(Path::new(self.args.file.as_str()))?;

        if self.args.active_worksheet && self.args.worksheet == "0" {
            let sheetname = book.get_active_sheet().get_name();
            self.args.worksheet = String::from(sheetname);
        }
        let sheet_index = Self::get_worksheet_index(&book, self.args.worksheet.as_str())?;

        self.book = Some(book);
        self.sheet_index = Some(sheet_index);

        Ok(())
    }

    pub(crate) fn get_worksheet_index(
        book: &Spreadsheet,
        worksheet_name: &str,
    ) -> Result<usize, Error> {
        let sheet_index = if book.get_sheet_by_name(worksheet_name).is_some() {
            // Le nom existe - trouve son index
            book.get_sheet_collection()
                .iter()
                .position(|s| s.get_name() == worksheet_name)
                .ok_or("cannot find sheet index")?
        } else if let Ok(num) = worksheet_name.parse::<usize>() {
            // Pas de nom correspondant, essaye comme numéro
            if book.get_sheet(&num).is_none() {
                return Err("cannot open sheet".into());
            }
            num
        } else {
            return Err("cannot open sheet".into());
        };
        Ok(sheet_index)
    }

    pub(crate) fn get_sheet(&self) -> Result<&Worksheet, Error> {
        let book = match &self.book {
            Some(book) => book,
            None => return Err("Call finish before get_sheet.".into()),
        };
        Ok(book.get_sheet(&self.sheet_index.unwrap()).unwrap())
    }

    pub(crate) fn get_worksheet_dimensions(&self) -> (u32, u32) {
        if self.worksheet_dimensions.borrow().is_none() {
            let mut num_cols = 0;
            let mut num_rows = 0;
            let sheet = self.get_sheet().unwrap();
            for cell in sheet.get_cell_collection() {
                let value = get_value(cell); //.get_formatted_value();

                if value.is_empty() {
                    continue;
                }

                let coord = cell.get_coordinate();
                let col_num = *coord.get_col_num();
                let row_num = *coord.get_row_num();
                if col_num > num_cols {
                    num_cols = col_num;
                }
                if row_num > num_rows {
                    num_rows = row_num;
                }
            }
            let mut dim = self.worksheet_dimensions.borrow_mut();
            *dim = Some((num_cols, num_rows));
        }
        let (c, r) = self.worksheet_dimensions.borrow().unwrap();
        (c, r)
    }

    pub(crate) fn get_row(&self, row_num: u32) -> Vec<String> {
        let num_cols = self.get_worksheet_dimensions().0 as usize;
        let sheet = self.get_sheet().unwrap();
        let row = sheet.get_collection_by_row(&row_num);

        let mut res = vec![String::new(); num_cols];

        for cell in row {
            let value = cell.get_formatted_value();
            let coord = cell.get_coordinate();
            let col = *coord.get_col_num() - 1;
            if col < num_cols.try_into().unwrap() {
                res[col as usize] = value;
            }
        }

        res
    }
}

impl Iterator for XlsxReader {
    type Item = Result<String, Error>;

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

pub fn list_worksheets(book: Spreadsheet) -> Result<Vec<(usize, String)>, Error> {
    let sheets = book.get_sheet_collection();
    let mut res = vec![];
    for (i, sheet) in sheets.iter().enumerate() {
        res.push((i, String::from(sheet.get_name())));
    }
    Ok(res)
}

pub fn xlsxtocsv(args: &Arguments) -> Result<(), Error> {
    let book = reader::xlsx::read(Path::new(&args.file))?;

    if args.list_worksheets {
        println!("List of worksheets :");

        return Ok(());
    }

    let sheet = match book.get_sheet_by_name(&args.worksheet) {
        Some(sheet) => sheet,
        None => {
            let sheetnum: u32 = match args.worksheet.parse() {
                Ok(sheetnum) => sheetnum,
                Err(_) => return Err("cannot open sheet".into()),
            };
            match book.get_sheet(&(sheetnum as usize)) {
                Some(sheet) => sheet,
                None => return Err("cannot open sheet".into()),
            }
        }
    };

    // get all the merged cells
    let merged_cells = MergedCells::new(
        sheet,
        args.fill_merged_cells_horizontal,
        args.fill_merged_cells_vertical,
    );

    // get non-empty value size of the worksheet
    let mut num_cols = 0;
    let mut num_rows = 0;

    for cell in sheet.get_cell_collection() {
        let value = get_value(cell); //.get_formatted_value();

        if value.is_empty() {
            continue;
        }

        let coord = cell.get_coordinate();
        let col_num = *coord.get_col_num();
        let row_num = *coord.get_row_num();
        if col_num > num_cols {
            num_cols = col_num;
        }
        if row_num > num_rows {
            num_rows = row_num;
        }
    }
    let num_cols = num_cols;
    let num_rows = num_rows;

    /*
    get hidden columns if needed
    */
    let mut hidden_columns: Vec<u32> = Vec::new();
    if !args.include_hidden_columns {
        for i in 1..=num_cols {
            if let Some(dim) = sheet.get_column_dimension_by_number(&i)
                && *dim.get_hidden()
            {
                hidden_columns.push(i);
            }
        }
    }

    // create the empty row string
    let mut empty_row = String::from("");
    for _ in 1..num_cols - (hidden_columns.len() as u32) {
        empty_row.push(args.separator);
    }
    empty_row += args.end_of_line.as_str();

    if args.skip_rows > num_rows {
        return Err("Number of rows < number of rows to skip".into());
    }

    let stdout = stdout();
    let mut writer = BufWriter::new(stdout.lock());

    // for each row...
    let mut seq_row_num = 0;
    for i in (args.skip_rows + 1)..=num_rows {
        let mut line = String::from("");

        // take hidden rows if asked for
        if !args.include_hidden_rows {
            match sheet.get_row_dimension(&i) {
                Some(dim) => {
                    if *dim.get_hidden() {
                        continue;
                    }
                }
                None => {
                    seq_row_num += 1;
                    line += number_row(&args.number_rows, args.separator, seq_row_num, i).as_str();
                    line += empty_row.as_str();
                    writer.write_all(line.as_bytes()).unwrap();
                    continue;
                }
            }
        }

        // number the row
        seq_row_num += 1;
        line += number_row(&args.number_rows, args.separator, seq_row_num, i).as_str();

        // for each column in row...
        let mut first = true;
        for j in 1..=num_cols {
            // if the column j has to be hidden, go to the next
            if hidden_columns.contains(&j) {
                continue;
            }
            if first {
                first = false;
            } else {
                line.push(args.separator);
            }

            let cell = match sheet.get_cell((j, i)) {
                Some(cell) => cell,
                None => continue,
            };

            // get value from cell depending on merged cells and fill merged policy
            let cell_coordinate = cell.get_coordinate();
            let mut value;
            if let Some((col, row)) = merged_cells.in_merged_cell(
                *cell_coordinate.get_col_num(),
                *cell_coordinate.get_row_num(),
            ) {
                value = match sheet.get_cell((col, row)) {
                    Some(merged_cell) => get_value(merged_cell), //.get_formatted_value(),
                    None => String::from(""),
                }
            } else {
                value = get_value(cell); //.get_formatted_value();
            }

            // apply modifications to cells value (trim spaces, replace separator chars, line breaks etc.)
            value = match args.trim {
                TrimSpaces::End => String::from(value.trim_end()),
                TrimSpaces::Start => String::from(value.trim_start()),
                TrimSpaces::Both => String::from(value.trim()),
                TrimSpaces::None => value,
            };
            value = value.replace('\r', "").replace('\n', " ");
            if let Some(ref replacement) = args.replace_separator_by {
                value = value.replace(args.separator, replacement);
            } else if value.contains(args.separator) {
                return Err(format!(
                    "Cell {} contains separator char, use -r to choose a replacement char",
                    cell.get_coordinate().get_coordinate()
                )
                .into());
            }
            if let Some(ref replacement) = args.replace_end_of_line_by {
                value = value.replace(&args.end_of_line, replacement);
            } else if value.contains(&args.end_of_line) {
                return Err(format!(
                    "Cell {} contains end of line string, use -R to choose a replacement string",
                    cell.get_coordinate().get_coordinate()
                )
                .into());
            }
            line += value.as_str();
        }
        line += args.end_of_line.as_str();
        writer.write_all(line.as_bytes()).unwrap();
    }

    Ok(())
}

fn number_row(number_row: &NumberRows, separator: char, seqrownum: u32, i: u32) -> String {
    match number_row {
        NumberRows::AsIs => format!("{}{}", i, separator),
        NumberRows::Sequential => format!("{}{}", seqrownum, separator),
        NumberRows::None => String::from(""),
    }
}

fn get_value(cell: &Cell) -> String {
    //cell.get_formatted_value()
    match cell.get_raw_value() {
        umya_spreadsheet::CellRawValue::String(val) => String::from(val.clone()),
        umya_spreadsheet::CellRawValue::RichText(text) => (*text.get_text()).to_owned(),
        umya_spreadsheet::CellRawValue::Lazy(lazy) => (*lazy.clone()).to_owned(),
        umya_spreadsheet::CellRawValue::Numeric(num) => format!("{}", num),
        umya_spreadsheet::CellRawValue::Bool(bo) => format!("{}", bo),
        umya_spreadsheet::CellRawValue::Error(_) => String::from(""),
        umya_spreadsheet::CellRawValue::Empty => String::from(""),
    }
}

struct MergedCells {
    merged_cells: Vec<Range>,
    fill_horizontal: bool,
    fill_vertical: bool,
}

impl MergedCells {
    pub fn new(sheet: &Worksheet, fill_horizontal: bool, fill_vertical: bool) -> Self {
        let merged = sheet.get_merge_cells();
        let mut merged_cells: Vec<Range> = vec![];

        for cell in merged {
            merged_cells.push(cell.clone());
        }

        MergedCells {
            merged_cells,
            fill_horizontal,
            fill_vertical,
        }
    }

    pub fn in_merged_cell(&self, col: u32, row: u32) -> Option<(u32, u32)> {
        for range in &self.merged_cells {
            if col >= *range.get_coordinate_start_col().unwrap().get_num()
                && col <= *range.get_coordinate_end_col().unwrap().get_num()
                && row >= *range.get_coordinate_start_row().unwrap().get_num()
                && row <= *range.get_coordinate_end_row().unwrap().get_num()
            {
                let col_start = *range.get_coordinate_start_col().unwrap().get_num();
                let row_start = *range.get_coordinate_start_row().unwrap().get_num();

                if (self.fill_horizontal || col == col_start)
                    && (self.fill_vertical || row == row_start)
                {
                    return Some((col_start, row_start));
                }
            }
        }
        None
    }
}
