/**
 * @license
 * Copyright (c) 2025 Handsoncode. All rights reserved.
 */
import { NoSheetWithIdError } from "../../errors.mjs";
import { EmptyValue } from "../../interpreter/InterpreterValue.mjs";
import { ArrayFormulaVertex, DenseStrategy, ValueCellVertex } from "../index.mjs";
/**
 * Manages cell vertices and provides access to vertex by SimpleCellAddress.
 * For each sheet it stores vertices according to AddressMappingStrategy: DenseStrategy or SparseStrategy.
 *
 * Pleceholder sheets:
 * - for placeholders sheets (sheets that are used in formulas but not yet added), it stores placeholder strategy entries (DenseStrategy(0, 0))
 * - placeholder strategy entries may contain EmptyCellVertex-es but never ValueCellVertex or FormulaVertex as they content is empty
 * - vertices in placeholder strategy entries are used only for dependency tracking
 */
export class AddressMapping {
  constructor(policy) {
    this.policy = policy;
    this.mapping = new Map();
  }
  /**
   * Gets the cell vertex at the specified address.
   */
  getCell(address, options = {}) {
    const sheetMapping = this.mapping.get(address.sheet);
    if (!sheetMapping) {
      if (options.throwIfSheetNotExists) {
        throw new NoSheetWithIdError(address.sheet);
      }
      return undefined;
    }
    const cell = sheetMapping.getCell(address);
    if (!cell && options.throwIfCellNotExists) {
      throw Error('Vertex for address missing in AddressMapping');
    }
    return cell;
  }
  /**
   * Gets the cell vertex at the specified address or throws if it doesn't exist.
   * @throws {NoSheetWithIdError} if sheet doesn't exist
   * @throws {Error} if cell doesn't exist
   */
  getCellOrThrow(address) {
    const sheetMapping = this.mapping.get(address.sheet);
    if (!sheetMapping) {
      throw new NoSheetWithIdError(address.sheet);
    }
    const cell = sheetMapping.getCell(address);
    if (!cell) {
      throw Error('Vertex for address missing in AddressMapping');
    }
    return cell;
  }
  /**
   * Gets the address mapping strategy for the specified sheet.
   * @throws {NoSheetWithIdError} if sheet doesn't exist
   */
  getStrategyForSheetOrThrow(sheetId) {
    const strategy = this.mapping.get(sheetId);
    if (strategy === undefined) {
      throw new NoSheetWithIdError(sheetId);
    }
    return strategy;
  }
  /**
   * Adds a new sheet with the specified strategy.
   * @throws {Error} if sheet is already added and throwIfSheetAlreadyExists is true
   */
  addSheetWithStrategy(sheetId, strategy, options = {
    throwIfSheetAlreadyExists: true
  }) {
    const strategyFound = this.mapping.get(sheetId);
    if (strategyFound) {
      if (options.throwIfSheetAlreadyExists) {
        throw Error('Sheet already added');
      }
      return strategyFound;
    }
    this.mapping.set(sheetId, strategy);
    return strategy;
  }
  /**
   * Adds a sheet or changes the strategy for an existing sheet.
   * Designed for the purpose of exchanging the placeholder strategy for a real strategy.
   */
  addSheetOrChangeStrategy(sheetId, sheetBoundaries) {
    const newStrategy = this.createStrategyBasedOnBoundaries(sheetBoundaries);
    const strategyPlaceholder = this.mapping.get(sheetId);
    if (!strategyPlaceholder) {
      this.mapping.set(sheetId, newStrategy);
      return newStrategy;
    }
    if (newStrategy instanceof DenseStrategy) {
      // new strategy is the same as the placeholder
      return strategyPlaceholder;
    }
    this.moveStrategyContent(strategyPlaceholder, newStrategy, sheetId);
    this.mapping.set(sheetId, newStrategy);
    return newStrategy;
  }
  /**
   * Moves the content of the source strategy to the target strategy.
   */
  moveStrategyContent(sourceStrategy, targetStrategy, sheetContext) {
    const sourceVertices = sourceStrategy.getEntries(sheetContext);
    for (const [address, vertex] of sourceVertices) {
      targetStrategy.setCell(address, vertex);
    }
  }
  /**
   * Adds a sheet and sets the strategy based on the sheet boundaries.
   * @throws {Error} if sheet already exists and throwIfSheetAlreadyExists is true
   */
  addSheetAndSetStrategyBasedOnBoundaries(sheetId, sheetBoundaries, options = {
    throwIfSheetAlreadyExists: true
  }) {
    this.addSheetWithStrategy(sheetId, this.createStrategyBasedOnBoundaries(sheetBoundaries), options);
  }
  /**
   * Creates a strategy based on the sheet boundaries.
   */
  createStrategyBasedOnBoundaries(sheetBoundaries) {
    const {
      height,
      width,
      fill
    } = sheetBoundaries;
    const strategyConstructor = this.policy.call(fill);
    return new strategyConstructor(width, height);
  }
  /**
   * Adds a placeholder strategy (DenseStrategy) for a sheet. If the sheet already exists, does nothing.
   */
  addSheetStrategyPlaceholderIfNotExists(sheetId) {
    if (this.mapping.has(sheetId)) {
      return;
    }
    this.mapping.set(sheetId, new DenseStrategy(0, 0));
  }
  /**
   * Removes a sheet from the address mapping.
   * If sheet does not exist, does nothing.
   * @returns {boolean} true if sheet was removed, false if it did not exist.
   */
  removeSheetIfExists(sheetId) {
    return this.mapping.delete(sheetId);
  }
  /**
   * Gets the interpreter value of a cell at the specified address.
   * @returns {InterpreterValue} The interpreter value (returns EmptyValue if cell doesn't exist)
   */
  getCellValue(address) {
    const vertex = this.getCell(address);
    if (vertex === undefined) {
      return EmptyValue;
    } else if (vertex instanceof ArrayFormulaVertex) {
      return vertex.getArrayCellValue(address);
    } else {
      return vertex.getCellValue();
    }
  }
  /**
   * Gets the raw cell content at the specified address.
   * @returns {RawCellContent} The raw cell content or null if cell doesn't exist or is not a value cell
   */
  getRawValue(address) {
    const vertex = this.getCell(address);
    if (vertex instanceof ValueCellVertex) {
      return vertex.getValues().rawValue;
    } else if (vertex instanceof ArrayFormulaVertex) {
      return vertex.getArrayCellRawValue(address);
    } else {
      return null;
    }
  }
  /**
   * Sets a cell vertex at the specified address.
   * @throws {Error} if sheet not initialized
   */
  setCell(address, newVertex) {
    const sheetMapping = this.mapping.get(address.sheet);
    if (!sheetMapping) {
      throw Error('Sheet not initialized');
    }
    sheetMapping.setCell(address, newVertex);
  }
  /**
   * Moves a cell from source address to destination address.
   * Supports cross-sheet moves (used for placeholder sheet merging).
   * @throws {Error} if source sheet not initialized
   * @throws {Error} if destination occupied
   * @throws {Error} if source cell doesn't exist
   */
  moveCell(source, destination) {
    const sheetMapping = this.mapping.get(source.sheet);
    if (!sheetMapping) {
      throw Error('Sheet not initialized.');
    }
    if (this.has(destination)) {
      throw new Error('Cannot move cell. Destination already occupied.');
    }
    const vertex = sheetMapping.getCell(source);
    if (vertex === undefined) {
      throw new Error('Cannot move cell. No cell with such address.');
    }
    this.setCell(destination, vertex);
    this.removeCell(source);
  }
  /**
   * Removes a cell at the specified address.
   * @throws Error if sheet not initialized
   */
  removeCell(address) {
    const sheetMapping = this.mapping.get(address.sheet);
    if (!sheetMapping) {
      throw Error('Sheet not initialized');
    }
    sheetMapping.removeCell(address);
  }
  /**
   * Checks if a cell exists at the specified address.
   */
  has(address) {
    const sheetMapping = this.mapping.get(address.sheet);
    if (sheetMapping === undefined) {
      return false;
    }
    return sheetMapping.has(address);
  }
  /**
   * Gets the height of the specified sheet.
   */
  getSheetHeight(sheetId) {
    const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
    return sheetMapping.getHeight();
  }
  /**
   * Gets the width of the specified sheet.
   */
  getSheetWidth(sheetId) {
    const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
    return sheetMapping.getWidth();
  }
  /**
   * Adds rows to a sheet.
   */
  addRows(sheetId, row, numberOfRows) {
    const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
    sheetMapping.addRows(row, numberOfRows);
  }
  /**
   * Removes rows from a sheet.
   */
  removeRows(removedRows) {
    const sheetMapping = this.getStrategyForSheetOrThrow(removedRows.sheet);
    sheetMapping.removeRows(removedRows);
  }
  /**
   * Adds columns to a sheet starting at the specified column index.
   */
  addColumns(sheetId, column, numberOfColumns) {
    const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
    sheetMapping.addColumns(column, numberOfColumns);
  }
  /**
   * Removes columns from a sheet.
   */
  removeColumns(removedColumns) {
    const sheetMapping = this.getStrategyForSheetOrThrow(removedColumns.sheet);
    sheetMapping.removeColumns(removedColumns);
  }
  /**
   * Returns an iterator of cell vertices within the specified rows span.
   */
  *verticesFromRowsSpan(rowsSpan) {
    yield* this.mapping.get(rowsSpan.sheet).verticesFromRowsSpan(rowsSpan); // eslint-disable-line @typescript-eslint/no-non-null-assertion
  }
  /**
   * Returns an iterator of cell vertices within the specified columns span.
   */
  *verticesFromColumnsSpan(columnsSpan) {
    yield* this.mapping.get(columnsSpan.sheet).verticesFromColumnsSpan(columnsSpan); // eslint-disable-line @typescript-eslint/no-non-null-assertion
  }
  /**
   * Returns an iterator of address-vertex pairs within the specified rows span.
   */
  *entriesFromRowsSpan(rowsSpan) {
    const sheetMapping = this.getStrategyForSheetOrThrow(rowsSpan.sheet);
    yield* sheetMapping.entriesFromRowsSpan(rowsSpan);
  }
  /**
   * Returns an iterator of address-vertex pairs within the specified columns span.
   */
  *entriesFromColumnsSpan(columnsSpan) {
    const sheetMapping = this.getStrategyForSheetOrThrow(columnsSpan.sheet);
    yield* sheetMapping.entriesFromColumnsSpan(columnsSpan);
  }
  /**
   * Returns an iterator of all address-vertex pairs across all sheets.
   * @returns {IterableIterator<[SimpleCellAddress, Maybe<CellVertex>]>} Iterator of [address, vertex] tuples
   */
  *entries() {
    for (const [sheet, mapping] of this.mapping.entries()) {
      yield* mapping.getEntries(sheet);
    }
  }
  /**
   * Returns an iterator of address-vertex pairs for a specific sheet.
   * @returns {IterableIterator<[SimpleCellAddress, CellVertex]>} Iterator of [address, vertex] tuples
   * @throws {NoSheetWithIdError} if sheet doesn't exist
   */
  *sheetEntries(sheetId) {
    const sheetMapping = this.getStrategyForSheetOrThrow(sheetId);
    yield* sheetMapping.getEntries(sheetId);
  }
  /**
   * Checks if a sheet has any entries.
   * @throws {NoSheetWithIdError} if sheet doesn't exist
   */
  hasAnyEntries(sheetId) {
    const iterator = this.sheetEntries(sheetId);
    return !iterator.next().done;
  }
}