//! This module defines core operations and config of the tabular data view
//! It operates in 2 coordinate systems:
//! - `DataCellId` - indices of src data cells
//! - `DisplayCellId` - indices of data after applied transformations like sorting/filtering, which is used to render cell on the screen
//!
//! It's designed to contain core logic of operations without relying on `TabularDataPreviewPane`, context or window handles.

use std::{
    collections::{HashMap, HashSet},
    sync::Arc,
};

use ui::table_row::TableRow;

use crate::{
    table_data_engine::{
        filtering_by_column::{FilterEntry, FilterStack, calculate_available_filters, retain_rows},
        sorting_by_column::{AppliedSorting, sort_data_rows},
    },
    types::{AnyColumn, DataRow, DisplayRow, TableCell, TableLikeContent},
};

pub mod filtering_by_column;
pub mod sorting_by_column;

#[derive(Default)]
pub(crate) struct TableDataEngine {
    pub filter_stack: FilterStack,
    /// Pre-computed unique values per column, used to populate filter menus
    all_filters: HashMap<AnyColumn, Vec<FilterEntry>>,
    pub applied_sorting: Option<AppliedSorting>,
    d2d_mapping: DisplayToDataMapping,
    pub contents: Arc<TableLikeContent>,
}

impl TableDataEngine {
    pub(crate) fn d2d_mapping(&self) -> &DisplayToDataMapping {
        &self.d2d_mapping
    }

    pub(crate) fn set_d2d_mapping(&mut self, mapping: DisplayToDataMapping) {
        self.d2d_mapping = mapping;
    }

    /// Recomputes the unique filter entries for every column from the current table data.
    /// Must be called after content changes (e.g. after parsing).
    pub fn calculate_available_filters(&mut self) {
        self.all_filters =
            calculate_available_filters(&self.contents.rows, self.contents.number_of_cols);
    }
}

/// Relation of Display (rendered) rows to Data (src) rows with applied transformations
/// Transformations applied:
/// - sorting by column
/// - filtering by column values
#[derive(Debug, Default)]
pub struct DisplayToDataMapping {
    /// All rows sorted, regardless of applied filtering. Recomputed every time sorting changes
    pub sorted_rows: Vec<DataRow>,
    /// Rows that survive the active filters. Recomputed every time filters change
    pub retained_rows: HashSet<DataRow>,
    /// Merged result: sorted rows intersected with retained rows
    pub mapping: Arc<HashMap<DisplayRow, DataRow>>,
}

impl DisplayToDataMapping {
    /// Computes the full display-to-data mapping from owned inputs.
    /// Intended to be called from a background thread.
    pub(crate) fn compute(
        contents: &Arc<TableLikeContent>,
        filter_stack: &FilterStack,
        sorting: Option<AppliedSorting>,
    ) -> Self {
        let mut mapping = Self::default();
        mapping.apply_sorting(sorting, &contents.rows);
        mapping.apply_filtering(filter_stack, &contents.rows);
        mapping.merge_mappings();
        mapping
    }

    /// Get the data row for a given display row
    pub fn get_data_row(&self, display_row: DisplayRow) -> Option<DataRow> {
        self.mapping.get(&display_row).copied()
    }

    /// Get the number of filtered rows
    pub fn visible_row_count(&self) -> usize {
        self.mapping.len()
    }

    /// Computes sorting
    fn apply_sorting(&mut self, sorting: Option<AppliedSorting>, rows: &[TableRow<TableCell>]) {
        let data_rows: Vec<DataRow> = (0..rows.len()).map(DataRow).collect();

        let sorted_rows = if let Some(sorting) = sorting {
            sort_data_rows(&rows, data_rows, sorting)
        } else {
            data_rows
        };

        self.sorted_rows = sorted_rows;
    }

    fn apply_filtering(&mut self, filter_stack: &FilterStack, rows: &[TableRow<TableCell>]) {
        self.retained_rows = retain_rows(rows, filter_stack, None);
    }

    /// Merges pre-computed sorting and filtering into the final display mapping
    fn merge_mappings(&mut self) {
        self.mapping = Arc::new(
            self.sorted_rows
                .iter()
                .filter(|data_row| self.retained_rows.contains(data_row))
                .enumerate()
                .map(|(display, data)| (DisplayRow(display), *data))
                .collect(),
        );
    }
}
