use crate::{
    expressions::{
        parser::{
            move_formula::ref_is_in_area,
            stringify::{to_string, to_string_displaced, DisplaceData},
            walk::forward_references,
        },
        types::{Area, CellReferenceIndex, CellReferenceRC},
    },
    model::Model,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged, deny_unknown_fields)]
pub enum CellValue {
    Value(String),
    None,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SetCellValue {
    cell: CellReferenceIndex,
    new_value: CellValue,
    old_value: CellValue,
}

impl Model {
    #[allow(clippy::expect_used)]
    pub(crate) fn shift_cell_formula(
        &mut self,
        sheet: u32,
        row: i32,
        column: i32,
        displace_data: &DisplaceData,
    ) {
        if let Some(f) = self
            .workbook
            .worksheet(sheet)
            .expect("Worksheet must exist")
            .cell(row, column)
            .expect("Cell must exist")
            .get_formula()
        {
            let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
            let cell_reference = CellReferenceRC {
                sheet: self.workbook.worksheets[sheet as usize].get_name(),
                row,
                column,
            };
            // FIXME: This is not a very performant way if the formula has changed :S.
            let formula = to_string(node, &cell_reference);
            let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
            if formula != formula_displaced {
                self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))
                    .expect("Failed to shift cell formula");
            }
        }
    }

    #[allow(clippy::expect_used)]
    pub fn forward_references(
        &mut self,
        source_area: &Area,
        target: &CellReferenceIndex,
    ) -> Result<Vec<SetCellValue>, String> {
        let mut diff_list: Vec<SetCellValue> = Vec::new();
        let target_area = &Area {
            sheet: target.sheet,
            row: target.row,
            column: target.column,
            width: source_area.width,
            height: source_area.height,
        };
        // Walk over every formula
        let cells = self.get_all_cells();
        for cell in cells {
            if let Some(f) = self
                .workbook
                .worksheet(cell.index)
                .expect("Worksheet must exist")
                .cell(cell.row, cell.column)
                .expect("Cell must exist")
                .get_formula()
            {
                let sheet = cell.index;
                let row = cell.row;
                let column = cell.column;

                // If cell is in the source or target area, skip
                if ref_is_in_area(sheet, row, column, source_area)
                    || ref_is_in_area(sheet, row, column, target_area)
                {
                    continue;
                }

                // Get the formula
                // Get a copy of the AST
                let node = &mut self.parsed_formulas[sheet as usize][f as usize].clone();
                let cell_reference = CellReferenceRC {
                    sheet: self.workbook.worksheets[sheet as usize].get_name(),
                    column: cell.column,
                    row: cell.row,
                };
                let context = CellReferenceIndex { sheet, column, row };
                let formula = to_string(node, &cell_reference);
                let target_sheet_name = &self.workbook.worksheets[target.sheet as usize].name;
                forward_references(
                    node,
                    &context,
                    source_area,
                    target.sheet,
                    target_sheet_name,
                    target.row,
                    target.column,
                );

                // If the string representation of the formula has changed update the cell
                let updated_formula = to_string(node, &cell_reference);
                if formula != updated_formula {
                    self.update_cell_with_formula(
                        sheet,
                        row,
                        column,
                        format!("={updated_formula}"),
                    )?;
                    // Update the diff list
                    diff_list.push(SetCellValue {
                        cell: CellReferenceIndex { sheet, column, row },
                        new_value: CellValue::Value(format!("={}", updated_formula)),
                        old_value: CellValue::Value(format!("={}", formula)),
                    });
                }
            }
        }
        Ok(diff_list)
    }
}
