import { CellValue, EvaluatedCell } from "./cells";

// -----------------------------------------------------------------------------
// MISC
// -----------------------------------------------------------------------------
import { CompiledFormula } from "../formulas/compiler";
import { CommandResult } from "./commands";
import { Format } from "./format";
import { ComputeFunction } from "./functions";
import { Range } from "./range";

/**
 * The following type is meant to be used in union with other aliases to prevent
 * Intellisense from resolving it.
 * See https://github.com/microsoft/TypeScript/issues/31940#issuecomment-841712377
 */
export type Alias = {} & {};

// Col/row Index
export type HeaderIndex = number & Alias;

// any DOM pixel value
export type Pixel = number & Alias;

// Unique identifier
export type UID = string & Alias;

export type SetDecimalStep = 1 | -1;
export type FilterId = UID & Alias;
export type TableId = UID & Alias;

/**
 * CSS style color string
 * e.g. "#ABC", "#AAAFFF", "rgb(30, 80, 16)"
 */
export type Color = string & Alias;

export interface RGBA {
  a: number;
  r: number;
  g: number;
  b: number;
}

export interface HSLA {
  a: number;
  h: number;
  s: number;
  l: number;
}

export interface Link {
  readonly label: string;
  readonly url: string;
  readonly isExternal: boolean;
  /**
   * Specifies if the URL is editable by the end user.
   * Special links might not allow it.
   */
  readonly isUrlEditable: boolean;
}

export interface Zone {
  left: HeaderIndex;
  right: HeaderIndex;
  top: HeaderIndex;
  bottom: HeaderIndex;
}

export interface AnchorZone {
  zone: Zone;
  cell: Position;
}

export interface Selection {
  anchor: AnchorZone;
  zones: Zone[];
}

export interface UnboundedZone {
  top: HeaderIndex;
  bottom: HeaderIndex | undefined;
  left: HeaderIndex;
  right: HeaderIndex | undefined;
  /**
   * The hasHeader flag is used to determine if the zone has a header (eg. A2:A or C3:3).
   *
   * The main issue is that the zone A1:A and A:A have different behavior. The "correct" way to handle this would be to
   * allow the top/left to be undefined, but this make typing and using unbounded zones VERY annoying. So we use this
   * boolean instead.
   */
  hasHeader?: boolean;
}

export interface ZoneDimension {
  numberOfRows: HeaderIndex;
  numberOfCols: HeaderIndex;
}

export type Align = "left" | "right" | "center" | undefined;
export type StyleAlign = Align | "default";

export type VerticalAlign = "top" | "middle" | "bottom" | undefined;

export type Wrapping = "overflow" | "wrap" | "clip" | undefined;

export interface Style {
  bold?: boolean;
  italic?: boolean;
  strikethrough?: boolean;
  underline?: boolean;
  align?: StyleAlign;
  wrapping?: Wrapping;
  verticalAlign?: VerticalAlign;
  fillColor?: Color;
  textColor?: Color;
  fontSize?: number; // in pt, not in px!
  rotation?: number; // in rad, clockwise because y+ is down
  hideGridLines?: boolean; // hide the grid lines for this cell. Mainly used for pivot tables and cell animations.
}

export interface DataBarFill {
  color: Color;
  percentage: number;
}

export interface UpdateCellData {
  content?: string;
  formula?: string;
  style?: Style | null;
  format?: Format | null;
}

export interface Sheet {
  id: UID;
  name: string;
  numberOfCols: number;
  rows: Row[];
  areGridLinesVisible: boolean;
  isVisible: boolean;
  panes: PaneDivision;
  color?: Color;
  isLocked?: boolean;
  backgroundColor?: Color;
}

export interface CellPosition {
  col: HeaderIndex;
  row: HeaderIndex;
  sheetId: UID;
}

export const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"] as const;
export type BorderStyle = (typeof borderStyles)[number];
// A complete border description is a pair [style, color]
export type BorderDescr = { style: BorderStyle; color: Color };

/**
 * A complete border(s) data is a set of position-color-style information
 */
export type BorderData = {
  position: BorderPosition;
  color?: Color;
  style?: BorderStyle;
};

export interface Border {
  top?: BorderDescr;
  left?: BorderDescr;
  bottom?: BorderDescr;
  right?: BorderDescr;
}

export type ReferenceDenormalizer = (range: Range) => FunctionResultObject;

export type EnsureRange = (range: Range) => Matrix<FunctionResultObject>;

export type GetSymbolValue = (symbolName: string, isRange: boolean) => Arg;

export type FormulaToExecute = (
  deps: Range[],
  refFn: ReferenceDenormalizer,
  range: EnsureRange,
  getSymbolValue: GetSymbolValue,
  ctx: object,
  functions: ComputeFunction[]
) => Matrix<FunctionResultObject> | FunctionResultObject;

export interface LiteralValues {
  numbers: { value: number }[];
  strings: { value: string }[];
}

export type Matrix<T = unknown> = T[][];

export type FunctionResultObject = {
  value: CellValue;
  format?: Format;
  errorOriginPosition?: CellPosition;
  message?: string;
  origin?: CellPosition;
  /**
   * When a formula returns a reference to another cell (rather than just a value),
   * this stores the referenced cell's position.
   *
   * Example: `LOOKUP(42, B1:B9, C1:C9)` might resolve to cell C5, setting
   * `position` to C5. Formulas that specifically expect a cell reference as input
   * can then receive C5 as a reference rather than just its value
   * (e.g. `=CELL("format", LOOKUP(42, B1:B9, C1:C9))`).
   */
  position?: CellPosition;
};

export type FunctionResultNumber = { value: number; format?: string };

// FORMULA FUNCTION VALUE AND FORMAT INPUT
export type Arg = Maybe<FunctionResultObject> | Matrix<FunctionResultObject>; // undefined corresponds to the lack of argument, e.g. =SUM(1,2,,4)

export function isMatrix(x: any): x is Matrix<any> {
  return Array.isArray(x) && Array.isArray(x[0]);
}

export interface ClipboardCell {
  evaluatedCell: EvaluatedCell;
  position: CellPosition;
  content: string;
  style?: Style | undefined;
  format?: Format | undefined;
  compiledFormula?: CompiledFormula;
  border?: Border;
}

export interface HeaderDimensions {
  start: Pixel;
  size: Pixel;
  end: Pixel;
}

export interface Row {
  cells: Record<number, number | undefined>; // number is a column index
}

export interface Position {
  col: HeaderIndex;
  row: HeaderIndex;
}

export interface PixelPosition {
  x: Pixel;
  y: Pixel;
}

export interface PixelDimension {
  width: Pixel;
  height: Pixel;
}

export interface Merge extends Zone {
  id: number;
}

export interface Highlight {
  range: Range;
  color: Color;
  interactive?: boolean;
  thinLine?: boolean;
  noFill?: boolean;
  /** transparency of the fill color (0-1) */
  fillAlpha?: number;
  noBorder?: boolean;
  dashed?: boolean;
}

export interface PaneDivision {
  /** Represents the number of frozen columns */
  xSplit: number;
  /** Represents the number of frozen rows */
  ySplit: number;
}

export const borderPositions = [
  "all",
  "hv",
  "h",
  "v",
  "external",
  "left",
  "top",
  "right",
  "bottom",
  "clear",
] as const;

export type BorderPosition = (typeof borderPositions)[number];

export const enum DIRECTION {
  UP = "up",
  DOWN = "down",
  LEFT = "left",
  RIGHT = "right",
}

export type ChangeType = "REMOVE" | "RESIZE" | "MOVE" | "CHANGE" | "NONE";
export type ApplyRangeChangeResult<T> = { changeType: ChangeType; range: T };
export type ApplyFormulaRangeChangeResult = { changeType: ChangeType; formula: string };
export type ApplyRangeChange = (range: Range) => ApplyRangeChangeResult<Range>;
export type ApplyRenameNamedRange = (currentRangeName: string) => string;

export type AdaptSheetName = { old: string; current: string };

export type RangeAdapter = {
  sheetId: UID;
  sheetName: AdaptSheetName;
  applyChange: ApplyRangeChange;
};

export type RangeAdapterFunctions = {
  applyChange: ApplyRangeChange;
  adaptRangeString: (defaultSheetId: UID, sheetXC: string) => ApplyRangeChangeResult<string>;
  adaptFormulaString: (defaultSheetId: UID, formula: string) => string;
  adaptCompiledFormula: (compiledFormula: CompiledFormula) => CompiledFormula;
};

export type Dimension = "COL" | "ROW";

export type ConsecutiveIndexes = HeaderIndex[];

export interface RangeProvider {
  adaptRanges: (adapterFunctions: RangeAdapterFunctions) => void;
}

export interface FormulaProvider {
  getFormulas: () => CompiledFormula[];
}

export type Validation<T> = (toValidate: T) => CommandResult | CommandResult[];

export type Increment = 1 | -1 | 0;

export interface Ref<T> {
  el: T | null;
}

/**
 * Container for a lazy computed value
 */
export interface Lazy<T> {
  /**
   * Return the computed value.
   * The value is computed only once and memoized.
   */
  (): T;
  /**
   * Map a lazy value to another lazy value.
   *
   * ```ts
   * // neither function is called here
   * const lazyValue = lazy(() => veryExpensive(...)).map((result) => alsoVeryExpensive(result));
   *
   * // both values are computed now
   * const value = lazyValue()
   * ```
   */
  map: <U>(callback: (value: T) => U) => Lazy<U>;
}

export type Maybe<T> = T | undefined;

export interface Cloneable<T> {
  clone: (args?: Partial<T>) => T;
}

export type CSSProperties<P extends string = string> = Record<P, string | undefined>;

export interface SortOptions {
  /** If true sort the headers of the range along with the rest */
  sortHeaders?: boolean;
  /** If true treat empty cells as "0" instead of undefined */
  emptyCellAsZero?: boolean;
}

export interface MenuMouseEvent extends MouseEvent {
  closedMenuId?: UID;
}

// https://github.com/Microsoft/TypeScript/issues/13923#issuecomment-557509399
// prettier-ignore
export type Immutable<T> =
  T extends ImmutablePrimitive ? T :
  T extends Array<infer U> ? ImmutableArray<U> :
  T extends Map<infer K, infer V> ? ImmutableMap<K, V> :
  T extends Set<infer M> ? ImmutableSet<M> :
  ImmutableObject<T>;

type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };

// DeepPartial implementation taken from the utility-types NPM package, which is
// Copyright (c) 2016 Piotr Witek <piotrek.witek@gmail.com> (http://piotrwitek.github.io)
// and used under the terms of the MIT license
export type DeepPartial<T> = T extends Function
  ? T
  : T extends Array<infer U>
  ? _DeepPartialArray<U>
  : T extends object
  ? _DeepPartialObject<T>
  : T | undefined;

type _DeepPartialArray<T> = Array<DeepPartial<T>>;
type _DeepPartialObject<T> = { [P in keyof T]?: DeepPartial<T[P]> };

export interface HeaderGroup {
  start: HeaderIndex;
  end: HeaderIndex;
  isFolded?: boolean;
}

export type Direction = "up" | "down" | "left" | "right";

export type SelectionStep = number | "end";

export interface Offset {
  col: number;
  row: number;
}

export interface PixelOffset {
  offsetX: Pixel;
  offsetY: Pixel;
}

export type DebouncedFunction<T> = T & {
  stopDebounce: () => void;
  isDebouncePending: () => boolean;
};

export interface GridClickModifiers {
  addZone: boolean;
  expandZone: boolean;
}

export const composerFocusTypes = ["inactive", "cellFocus", "contentFocus"] as const;
export type ComposerFocusType = (typeof composerFocusTypes)[number];

export type EditionMode =
  | "editing"
  | "selecting" // should tell if you need to underline the current range selected.
  | "inactive";

export type SortDirection = "asc" | "desc";

export interface ValueAndLabel<T = string> {
  value: T;
  label: string;
  separator?: boolean;
}

export interface NamedRange {
  name: string;
  range: Range;
}

export interface SheetViewDimensions {
  height: Pixel;
  width: Pixel;
  gridOffsetX?: Pixel;
  gridOffsetY?: Pixel;
}
