/**
 * Copyright (c) Tiny Technologies, Inc. All rights reserved.
 * Licensed under the LGPL or a commercial license.
 * For LGPL see License.txt in the project root for license information.
 * For commercial licenses see https://www.tiny.cloud/
 */

/**
 * Contains various tools for rect/position calculation.
 *
 * @class tinymce.geom.Rect
 */

export interface GeomRect {
  readonly x: number;
  readonly y: number;
  readonly w: number;
  readonly h: number;
}

interface Rect {
  inflate: (rect: GeomRect, w: number, h: number) => GeomRect;
  relativePosition: (rect: GeomRect, targetRect: GeomRect, rel: string) => GeomRect;
  findBestRelativePosition: (rect: GeomRect, targetRect: GeomRect, constrainRect: GeomRect, rels: string[]) => string | null;
  intersect: (rect: GeomRect, cropRect: GeomRect) => GeomRect | null;
  clamp: (rect: GeomRect, clampRect: GeomRect, fixedSize?: boolean) => GeomRect;
  create: (x: number, y: number, w: number, h: number) => GeomRect;
  fromClientRect: (clientRect: ClientRect) => GeomRect;
}

const min = Math.min, max = Math.max, round = Math.round;

/**
 * Returns the rect positioned based on the relative position name
 * to the target rect.
 *
 * @method relativePosition
 * @param {Rect} rect Source rect to modify into a new rect.
 * @param {Rect} targetRect Rect to move relative to based on the rel option.
 * @param {String} rel Relative position. For example: tr-bl.
 */
const relativePosition = (rect: GeomRect, targetRect: GeomRect, rel: string) => {
  let x = targetRect.x;
  let y = targetRect.y;
  const w = rect.w;
  const h = rect.h;
  const targetW = targetRect.w;
  const targetH = targetRect.h;

  const relChars = (rel || '').split('');

  if (relChars[0] === 'b') {
    y += targetH;
  }

  if (relChars[1] === 'r') {
    x += targetW;
  }

  if (relChars[0] === 'c') {
    y += round(targetH / 2);
  }

  if (relChars[1] === 'c') {
    x += round(targetW / 2);
  }

  if (relChars[3] === 'b') {
    y -= h;
  }

  if (relChars[4] === 'r') {
    x -= w;
  }

  if (relChars[3] === 'c') {
    y -= round(h / 2);
  }

  if (relChars[4] === 'c') {
    x -= round(w / 2);
  }

  return create(x, y, w, h);
};

/**
 * Tests various positions to get the most suitable one.
 *
 * @method findBestRelativePosition
 * @param {Rect} rect Rect to use as source.
 * @param {Rect} targetRect Rect to move relative to.
 * @param {Rect} constrainRect Rect to constrain within.
 * @param {Array} rels Array of relative positions to test against.
 */
const findBestRelativePosition = (rect: GeomRect, targetRect: GeomRect, constrainRect: GeomRect, rels: string[]) => {
  let pos, i;

  for (i = 0; i < rels.length; i++) {
    pos = relativePosition(rect, targetRect, rels[i]);

    if (pos.x >= constrainRect.x && pos.x + pos.w <= constrainRect.w + constrainRect.x &&
      pos.y >= constrainRect.y && pos.y + pos.h <= constrainRect.h + constrainRect.y) {
      return rels[i];
    }
  }

  return null;
};

/**
 * Inflates the rect in all directions.
 *
 * @method inflate
 * @param {Rect} rect Rect to expand.
 * @param {Number} w Relative width to expand by.
 * @param {Number} h Relative height to expand by.
 * @return {Rect} New expanded rect.
 */
const inflate = (rect: GeomRect, w: number, h: number) => {
  return create(rect.x - w, rect.y - h, rect.w + w * 2, rect.h + h * 2);
};

/**
 * Returns the intersection of the specified rectangles.
 *
 * @method intersect
 * @param {Rect} rect The first rectangle to compare.
 * @param {Rect} cropRect The second rectangle to compare.
 * @return {Rect} The intersection of the two rectangles or null if they don't intersect.
 */
const intersect = (rect: GeomRect, cropRect: GeomRect) => {
  const x1 = max(rect.x, cropRect.x);
  const y1 = max(rect.y, cropRect.y);
  const x2 = min(rect.x + rect.w, cropRect.x + cropRect.w);
  const y2 = min(rect.y + rect.h, cropRect.y + cropRect.h);

  if (x2 - x1 < 0 || y2 - y1 < 0) {
    return null;
  }

  return create(x1, y1, x2 - x1, y2 - y1);
};

/**
 * Returns a rect clamped within the specified clamp rect. This forces the
 * rect to be inside the clamp rect.
 *
 * @method clamp
 * @param {Rect} rect Rectangle to force within clamp rect.
 * @param {Rect} clampRect Rectable to force within.
 * @param {Boolean} fixedSize True/false if size should be fixed.
 * @return {Rect} Clamped rect.
 */
const clamp = (rect: GeomRect, clampRect: GeomRect, fixedSize?: boolean) => {
  let x1 = rect.x;
  let y1 = rect.y;
  let x2 = rect.x + rect.w;
  let y2 = rect.y + rect.h;
  const cx2 = clampRect.x + clampRect.w;
  const cy2 = clampRect.y + clampRect.h;

  const underflowX1 = max(0, clampRect.x - x1);
  const underflowY1 = max(0, clampRect.y - y1);
  const overflowX2 = max(0, x2 - cx2);
  const overflowY2 = max(0, y2 - cy2);

  x1 += underflowX1;
  y1 += underflowY1;

  if (fixedSize) {
    x2 += underflowX1;
    y2 += underflowY1;
    x1 -= overflowX2;
    y1 -= overflowY2;
  }

  x2 -= overflowX2;
  y2 -= overflowY2;

  return create(x1, y1, x2 - x1, y2 - y1);
};

/**
 * Creates a new rectangle object.
 *
 * @method create
 * @param {Number} x Rectangle x location.
 * @param {Number} y Rectangle y location.
 * @param {Number} w Rectangle width.
 * @param {Number} h Rectangle height.
 * @return {Rect} New rectangle object.
 */
const create = (x: number, y: number, w: number, h: number) => {
  return { x, y, w, h };
};

/**
 * Creates a new rectangle object form a clientRects object.
 *
 * @method fromClientRect
 * @param {ClientRect} clientRect DOM ClientRect object.
 * @return {Rect} New rectangle object.
 */
const fromClientRect = (clientRect: ClientRect) => {
  return create(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
};

const Rect: Rect = {
  inflate,
  relativePosition,
  findBestRelativePosition,
  intersect,
  clamp,
  create,
  fromClientRect
};

export default Rect;
