// @ts-nocheck
import { asTRBL, getOrientation, getMid } from 'diagram-js/lib/layout/LayoutUtil';
import { find, reduce } from 'min-dash';
import { Shape } from 'diagram-js/lib/model';
import { Point } from 'diagram-js/lib/util/Types';
import { DEFAULT_DISTANCE } from '../config/constants';

// padding to detect element placement
const PLACEMENT_DETECTION_PAD = 10;
const DEFAULT_MAX_DISTANCE = 350;

/**
 * Get free position starting from given position.
 *
 * @param {Shape} source
 * @param {Shape} element
 * @param {Point} position
 * @param {Function} getNextPosition
 * @return {Point}
 */
export function findFreePosition(source: Shape, element: Shape, position: Point, getNextPosition: Function): Point {
  let connectedAtPosition;
  while ((connectedAtPosition = getConnectedAtPosition(source, position, element))) {
    position = getNextPosition(element, position, connectedAtPosition);
  }
  return position;
}

/**
 * Returns function that returns next position.
 *
 * @param {Object} nextPositionDirection
 * @param {Object} [nextPositionDirection.x]
 * @param {Object} [nextPositionDirection.y]
 * @returns {Function}
 */
export function generateGetNextPosition(nextPositionDirection: { x?: any; y?: any }): Function {
  return function (element: Shape, previousPosition: Point, connectedAtPosition: Point) {
    const nextPosition: any = {
      x: previousPosition.x,
      y: previousPosition.y,
    };
    ['x', 'y'].forEach(axis => {
      const nextPositionDirectionForAxis = nextPositionDirection[axis];
      if (!nextPositionDirectionForAxis) return;
      const dimension = axis === 'x' ? 'width' : 'height';
      const margin = nextPositionDirectionForAxis.margin;
      const minDistance = nextPositionDirectionForAxis.minDistance;
      if (margin < 0) nextPosition[axis] = Math.min(connectedAtPosition[axis] + margin - element[dimension] / 2, previousPosition[axis] - minDistance + margin);
      else {
        nextPosition[axis] = Math.max(
          connectedAtPosition[axis] + connectedAtPosition[dimension] + margin + element[dimension] / 2,
          previousPosition[axis] + minDistance + margin,
        );
      }
    });
    return nextPosition;
  };
}

/**
 * Return target at given position, if defined.
 *
 * This takes connected elements from host and attachers
 * into account, too.
 */
export function getConnectedAtPosition(source: Shape, position: Point, element: Shape): Shape | undefined {
  const bounds = {
    x: position.x - element.width / 2,
    y: position.y - element.height / 2,
    width: element.width,
    height: element.height,
  };
  const closure = getAutoPlaceClosure(source, element);
  return find(closure, (target: Shape) => {
    if (target === element) return false;
    const orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD);
    return orientation === 'intersect';
  });
}

/**
 * Compute optimal distance between source and target based on existing connections to and from source.
 * Assumes left-to-right and top-to-down modeling.
 *
 * @param {Shape} source
 * @param {Object} [hints]
 * @param {number} [hints.defaultDistance]
 * @param {string} [hints.direction]
 * @param {Function} [hints.filter]
 * @param {Function} [hints.getWeight]
 * @param {number} [hints.maxDistance]
 * @param {string} [hints.reference]
 * @return {number}
 */
export function getConnectedDistance(
  source: Shape,
  hints?: {
    defaultDistance?: number;
    direction?: string;
    filter?: Function;
    getWeight?: Function;
    maxDistance?: number;
    reference?: string;
  },
): number {
  if (!hints) hints = {};
  function getDefaultWeight(connection: any): number {
    return connection.source === source ? 1 : -1;
  }
  const defaultDistance = hints.defaultDistance || DEFAULT_DISTANCE;
  const direction = hints.direction || 'e';
  let filter = hints.filter;
  const getWeight = hints.getWeight || getDefaultWeight;
  const maxDistance = hints.maxDistance || DEFAULT_MAX_DISTANCE;
  const reference = hints.reference || 'start';
  if (!filter) filter = noneFilter;
  function getDistance(a: any, b: any): number {
    if (direction === 'n') {
      if (reference === 'start') {
        return asTRBL(a).top - asTRBL(b).bottom;
      } else if (reference === 'center') {
        return asTRBL(a).top - getMid(b).y;
      } else {
        return asTRBL(a).top - asTRBL(b).top;
      }
    } else if (direction === 'w') {
      if (reference === 'start') return asTRBL(a).left - asTRBL(b).right;
      else if (reference === 'center') return asTRBL(a).left - getMid(b).x;
      else return asTRBL(a).left - asTRBL(b).left;
    } else if (direction === 's') {
      if (reference === 'start') return asTRBL(b).top - asTRBL(a).bottom;
      else if (reference === 'center') return getMid(b).y - asTRBL(a).bottom;
      else return asTRBL(b).bottom - asTRBL(a).bottom;
    } else {
      if (reference === 'start') return asTRBL(b).left - asTRBL(a).right;
      else if (reference === 'center') return getMid(b).x - asTRBL(a).right;
      else return asTRBL(b).right - asTRBL(a).right;
    }
  }

  const sourcesDistances = source.incoming.filter(filter).map((connection: any) => {
    const weight = getWeight(connection);
    const distance = weight < 5 ? getDistance(connection.source, source) : getDistance(source, connection.source);
    return {
      id: connection.source.id,
      distance: distance,
      weight: weight,
    };
  });

  const targetsDistances = source.outgoing.filter(filter).map((connection: any) => {
    const weight = getWeight(connection);
    const distance = weight > 5 ? getDistance(source, connection.target) : getDistance(connection.target, source);
    return {
      id: connection.target.id,
      distance: distance,
      weight: weight,
    };
  });

  const distances = sourcesDistances.concat(targetsDistances).reduce((accumulator, currentValue) => {
    accumulator[currentValue.id + '__weight_' + currentValue.weight] = currentValue;
    return accumulator;
  }, {});

  const distancesGrouped = reduce(
    distances,
    (accumulator, currentValue) => {
      const distance = currentValue.distance;
      const weight = currentValue.weight;
      if (distance < 0 || distance > maxDistance) return accumulator;
      if (!accumulator[String(distance)]) accumulator[String(distance)] = 0;
      accumulator[String(distance)] += 1 * weight;
      if (!accumulator.distance || accumulator[accumulator.distance] < accumulator[String(distance)]) accumulator.distance = distance;
      return accumulator;
    },
    {},
  );
  return distancesGrouped.distance || defaultDistance;
}
/**
 * Returns all connected elements around the given source.
 * This includes:
 *   - connected elements
 *   - host connected elements
 *   - attachers connected elements
 * @param {Shape} source
 * @return {Array<Shape>}
 */
function getAutoPlaceClosure(source: Shape, element: Shape): Array<Shape> {
  let allConnected = getConnected(source);
  if (source.host) allConnected = allConnected.concat(getConnected(source.host));
  if (source.attachers)
    allConnected = allConnected.concat(
      source.attachers.reduce((shapes: Array<Shape>, attacher: Shape) => {
        return shapes.concat(getConnected(attacher));
      }, []),
    );
  return allConnected;
}
function getConnected(element: Shape): Array<Shape> {
  return getTargets(element).concat(getSources(element));
}
function getSources(shape: Shape): Array<Shape> {
  return shape.incoming.map((connection: any) => {
    return connection.source;
  });
}
function getTargets(shape: Shape): Array<Shape> {
  return shape.outgoing.map((connection: any) => {
    return connection.target;
  });
}
function noneFilter(): boolean {
  return true;
}
