import * as React from 'react';
import { Orientation } from '../../../Enums/Orientation';
import { Position } from '../../../Enums/Position';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, SHOW_SELF_DIMENSIONS } from '../../../utils/default';
import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../../utils/itertools';
import { TransformX, TransformY } from '../../../utils/svg';
import { Dimension } from './Dimension';

interface IDimensionLayerProps {
  containers: Map<string, IContainerModel>
  root: IContainerModel
  scale: number
}

const MODULE_STROKE_WIDTH = 1;

/**
 * Fonction that call another function given the positions
 * @param dimMapped Position mapped depending on the Position enum in order:
 * [0:left, 1:bottom, 2:up, 3:right]
 * @param positions List of positions
 * @param horizontalAction Action called when a left or right position is present
 * @param verticalAction Action called when a down or up position is present
 * @param params Params for the actions
 * (the two actions must have the same number of params, and in the same order)
 */
function ActionByPosition(
  dimMapped: number[],
  positions: Position[],
  horizontalAction: (dim: number, ...params: any[]) => void,
  verticalAction: (dim: number, ...params: any[]) => void,
  params: any[]
): void {
  positions.forEach((position: Position) => {
    const dim = dimMapped[position];
    switch (position) {
      case Position.Left:
      case Position.Right:
        verticalAction(dim, ...params);
        break;
      case Position.Down:
      case Position.Up:
        horizontalAction(dim, ...params);
        break;
    }
  });
}

/**
 * Returns a list of dimensions of all containers in root
 * @param param0 Object with the root container and the scale of the svg
 * @returns A list of dimensions
 */
function Dimensions({ containers, root, scale }: IDimensionLayerProps): React.ReactNode[] {
  const it = MakeRecursionDFSIterator(root, containers, 0, [0, 0]);
  const dimensions: React.ReactNode[] = [];
  const topDim = root.properties.y;
  const leftDim = root.properties.x;
  const rightDim = root.properties.x + root.properties.width;
  const bottomDim = root.properties.y + root.properties.height;

  if (!SHOW_SELF_DIMENSIONS) {
    return [];
  }

  for (const { container, depth, currentTransform } of it) {
    const containerLeftDim = leftDim - (DIMENSION_MARGIN * (depth + 1)) / scale;
    const containerTopDim = topDim - (DIMENSION_MARGIN * (depth + 1)) / scale;
    const containerBottomDim = bottomDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
    const containerRightDim = rightDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
    const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim];
    if (SHOW_SELF_DIMENSIONS && container.properties.showSelfDimensions.length > 0) {
      ActionByPosition(
        dimMapped,
        container.properties.showSelfDimensions,
        AddHorizontalSelfDimension,
        AddVerticalSelfDimension,
        [
          container,
          currentTransform,
          dimensions,
          scale]
      );
    }

    if (SHOW_BORROWER_DIMENSIONS && container.properties.showDimensionWithMarks.length > 0) {
      ActionByPosition(
        dimMapped,
        container.properties.showDimensionWithMarks,
        AddHorizontalBorrowerDimension,
        AddVerticalBorrowerDimension,
        [
          containers,
          container,
          depth,
          currentTransform,
          dimensions,
          scale]
      );
    }

    if (SHOW_CHILDREN_DIMENSIONS && container.properties.showChildrenDimensions.length > 0 && container.children.length >= 2) {
      ActionByPosition(
        dimMapped,
        container.properties.showChildrenDimensions,
        AddHorizontalChildrenDimension,
        AddVerticalChildrenDimension,
        [
          containers,
          container,
          currentTransform,
          dimensions,
          scale]
      );
    }
  }

  return dimensions;
}

/**
 * A layer containing all dimension
 * @param props
 * @returns
 */
export function DimensionLayer(props: IDimensionLayerProps): JSX.Element {
  return (
    <g>
      { Dimensions(props) }
    </g>
  );
}

/// Dimensions Actions ///

function AddHorizontalChildrenDimension(
  yDim: number,
  containers: Map<string, IContainerModel>,
  container: IContainerModel,
  currentTransform: [number, number],
  dimensions: React.ReactNode[],
  scale: number
): void {
  const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`;

  const lastChildId = container.children[container.children.length - 1];
  const lastChild = FindContainerById(containers, lastChildId);

  if (lastChild === undefined) {
    return;
  }

  let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference);
  let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference);

  // Find the min and max
  for (let i = container.children.length - 2; i >= 0; i--) {
    const childId = container.children[i];
    const child = FindContainerById(containers, childId);

    if (child === undefined) {
      continue;
    }

    const left = TransformX(child.properties.x, child.properties.width, child.properties.positionReference);
    if (left < xChildrenStart) {
      xChildrenStart = left;
    }
    const right = TransformX(child.properties.x, child.properties.width, child.properties.positionReference);
    if (right > xChildrenEnd) {
      xChildrenEnd = right;
    }
  }

  if (xChildrenStart === xChildrenEnd) {
    // do not show an empty dimension
    return;
  }

  const textChildren = (xChildrenEnd - xChildrenStart)
    .toFixed(2)
    .toString();

  const offset = currentTransform[0] + container.properties.x;
  dimensions.push(<Dimension
    key={childrenId}
    id={childrenId}
    xStart={xChildrenStart + offset}
    xEnd={xChildrenEnd + offset}
    yStart={yDim}
    yEnd={yDim}
    strokeWidth={MODULE_STROKE_WIDTH}
    text={textChildren}
    scale={scale} />);
}

function AddVerticalChildrenDimension(
  xDim: number,
  containers: Map<string, IContainerModel>,
  container: IContainerModel,
  currentTransform: [number, number],
  dimensions: React.ReactNode[],
  scale: number
): void {
  const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`;

  const lastChildId = container.children[container.children.length - 1];
  const lastChild = FindContainerById(containers, lastChildId);

  if (lastChild === undefined) {
    return;
  }

  let yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference);
  let yChildrenEnd = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference);

  // Find the min and max
  for (let i = container.children.length - 2; i >= 0; i--) {
    const childId = container.children[i];
    const child = FindContainerById(containers, childId);

    if (child === undefined) {
      continue;
    }

    const top = TransformY(child.properties.y, child.properties.height, child.properties.positionReference);
    if (top < yChildrenStart) {
      yChildrenStart = top;
    }
    const bottom = TransformY(child.properties.y, child.properties.height, child.properties.positionReference);
    if (bottom > yChildrenEnd) {
      yChildrenEnd = bottom;
    }
  }

  if (yChildrenStart === yChildrenEnd) {
    // do not show an empty dimension
    return;
  }

  const textChildren = (yChildrenEnd - yChildrenStart)
    .toFixed(2)
    .toString();

  const offset = currentTransform[0] + container.properties.x;
  dimensions.push(<Dimension
    key={childrenId}
    id={childrenId}
    xStart={xDim}
    yStart={yChildrenStart + offset}
    xEnd={xDim}
    yEnd={yChildrenEnd + offset}
    strokeWidth={MODULE_STROKE_WIDTH}
    text={textChildren}
    scale={scale}
  />);
}

function AddHorizontalBorrowerDimension(
  yDim: number,
  containers: Map<string, IContainerModel>,
  container: IContainerModel,
  depth: number,
  currentTransform: [number, number],
  dimensions: React.ReactNode[],
  scale: number
): void {
  const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
  const marks = []; // list of vertical lines for the dimension
  for (const {
    container: childContainer, currentTransform: childCurrentTransform
  } of it) {
    const isHidden = !childContainer.properties.markPosition.includes(Orientation.Horizontal);
    if (isHidden) {
      continue;
    }

    const x = TransformX(
      childContainer.properties.x,
      childContainer.properties.width,
      childContainer.properties.positionReference
    );

    const restoredX = x + childCurrentTransform[0];

    marks.push(
      restoredX
    );
  }

  const restoredX = container.properties.x + currentTransform[0];
  marks.push(restoredX);
  marks.push(restoredX + container.properties.width);
  marks.sort((a, b) => a - b);
  let count = 0;
  for (const { cur, next } of Pairwise(marks)) {
    const id = `dim-y${yDim.toFixed(0)}-borrow-${container.properties.id}-{${count}}`;
    dimensions.push(<Dimension
      key={id}
      id={id}
      xStart={cur}
      xEnd={next}
      yStart={yDim}
      yEnd={yDim}
      strokeWidth={MODULE_STROKE_WIDTH}
      text={(next - cur).toFixed(0).toString()}
      scale={scale} />);
    count++;
  }
}

function AddVerticalBorrowerDimension(
  xDim: number,
  containers: Map<string, IContainerModel>,
  container: IContainerModel,
  depth: number,
  currentTransform: [number, number],
  dimensions: React.ReactNode[],
  scale: number
): void {
  const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
  const marks = []; // list of vertical lines for the dimension
  for (const {
    container: childContainer, currentTransform: childCurrentTransform
  } of it) {
    const isHidden = !childContainer.properties.markPosition.includes(Orientation.Vertical);
    if (isHidden) {
      continue;
    }

    const y = TransformY(
      childContainer.properties.y,
      childContainer.properties.height,
      childContainer.properties.positionReference
    );

    const restoredy = y + childCurrentTransform[1];

    marks.push(
      restoredy
    );
  }

  const restoredY = container.properties.y + currentTransform[1];
  marks.push(restoredY);
  marks.push(restoredY + container.properties.height);
  marks.sort((a, b) => a - b);
  let count = 0;
  for (const { cur, next } of Pairwise(marks)) {
    const id = `dim-x${xDim.toFixed(0)}-borrow-${container.properties.id}-{${count}}`;
    dimensions.push(<Dimension
      key={id}
      id={id}
      xStart={xDim}
      xEnd={xDim}
      yStart={cur}
      yEnd={next}
      strokeWidth={MODULE_STROKE_WIDTH}
      text={(next - cur).toFixed(0).toString()}
      scale={scale} />);
    count++;
  }
}

function AddVerticalSelfDimension(
  xDim: number,
  container: IContainerModel,
  currentTransform: [number, number],
  dimensions: React.ReactNode[],
  scale: number
): void {
  const height = container.properties.height;
  const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`;
  const yStart = container.properties.y + currentTransform[1];
  const yEnd = yStart + height;
  const textVert = height
    .toFixed(0)
    .toString();
  dimensions.push(
    <Dimension
      key={idVert}
      id={idVert}
      xStart={xDim}
      yStart={yStart}
      xEnd={xDim}
      yEnd={yEnd}
      strokeWidth={MODULE_STROKE_WIDTH}
      text={textVert}
      scale={scale} />
  );
}

function AddHorizontalSelfDimension(
  yDim: number,
  container: IContainerModel,
  currentTransform: [number, number],
  dimensions: React.ReactNode[],
  scale: number
): void {
  const width = container.properties.width;
  const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`;
  const xStart = container.properties.x + currentTransform[0];
  const xEnd = xStart + width;
  const text = width
    .toFixed(0)
    .toString();
  dimensions.push(
    <Dimension
      key={id}
      id={id}
      xStart={xStart}
      yStart={yDim}
      xEnd={xEnd}
      yEnd={yDim}
      strokeWidth={MODULE_STROKE_WIDTH}
      text={text}
      scale={scale} />
  );
}
