import {CellDelta, TabularDiff, TabularDiffs} from 'app/common/TabularDiff';
import toPairs = require('lodash/toPairs');

/**
 * An ActionSummary represents the overall effect of changes that took place
 * during a period of history.
 *   - Only net changes are represented.  Intermediate changes within the period are
 *     not represented.  Changes that are done and undone within the period are not
 *     represented.
 *   - Net addition, removal, and renaming of tables is represented.  The names
 *     of tables, for ActionSummary purposes are their tableIds, the database-safe
 *     version of their names.
 *   - Net addition, removal, and renaming of columns is represented.  As for tables,
 *     the names of columns for ActionSummary purposes are their colIds.
 *   - Net additions and removals of rows are partially represented.  The rowIds of added
 *     and removed rows are represented fully.  The *values* of cells in the rows that
 *     were added or removed are stored in some cases.  There is a threshold on the
 *     number of rows whose values will be cached for each DocAction scanned.
 *   - Net updates of rows are partially represented.  The rowIds of updated rows are
 *     represented fully, but the *values* of updated cells partially, as for additions/
 *     removals.
 *   - Cell value changes affecting _grist_* tables are always represented in full,
 *     even if they are bulk changes.
 *
 * The representation of table name changes and column name changes is the same,
 * simply a list of name pairs [before, after].  We represent the addition of a
 * a table (or column) as the special name pair [null, initialName], and the
 * removal of a table (or column) as the special name pair [finalName, null].
 *
 * An ActionSummary contains two fields:
 *   - tableRenames: a list of table name changes (including addition/removal).
 *   - tableDeltas: a dictionary of changes within a table.
 *
 * The key of the tableDeltas dictionary is the name of a table at the end of the
 * period of history covered by the ActionSummary.
 *   - For example, if we add a table called N, we use the key N for it.
 *   - If we rename a table from N1 to N2, we use the key N2 for it.
 *   - If we add a table called N1, then rename it to N2, we use the key N2 for it.
 * If the table was removed during that period, we use its name at the beginning
 * of the period, preceded by "-".
 *   - If we remove a table called N, we use the key -N for it.
 *   - If we add a table called N, then remove it, there is no net change to represent.
 *   - If we remove a table called N, then add a new table called N, we use the key -N
 *     for the first, and the key N for the second.
 *
 * The changes within a table are represented as a TableDelta, which has the following
 * fields:
 *   - columnRenames: a list of column name changes (including addition/removal).
 *   - columnDeltas: a dictionary of changes within a column.
 *   - updateRows, removeRows, addRows: lists of affected rows.
 *
 * The columnRenames/columnDeltas pair work just like tableRenames/tableDeltas, just
 * on the scope of columns within a table rather than tables within a document.
 *
 * The changes within a column are represented as a ColumnDelta, which is a dictionary
 * keyed by rowIds.  It contains CellDelta values.  CellDelta values represent before
 * and after values of a particular cell.
 *   - a CellDelta of [null, [value]] represents a cell that was non-existent coming into
 *     existence with the given value.
 *   - a CellDelta of [[value], null] represents an existing cell with the given value that
 *     is removed.
 *   - a CellDelta of [[value1], [value2]] represents a change in value of a cell between
 *     two known values.
 *   - a CellDelta of ['?', [value2]] represents a change in value of a cell from an
 *     unknown value to a known value.  Unknown values happen when we know a cell was
 *     implicated in a bulk change but its value didn't happen to be stored.
 *   - a CellDelta of [[value1], '?'] represents a change in value of a cell from an
 *     known value to an unknown value.
 * The CellDelta itself does not tell you whether the rowId has the same identity before
 * and after -- for example it may have been removed and then added.  That information
 * is available by consulting the removeRows and addRows fields.
 *
 */

/**
 * A collection of changes related to a set of tables.
 */
export interface ActionSummary {
  tableRenames: LabelDelta[];  /** a list of table renames/additions/removals */
  tableDeltas: {[tableId: string]: TableDelta};  /** changes within an individual table */
}

/**
 * A collection of changes related to rows and columns of a single table.
 */
export interface TableDelta {
  updateRows: number[];  /** rowIds of rows that exist before+after and were changed during */
  removeRows: number[];  /** rowIds of rows that existed before but were removed during */
  addRows: number[];     /** rowIds of rows that were added during, and exist after */
  /** Partial record of cell-level changes - large bulk changes not included. */
  columnDeltas: {[colId: string]: ColumnDelta};
  columnRenames: LabelDelta[];  /** a list of column renames/additions/removals */
}

/**
 * Pairs of before/after names of tables and columns.  Null represents non-existence,
 * so the addition and removal of tables/columns can be represented.
 */
export type LabelDelta = [string|null, string|null];

/**
 * A collection of changes related to cells in a specific column.
 */
export interface ColumnDelta {
  [rowId: number]: CellDelta;
}


/** Create an ActionSummary for a period with no action */
export function createEmptyActionSummary(): ActionSummary {
  return { tableRenames: [], tableDeltas: {} };
}

/** Create a TableDelta for a period with no action */
export function createEmptyTableDelta(): TableDelta {
  return {
    updateRows: [],
    removeRows: [],
    addRows: [],
    columnDeltas: {},
    columnRenames: []
  };
}


/**
 * Distill a summary further, into tabular form, for ease of rendering.
 */
export function asTabularDiffs(summary: ActionSummary): TabularDiffs {
  const allChanges: TabularDiffs = {};
  for (const [tableId, td] of toPairs(summary.tableDeltas)) {
    const tableChanges: TabularDiff = allChanges[tableId] = {
      header: [],
      cells: [],
    };
    // swap order to row-dominant for visualization purposes
    const perRow: {[row: number]: {[name: string]: any}} = {};
    const activeCols = new Set<string>();
    // iterate through the column-dominant representation grist prefers internally
    for (const [col, perCol] of toPairs(td.columnDeltas)) {
      activeCols.add(col);
      // iterate through the rows for that column, writing out the row-dominant
      // results we want for visualization.
      for (const row of Object.keys(perCol)) {
        if (!perRow[row as any]) { perRow[row as any] = {}; }
        perRow[row as any][col] = perCol[row as any];
      }
    }
    // TODO: recover order of columns; recover row numbers (as opposed to rowIds)
    const activeColsWithoutManualSort = [...activeCols].filter(c => c !== 'manualSort');
    tableChanges.header = activeColsWithoutManualSort;
    const addedRows = new Set(td.addRows);
    const removedRows = new Set(td.removeRows);
    const updatedRows = new Set(td.updateRows);
    const rowIds = Object.keys(perRow).map(row => parseInt(row, 10));
    const presentRows = new Set(rowIds);
    const droppedRows = [...addedRows, ...removedRows, ...updatedRows]
      .filter(x => !presentRows.has(x))
      .sort((a, b) => a - b);

    // Now that we have pulled together rows of changes, we will add a summary cell
    // to each row to show whether they were caused by row updates, additions or removals.
    // We also at this point make sure the cells of the row are output in a consistent
    // order with a header.
    for (const rowId of rowIds) {
      if (droppedRows.length > 0) {
        // Bulk additions/removals/updates may result in just some rows being saved.
        // We signal this visually with a "..." row.  The order of where this should
        // go isn't well defined at this point (there's a row number TODO above).
        if (rowId > droppedRows[0]) {
          tableChanges.cells.push(['...', droppedRows[0],
                                   activeColsWithoutManualSort.map(x => [null, null] as [null, null])]);
          while (rowId > droppedRows[0]) {
            droppedRows.shift();
          }
        }
      }
      // For each rowId, we need to issue either 1 or 2 rows.  We issue 2 rows
      // if the rowId is both added and removed - in this scenario, the rows
      // before and after are unrelated.  In all other cases, the before and
      // after values refer to the same row.
      const versions: Array<[string, (diff: CellDelta) => CellDelta]> = [];
      if (addedRows.has(rowId) && removedRows.has(rowId)) {
        versions.push(['-', (diff) => [diff[0], null]]);
        versions.push(['+', (diff) => [null, diff[1]]]);
      } else {
        let code: string = '...';
        if (updatedRows.has(rowId)) { code = '→'; }
        if (addedRows.has(rowId))   { code = '+';  }
        if (removedRows.has(rowId)) { code = '-';  }
        versions.push([code, (diff) => diff]);
      }
      for (const [code, transform] of versions) {
        const acc: CellDelta[] = [];
        const perCol = perRow[rowId];
        activeColsWithoutManualSort.forEach(col => {
          const diff = perCol ? perCol[col] : null;
          if (!diff) {
            acc.push([null, null]);
          } else {
            acc.push(transform(diff));
          }
        });
        tableChanges.cells.push([code, rowId, acc]);
      }
    }
  }
  return allChanges;
}

/**
 * Return a suitable key for a removed table/column.  We cannot use their id directly
 * since it could clash with an added table/column of the same name.
 */
export function defunctTableName(id: string): string {
  return `-${id}`;
}

export function rootTableName(id: string): string {
  return id.replace('-', '');
}

/**
 * Returns a list of all tables changed by the summarized action.  Changes include
 * schema or data changes.  Tables are identified by their post-action name.
 * Deleted tables are identified by their pre-action name, with "-" prepended.
 */
export function getAffectedTables(summary: ActionSummary): string[] {
  return [
    // Tables added, renamed, or removed in this action.
    ...summary.tableRenames.map(pair => pair[1] || defunctTableName(pair[0] || "")),
    // Tables modified in this action.
    ...Object.keys(summary.tableDeltas)
  ];
}

/**
 * Given a tableId from after the specified renames, figure out what the tableId was before
 * the renames.  Returns null if table didn't exist.
 */
export function getTableIdBefore(renames: LabelDelta[], tableIdAfter: string|null): string|null {
  if (tableIdAfter === null) { return tableIdAfter; }
  const rename = renames.find(_rename => _rename[1] === tableIdAfter);
  return rename ? rename[0] : tableIdAfter;
}

/**
 * Given a tableId from before the specified renames, figure out what the tableId is after
 * the renames.  Returns null if there is no valid tableId to return.
 */
export function getTableIdAfter(renames: LabelDelta[], tableIdBefore: string|null): string|null {
  if (tableIdBefore === null) { return tableIdBefore; }
  const rename = renames.find(_rename => _rename[0] === tableIdBefore);
  const tableIdAfter = rename ? rename[1] : tableIdBefore;
  if (tableIdAfter?.startsWith('-')) { return null; }
  return tableIdAfter;
}