AWT.js

/**
 *  File    : AWT.js
 *  Created : 12/04/2015
 *  By      : Francesc Busquets <francesc@gmail.com>
 *
 *  JClic.js
 *  An HTML5 player of JClic activities
 *  https://projectestac.github.io/jclic.js
 *
 *  @source https://github.com/projectestac/jclic.js
 *
 *  @license EUPL-1.2
 *  @licstart
 *  (c) 2000-2020 Educational Telematic Network of Catalonia (XTEC)
 *
 *  Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
 *  the European Commission- subsequent versions of the EUPL (the "Licence");
 *  You may not use this work except in compliance with the Licence.
 *
 *  You may obtain a copy of the Licence at:
 *  https://joinup.ec.europa.eu/software/page/eupl
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *  Licence for the specific language governing permissions and limitations
 *  under the Licence.
 *  @licend
 *  @module
 */

/* global console, window */

import $ from 'jquery';
import { settings, findParentsWithChild, getBoolean, getAttr, setAttr, checkColor, colorHasTransparency, fx } from './Utils.js';
import WebFont from 'webfontloader';

/**
 * Font contains properties and provides methods to manage fonts
 */
export class Font {
  /**
   * Font constructor
   * @param {string} [family='Arial']
   * @param {number} [size=17]
   * @param {number} [bold=0]
   * @param {number} [italic=0]
   * @param {string} [variant='']
   */
  constructor(family, size, bold, italic, variant) {
    if (family)
      this.family = family;
    if (typeof size === 'number')
      this.size = size;
    if (bold)
      this.bold = bold;
    if (italic)
      this.italic = italic;
    if (variant)
      this.variant = variant;
    this._metrics = { ascent: -1, descent: -1, height: -1 };
  }

  /**
   * Finds the XML elements with typeface specifications, checks its value against the font
   * substitution list, replacing the `family` attribute and loading the alternative font when needed.
   * @param {external:jQuery} $tree - The xml element to be processed
   * @param {object} [options] - Optional param that can contain a `fontSubstitutions` attribute with
   * a substition table to be added to {@link module:AWT.Font.SUBSTITUTIONS SUBSTITUTIONS}
   */
  static checkTree($tree, options) {
    let substitutions = Font.SUBSTITUTIONS;
    // Load own fonts and remove it from the substitution table
    if (options && options.ownFonts) {
      options.ownFonts.forEach(name => {
        // Check WebFont as a workaround to avoid problems with a different version of `webfontloader` in agora.xtec.cat
        if (Font.ALREADY_LOADED_FONTS.indexOf(name) < 0 && WebFont && WebFont.load) {
          WebFont.load({ custom: { families: [name] } });
          Font.ALREADY_LOADED_FONTS.push(name);
          delete substitutions[name.trim().toLowerCase()];
        }
      });
    }

    // Add custom font substitutions
    if (options && options.fontSubstitutions)
      //substitutions = Object.assign({}, substitutions, options.fontSubstitutions)
      substitutions = $.extend(Object.create(substitutions), options.fontSubstitutions);

    if ($tree.jquery)
      $tree.find('style[family],font[family]').each((_n, style) => {
        const $style = $(style),
          name = $style.attr('family').trim().toLowerCase();
        if (name in substitutions) {
          const newName = substitutions[name];
          if (newName !== '') {
            Font.loadGoogleFont(newName);
            $style.attr('family', newName);
          }
        }
      });
    else {
      findParentsWithChild($tree, 'family').forEach(parent => {
        if (typeof parent.family === 'string') {
          const name = parent.family;
          if (Font.GOOGLEFONTS.includes(name))
            Font.loadGoogleFont(name);
          else {
            const newName = substitutions[name.trim().toLowerCase()];
            if (newName) {
              Font.loadGoogleFont(newName);
              parent.family = newName;
            }
          }
        }
      });
    }
  }

  /**
   * Try to load a specific font from [http://www.google.com/fonts]
   * @param {string} name - The font family name
   */
  // Check WebFont as a workaround to avoid problems with a different version of `webfontloader` in agora.xtec.cat
  static loadGoogleFont(name) {
    if (name && !Font.ALREADY_LOADED_FONTS.includes(name) && WebFont && WebFont.load) {
      WebFont.load({ google: { families: [name] } });
      Font.ALREADY_LOADED_FONTS.push(name);
    }
  }

  /**
   * Try to load a set of Google fonts
   * @param {string[]} fonts - An array of font names
   */
  static loadGoogleFonts(fonts) {
    if (fonts && fonts.forEach)
      fonts.forEach(name => Font.loadGoogleFont(name));
  }

  /**
   * Reads the properties of this Font from an XML element
   * @param {external:jQuery} $xml - The xml element to be parsed
   * @returns {module:AWT.Font}
   */
  setProperties($xml) {
    if ($xml.attr('family'))
      this.family = $xml.attr('family');
    if ($xml.attr('size'))
      this.size = Number($xml.attr('size'));
    if ($xml.attr('bold'))
      this.bold = getBoolean($xml.attr('bold'));
    if ($xml.attr('italic'))
      this.italic = getBoolean($xml.attr('italic'));
    if ($xml.attr('variant'))
      this.variant = $xml.attr('variant');
    return this;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return getAttr(this, ['family|Arial', 'size|17', 'bold|0', 'italic|0', 'variant']);
  }

  /**
   * Reads the properties of this Font from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:AWT.Font}
   */
  setAttributes(data) {
    return setAttr(this, data, ['family', 'size', 'bold', 'italic', 'variant']);
  }

  /**
   * Allows to change the `size` member, recalculating the vertical metrics.
   * @param {number} size - The new size to set
   * @returns {module:AWT.Font}
   */
  setSize(size) {
    const currentSize = this.size;
    this.size = size;
    if (currentSize !== size)
      this._metrics.height = -1;
    return this;
  }

  /**
   * Increases or decreases the current font size by the specified amount
   * @param {number} amount - The amount to increase or decrease current size
   * @returns {module:AWT.Font}
   */
  zoom(amount) {
    return this.setSize(this.size + amount);
  }

  /**
   * Calculates the font metrics
   * @returns {Object} - The font metrics
   */
  getMetrics() {
    if (this._metrics.height < 0) {
      // Look for an equivalent font already calculated
      const font = Font.ALREADY_CALCULATED_FONTS.find(font => font.equals(this));
      if (font)
        Object.assign(this._metrics, font._metrics);

      if (this._metrics.height < 0) {
        this._calcHeight();
        if (this._metrics.height > 0)
          Font.ALREADY_CALCULATED_FONTS.push(this);
      }
    }
    return this._metrics;
  }

  /**
   * Calculates the font metrics and returns its height
   * @returns {number} - The font height
   */
  getHeight() {
    return this.getMetrics().height;
  }

  /**
   * Translates the Font properties into CSS statements
   * @param {object} css - The object where to add CSS properties. When null or undefined, a new
   * object will be created and returned.
   * @returns {object} - A set of CSS property-values pairs, ready to be used by JQuery
   * [.css(properties)](http://api.jquery.com/css/#css-properties).
   */
  toCss(css) {
    if (!css)
      css = {};
    css['font-family'] = this.family;
    css['font-size'] = `${this.size}px`;
    if (this.hasOwnProperty('bold'))
      css['font-weight'] = this.bold ? 'bold' : 'normal';
    if (this.hasOwnProperty('italic'))
      css['font-style'] = this.italic ? 'italic' : 'normal';
    if (this.hasOwnProperty('variant'))
      css['font-variant'] = this.variant;
    return css;
  }

  /**
   * Gets the codification of this font in a single string, suitable to be used in a `font`
   * CSS attribute.
   * @returns {string} - A string with all the CSS font properties concatenated
   */
  cssFont() {
    return `${this.italic ? 'italic ' : 'normal'} ${this.variant === '' ? 'normal' : this.variant} ${this.bold ? 'bold ' : 'normal'} ${this.size}px ${this.family}`;
  }

  /**
   * The {@link https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics TextMetrics} object used
   * by {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D CanvasRenderingContext2D}
   * does not provide a `heigth` value for rendered text.
   * This {@link http://stackoverflow.com/questions/1134586/how-can-you-find-the-height-of-text-on-an-html-canvas stackoverflow question}
   * has an excellent response by Daniel Earwicker explaining how to measure the
   * vertical dimension of rendered text using a `span` element.
   * The code has been slighty adapted to deal with Font objects.
   *
   * _Warning_: Do not call this method direcly. Use {@link module:AWT.Font#getHeight getHeight()} or {@link module:AWT.Font#getMetrics getMetrics()} instead
   *
   * @returns {module:AWT.Font}
   */
  _calcHeight() {
    const
      $text = $('<span/>').html('Hg').css(this.toCss()),
      $block = $('<div/>').css({ display: 'inline-block', width: '1px', height: '0px' }),
      $div = $('<div/>').append($text, $block);

    $('body').append($div);
    try {
      $block.css({ verticalAlign: 'baseline' });
      this._metrics.ascent = $block.offset().top - $text.offset().top;
      $block.css({ verticalAlign: 'bottom' });
      this._metrics.height = $block.offset().top - $text.offset().top;
      this._metrics.descent = this._metrics.height - this._metrics.ascent;
    } finally {
      $div.remove();
    }
    return this;
  }

  /**
   * Checks if two Font objects are equivalent
   * @param {module:AWT.Font} font - The Font object to compare against this one
   * @returns {boolean} - `true` if both objects are equivalent, `false` otherwise
   */
  equals(font) {
    return this.family === font.family &&
      this.size === font.size &&
      this.bold === font.bold &&
      this.italic === font.italic &&
      this.variant === font.variant;
  }
}


/**
 * Array of font objects with already calculated heights */
Font.ALREADY_CALCULATED_FONTS = [];

/**
 * Array of font names already loaded from Google Fonts, or generic names provided by browsers by default
 * See: https://developer.mozilla.org/en-US/docs/Web/CSS/font-family */
Font.ALREADY_LOADED_FONTS = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy'];

/**
 * Google Fonts equivalent for special fonts used in some JClic projects.
 * More substitutions can be added to the list for specific projects indicating a
 * `fontSubstitutions` object in the `data-options` attribute of the HTML `div` element
 * containing the player.
 * For example:
 * `<div class ="JClic" data-project="demo.jclic" data-options='{"fontSubstitutions":{"arial":"Arimo"}}'/>`
 */
Font.SUBSTITUTIONS = {
  // Lowercase versions of JDK Logical Fonts (see: https://docs.oracle.com/javase/tutorial/2d/text/fonts.html)
  'dialog': 'sans-serif',
  'dialoginput': 'sans-serif',
  'monospaced': 'monospace',
  //'serif': 'serif',
  'sansserif': 'sans-serif',
  // Other fonts commonly used in JClic activities, mapped to similar Google Fonts
  'abc': 'Kalam',
  'a.c.m.e. secret agent': 'Permanent Marker',
  'comic sans ms': 'Patrick Hand',
  'impact': 'Oswald',
  'massallera': 'Vibur',
  'memima': 'Vibur',
  'memima_n1': 'Vibur',
  'memima_n2': 'Vibur',
  'memimas-regularalternate': 'Vibur',
  'palmemim': 'Vibur',
  'zurichcalligraphic': 'Felipa'
};
/**
 * Google Fonts currently used in substitutions
 */
Font.GOOGLEFONTS = [
  'Kalam', 'Permanent Marker', 'Patrick Hand', 'Oswald', 'Vibur', 'Felipa',
];

Object.assign(Font.prototype, {
  /**
   * The `font-family` property
   * @name module:AWT.Font#family
   * @type {string} */
  family: 'Arial',
  /**
   * The font size
   * __Warning__: Do not change `size` directly. Use {@link module:AWT.Font#setSize setSize()} instead.
   * @name module:AWT.Font#size
   * @type {number} */
  size: 17,
  /**
   * The font _bold_ value
   * @name module:AWT.Font#bold
   * @type {number} */
  bold: 0,
  /**
   * The font _italic_ value
   * @name module:AWT.Font#italic
   * @type {number} */
  italic: 0,
  /**
   * The font _variant_ value
   * @name module:AWT.Font#variant
   * @type {string}*/
  variant: '',
  /**
   * The font *_metrics* property contains the values for `ascent`, `descent` and `height`
   * attributes. Vertical font metrics are calculated in
   * {@link module:AWT.Font#_calcHeight|_calcHeight()} as needed.
   * @name module:AWT.Font#_metrics
   * @private
   * @type {{ascent: number, descent: number, height: number}} */
  _metrics: { ascent: -1, descent: -1, height: -1 },
});

/**
 * Contains parameters and methods to draw complex color gradients
 */
export class Gradient {
  /**
   * Gradient constructor
   * @param {string} c1 - The initial color, in any CSS-valid form.
   * @param {string} c2 - The final color, in any CSS-valid form.
   * @param {number} [angle=0] - The inclination of the gradient relative to the horizontal line.
   * @param {number} [cycles=1] - The number of times the gradient will be repeated.
   */
  constructor(c1, c2, angle, cycles) {
    if (c1)
      this.c1 = c1;
    if (c2)
      this.c2 = c2;
    if (typeof angle === 'number')
      this.angle = angle % 360;
    if (typeof cycles === 'number')
      this.cycles = cycles;
  }

  /**
   * Reads the properties of this Gradient from an XML element
   * @param {external:jQuery} $xml - The xml element to be parsed
   * @returns {module:AWT.Gradient}
   */
  setProperties($xml) {
    this.c1 = checkColor($xml.attr('source'), 'black');
    this.c2 = checkColor($xml.attr('dest'), 'white');
    this.angle = Number($xml.attr('angle') || 0) % 360;
    this.cycles = Number($xml.attr('cycles') || 1);
    return this;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return getAttr(this, [
      'c1', 'c2', 'angle|0', 'cycles|1'
    ]);
  }

  /**
   * Reads the properties of this Gradient from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:AWT.Gradient}
   */
  setAttributes(data) {
    return setAttr(this, data, ['c1', 'c2', 'angle', 'cycles']);
  }

  /**
   * Creates a {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient|CanvasGradient}
   * based on the provided context and rectangle.
   * @param {external:CanvasRenderingContext2D} ctx - The 2D rendering context
   * @param {module:AWT.Rectangle} rect - The rectangle where this gradient will be applied to
   * @returns {module:AWT.Gradient}
   */
  getGradient(ctx, rect) {
    const
      p2 = rect.getOppositeVertex(),
      gradient = ctx.createLinearGradient(rect.pos.x, rect.pos.y, p2.x, p2.y),
      step = 1 / Math.max(this.cycles, 1);
    for (let i = 0; i <= this.cycles; i++)
      gradient.addColorStop(i * step, i % 2 ? this.c1 : this.c2);
    return gradient;
  }

  /**
   * Gets the CSS 'linear-gradient' expression of this Gradient
   * @returns {string} - A string ready to be used as a value for the `linear-gradient` CSS attribute
   */
  getCss() {
    let result = `linear-gradient(${(this.angle + 90)}deg, ${this.c1}, ${this.c2}`;
    for (let i = 1; i < this.cycles; i++)
      result = `${result}, ${i % 2 > 0 ? this.c1 : this.c2}`;
    return `${result})`;
  }

  /**
   * Checks if any of the gradient colors has transparency
   * @returns {boolean} - `true` if this gradient uses colors with transparency, `false` otherwise.
   */
  hasTransparency() {
    return colorHasTransparency(this.c1) || colorHasTransparency(this.c2);
  }
}

Object.assign(Gradient.prototype, {
  /**
   * Initial color
   * @name module:AWT.Gradient#c1
   * @type {string} */
  c1: 'white',
  /**
   * Final color
   * @name module:AWT.Gradient#c2
   * @type {string} */
  c2: 'black',
  /**
   * Tilt angle
   * @name module:AWT.Gradient#angle
   * @type {number} */
  angle: 0,
  /**
   * Number of repetitions of the gradient
   * @name module:AWT.Gradient#cycles
   * @type {number} */
  cycles: 1,
});

/**
 * Contains properties used to draw lines in HTML `canvas` elements.
 * @see {@link http://bucephalus.org/text/CanvasHandbook/CanvasHandbook.html#line-caps-and-joins}
 */
export class Stroke {
  /**
   * Stroke constructor
   * @param {number} [lineWidth=1] - The line width of the stroke
   * @param {string} [lineCap='butt'] - The line ending type. Possible values are: `butt`, `round`
   * and `square`.
   * @param {string} [lineJoin='miter'] - The type of drawing used when two lines join. Possible
   * values are: `round`, `bevel` and `miter`.
   * @param {number} [miterLimit=10] - The ratio between the miter length and half `lineWidth`.
   */
  constructor(lineWidth, lineCap, lineJoin, miterLimit) {
    if (typeof lineWidth === 'number')
      this.lineWidth = lineWidth;
    if (lineCap)
      this.lineCap = lineCap;
    if (lineJoin)
      this.lineJoin = lineJoin;
    if (typeof miterLimit === 'number')
      this.miterLimit = miterLimit;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return getAttr(this, [
      'lineWidth|1', 'lineCap|butt', 'lineJoin|miter', 'miterLimit|10',
    ]);
  }

  /**
   * Reads the properties of this Stroke from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:AWT.Stroke}
   */
  setAttributes(data) {
    return setAttr(this, data, ['lineWidth', 'lineCap', 'lineJoin', 'miterLimit']);
  }

  /**
   * Sets the properties of this stroke to a CanvasRenderingContext2D
   * @param {external:CanvasRenderingContext2D} ctx - The canvas 2D rendering context
   * @returns {external:CanvasRenderingContext2D}
   */
  setStroke(ctx) {
    ctx.lineWidth = this.lineWidth;
    ctx.lineCap = this.lineCap;
    ctx.lineJoin = this.lineJoin;
    ctx.miterLimit = this.miterLimit;
    return ctx;
  }
}

Object.assign(Stroke.prototype, {
  /**
   * The line width
   * @name module:AWT.Stroke#lineWidth
   * @type {number} */
  lineWidth: 1.0,
  /**
   * The line ending type (`butt`, `round` or `square`)
   * @name module:AWT.Stroke#lineCap
   * @type {string} */
  lineCap: 'butt',
  /**
   * The drawing used when two lines join (`round`, `bevel` or `miter`)
   * @name module:AWT.Stroke#lineJoin
   * @type {string} */
  lineJoin: 'miter',
  /**
   * Ratio between the miter length and half `lineWidth`
   * @name module:AWT.Stroke#miterLimit
   * @type {number} */
  miterLimit: 10.0,
});

/**
 * Contains the `x` andy `y` coordinates of a point, and provides some useful methods.
 */
export class Point {
  /**
   * Point constructor
   * @param {number|Point} x - When `x` is an `Point` object, a clone of it will be created.
   * @param {number} [y] - Not used when `x` is an `Point`
   */
  constructor(x, y) {
    if (x instanceof Point) {
      // Special case: constructor passing another point as unique parameter
      this.x = x.x;
      this.y = x.y;
    } else {
      this.x = x || 0;
      this.y = y || 0;
    }
  }

  /**
   * Reads the properties of this Point from an XML element
   * @param {external:jQuery} $xml - The xml element to be parsed
   * @returns {module:AWT.Point}
   */
  setProperties($xml) {
    this.x = Number($xml.attr('x'));
    this.y = Number($xml.attr('y'));
    return this;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return getAttr(this, ['x', 'y']);
  }

  /**
   * Reads the properties of this Point from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:AWT.Point}
   */
  setAttributes(data) {
    return setAttr(this, data, ['x', 'y']);
  }

  /**
   * Moves this Point to a new position, by a specified displacement
   * @param {Point|Dimension} delta - The amount to move
   * @returns {module:AWT.Point}
   */
  moveBy(delta) {
    this.x += delta.x || delta.width || 0;
    this.y += delta.y || delta.height || 0;
    return this;
  }

  /**
   * Moves this Point to a new position
   * @param {number|Point} newPos - The new position, or a x coordinate
   * @param {number} [y] - `null` or `undefined` when `newPos` is a Point
   * @returns {module:AWT.Point}
   */
  moveTo(newPos, y) {
    if (typeof newPos === 'number') {
      this.x = newPos;
      this.y = y;
    } else {
      this.x = newPos.x;
      this.y = newPos.y;
    }
    return this;
  }

  /**
   * Multiplies the `x` and `y` coordinates by a specified `delta`
   * @param {Point|Dimension} delta - The amount to multiply by.
   * @returns {module:AWT.Point}
   */
  multBy(delta) {
    this.x *= delta.x || delta.width || 0;
    this.y *= delta.y || delta.height || 0;
    return this;
  }

  /**
   * Checks if two points are at the same place
   * @param {module:AWT.Point} p - The Point to check against to
   * @returns {boolean}
   */
  equals(p) {
    return this.x === p.x && this.y === p.y;
  }

  /**
   * Calculates the distance between two points
   * @param {module:AWT.Point} point - The Point to calculate the distance against to
   * @returns {number} - The distance between the two points.
   */
  distanceTo(point) {
    return Math.sqrt(Math.pow(this.x - point.x, 2), Math.pow(this.y - point.y, 2));
  }

  /**
   * Clones this point
   * @returns {module:AWT.Point}
   */
  clone() {
    return new Point(this);
  }
}

Object.assign(Point.prototype, {
  /**
   * @name module:AWT.Point#x
   * @type {number} */
  x: 0,
  /**
   * @name module:AWT.Point#y
   * @type {number} */
  y: 0,
});

/**
 * This class encapsulates `width` and `height` properties.
 */
export class Dimension {
  /**
   * Dimension constructor
   * @param {number|Point} w - The width of this Dimension, or the upper-left vertex of a
   * virtual Rectangle
   * @param {number|Point} h - The height of this Dimension, or the bottom-right vertex of a
   * virtual Rectangle
   */
  constructor(w, h) {
    if (w instanceof Point && h instanceof Point) {
      this.width = h.x - w.x;
      this.height = h.y - w.y;
    } else {
      this.width = w || 0;
      this.height = h || 0;
    }
  }

  /**
   * Reads the properties of this Dimension from an XML element
   * @param {external:jQuery} $xml - The xml element to be parsed
   * @returns {module:AWT.Dimension}
   */
  setProperties($xml) {
    this.width = Number($xml.attr('width'));
    this.height = Number($xml.attr('height'));
    return this;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return getAttr(this, ['width', 'height']);
  }

  /**
   * Reads the properties of this Dimension from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:AWT.Dimension}
   */
  setAttributes(data) {
    return setAttr(this, data, ['width', 'height']);
  }

  /**
   * Check if two dimensions are equivalent
   * @param {module:AWT.Dimension} d
   * @returns {boolean}
   */
  equals(d) {
    return this.width === d.width && this.height === d.height;
  }

  /**
   * Multiplies the `w` and `h` co-ordinates by a specified `delta`
   * @param {Point|Dimension} delta
   * @returns {module:AWT.Dimension}
   */
  multBy(delta) {
    this.width *= delta.x || delta.width || 0;
    this.height *= delta.y || delta.height || 0;
    return this;
  }

  /**
   * Sets new values for width and height.
   * `width` can be a number or another `Dimension` object
   * @param {number|Dimension} width - The new width, or a full Dimension to copy it from.
   * @param {number} [height] - Not used when `width` is a Dimension
   * @returns {module:AWT.Dimension}
   */
  setDimension(width, height) {
    if (width instanceof Dimension) {
      height = width.height;
      width = width.width;
    }
    this.width = width;
    this.height = height;
    return this;
  }

  /**
   * Calculates the area of a Rectangle with this dimension
   * @returns {number} The resulting area
   */
  getSurface() {
    return this.width * this.height;
  }
}

Object.assign(Dimension.prototype, {
  /**
   * @name module:AWT.Dimension#width
   * @type {number} */
  width: 0,
  /**
   * @name module:AWT.Dimension#height
   * @type {number} */
  height: 0,
});

/**
 * Shape is a generic abstract class for rectangles, ellipses and stroke-free shapes.
 * @abstract
 */
export class Shape {
  /**
   * Shape  constructor
   * @param {module:AWT.Point} pos - The top-left coordinates of this Shape
   */
  constructor(pos) {
    this.pos = pos || new Point();
  }

  /**
   * Shifts the shape a specified amount in horizontal and vertical directions
   * @param {Point|Dimension} delta - The amount to shift the Shape
   * @returns {module:AWT.Shape}
   */
  moveBy(delta) {
    this.pos.moveBy(delta);
    return this;
  }

  /**
   * Moves this shape to a new position
   * @param {module:AWT.Point} newPos - The new position of the shape
   * @returns {module:AWT.Shape}
   */
  moveTo(newPos) {
    this.pos.moveTo(newPos);
    return this;
  }

  /**
   * Gets the enclosing {@link module:AWT.Rectangle Rectangle} of this Shape.
   * @returns {module:AWT.Rectangle}
   */
  getBounds() {
    return new Rectangle(this.pos);
  }

  /**
   * Checks if two shapes are equivalent.
   * @param {module:AWT.Shape} p - The Shape to compare against
   * @returns {boolean}
   */
  equals(p) {
    return this.pos.equals(p.pos);
  }

  /**
   * Multiplies the dimension of the Shape by the specified `delta` amount.
   * @param {Point|Dimension} _delta - Object containing the X and Y ratio to be scaled.
   * @returns {module:AWT.Shape}
   */
  scaleBy(_delta) {
    // Nothing to scale in abstract shapes
    return this;
  }

  /**
   * Gets a clone of this shape moved to the `pos` component of the rectangle and scaled
   * by its `dim` value.
   * @param {module:AWT.Rectangle} rect - The rectangle to be taken as a base for moving and scaling
   * this shape.
   * @returns {module:AWT.Shape}
   */
  getShape(rect) {
    return this.clone().scaleBy(rect.dim).moveBy(rect.pos);
  }

  /**
   * Checks if the provided {@link module:AWT.Point} is inside this shape.
   * @param {module:AWT.Point} _p - The point to check
   * @returns {boolean}
   */
  contains(_p) {
    // Nothing to check in abstract shapes
    return false;
  }

  /**
   * Checks if the provided {@link module:AWT.Rectangle Rectangle} `r` intersects with this shape.
   * @param {module:AWT.Rectangle} _r
   * @returns {boolean}
   */
  intersects(_r) {
    // Nothing to check in abstract shapes
    return false;
  }

  /**
   * Fills the Shape with the current style in the provided HTML canvas context
   * @param {external:CanvasRenderingContext2D} ctx - The canvas 2D rendering context where to fill this shape.
   * @param {module:AWT.Rectangle} [dirtyRegion] - The context region to be updated. Used as clipping
   * region when drawing.
   * @returns {external:CanvasRenderingContext2D} - The provided rendering context
   */
  fill(ctx, dirtyRegion) {
    ctx.save();
    if (dirtyRegion && dirtyRegion.getSurface() > 0) {
      // Clip the dirty region
      ctx.beginPath();
      ctx.rect(dirtyRegion.pos.x, dirtyRegion.pos.y, dirtyRegion.dim.width, dirtyRegion.dim.height);
      ctx.clip();
    }
    // Prepare shape path and fill
    this.preparePath(ctx);
    ctx.fill();
    ctx.restore();
    return ctx;
  }

  /**
   * Draws this shape in the provided HTML canvas 2D rendering context.
   * @param {external:CanvasRenderingContext2D} ctx - The canvas 2D rendering context where to draw the shape.
   * @returns {external:CanvasRenderingContext2D} - The provided rendering context
   */
  stroke(ctx) {
    this.preparePath(ctx);
    ctx.stroke();
    return ctx;
  }

  /**
   * Prepares an HTML canvas 2D rendering context with a path that can be used to stroke a line,
   * to fill a surface or to define a clipping region.
   * @param {external:CanvasRenderingContext2D} ctx
   * @returns {external:CanvasRenderingContext2D} - The provided rendering context
   */
  preparePath(ctx) {
    // Nothing to do in abstract shapes
    return ctx;
  }

  /**
   * Creates a clipping region on the specified HTML canvas 2D rendering context
   * @param {external:CanvasRenderingContext2D} ctx - The rendering context
   * @param {string} [fillRule='nonzero'] - Can be 'nonzero' (default when not set) or 'evenodd'
   * @returns {external:CanvasRenderingContext2D} - The provided rendering context
   */
  clip(ctx, fillRule) {
    this.preparePath(ctx);
    ctx.clip(fillRule || 'nonzero');
    return ctx;
  }

  /**
   * Shorthand method for determining if a Shape is an {@link module:AWT.Rectangle Rectangle}
   * @returns {boolean}
   */
  isRect() {
    return false;
  }

  /**
   * Overwrites the original 'Object.toString' method with a more descriptive text
   * @returns {string}
   */
  toString() {
    return `Shape enclosed in ${this.getBounds().getCoords()}`;
  }

  /**
   * Reads the properties of this Shape from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:AWT.Shape}
   */
  setAttributes(data) {
    return Shape.buildShape(data);
    /*
    return setAttr(this, data, [
      'type',
      { key: 'pos', fn: Point },
    ]);
    */
  }

  /**
   * Builds a shape based on the provided `data` object.
   * Data should contain a 'type' member, specifying the type of shape requested ('rect', 'ellipse', 'rectangle' or 'path')
   * @param {object} data - Specific data for this shape
   * @returns {module:AWT.Shape}
   */
  static buildShape(data) {
    const shapeType = (data.type === 'rect' && Rectangle) || (data.type === 'ellipse' && Ellipse) || (data.type === 'path' && Path) || null;
    if (!shapeType) {
      console.log('unknown shape:', data);
    } else
      return (new shapeType()).setAttributes(data);
  }
}

Object.assign(Shape.prototype, {
  /**
   * Shape type id
   * @name module:AWT.Shape#type
   * @type {string} */
  type: 'shape',
  /**
   * The current position of the shape
   * @name module:AWT.Shape#pos
   * @type {module:AWT.Point} */
  pos: new Point(),
  /**
   * The type of shape (Rectangle, ellipse, path...)
   * @name module:AWT.Shape#type
   * @type {string} */
  type: 'shape',
});

/**
 * The rectangular {@link module:AWT.Shape} accepts five different sets of parameters:
 * @example
 * // Calling Rectangle() with different sets of parameters
 * // A Point and a Dimension:
 * new Rectangle(pos, dim)
 * // Another Rectangle, to be cloned:
 * new Rectangle(rect)
 * // Two Point objects containing the coordinates of upper-left and lower-right vertexs:
 * new Rectangle(p0, p1)
 * // An array of four numbers with the coordinates of the same vertexs:
 * new Rectangle([x0, y0, x1, y1])
 * // Four single numbers, meaning the same coordinates as above:
 * new Rectangle(x0, y0, x1, y1)
 * @extends module:AWT.Shape
 */
export class Rectangle extends Shape {
  /**
   * Rectangle constructor
   * @param {Point|Rectangle|number|number[]} pos
   * @param {Dimension|number} [dim]
   * @param {number} [w]
   * @param {number} [h]
   */
  constructor(pos, dim, w, h) {
    let p = pos, d = dim;
    // Special case: constructor with a Rectangle as a unique parameter
    if (pos instanceof Rectangle) {
      d = new Dimension(pos.dim.width, pos.dim.height);
      p = new Point(pos.pos.x, pos.pos.y);
    } else if (pos instanceof Point) {
      p = new Point(pos.x, pos.y);
      if (dim instanceof Dimension)
        d = new Dimension(dim.width, dim.height);
    } else if (pos instanceof Array) {
      // Assume `pos` is an array of numbers indicating: x0, y0, x1, y1
      p = new Point(pos[0], pos[1]);
      d = new Dimension(pos[2] - pos[0], pos[3] - pos[1]);
    } else if (typeof w === 'number' && typeof h === 'number') {
      // width and height passed. Treat all parameters as co-ordinates:
      p = new Point(pos, dim);
      d = new Dimension(w, h);
    }
    super(p);

    if (d instanceof Dimension)
      this.dim = d;
    else if (d instanceof Point)
      this.dim = new Dimension(d.x - this.pos.x, d.y - this.pos.y);
    else
      this.dim = new Dimension();

    this.type = 'rect';
  }

  /**
   * Gets the enclosing {@link module:AWT.Rectangle Rectangle} of this Shape.
   * @returns {module:AWT.Rectangle}
   */
  getBounds() {
    return this;
  }

  /**
   * Sets this Rectangle the position and dimension of another one
   * @param {module:AWT.Rectangle} rect
   * @returns {module:AWT.Rectangle}
   */
  setBounds(rect) {
    if (!rect)
      rect = new Rectangle();
    this.pos.x = rect.pos.x;
    this.pos.y = rect.pos.y;
    this.dim.width = rect.dim.width;
    this.dim.height = rect.dim.height;
    return this;
  }

  /**
   * Checks if two shapes are equivalent.
   * @param {module:AWT.Shape} r - The Shape to compare against
   * @returns {boolean}
   */
  equals(r) {
    return r instanceof Rectangle && this.pos.equals(r.pos) && this.dim.equals(r.dim);
  }

  /**
   * Clones this Rectangle
   * @returns {module:AWT.Rectangle}
   */
  clone() {
    return new Rectangle(this);
  }

  /**
   * Multiplies the dimension of the Shape by the specified `delta` amount.
   * @param {Point|Dimension} delta - Object containing the X and Y ratio to be scaled.
   * @returns {module:AWT.Rectangle}
   */
  scaleBy(delta) {
    this.pos.multBy(delta);
    this.dim.multBy(delta);
    return this;
  }

  /**
   * Expands the boundaries of this shape. This affects the current position and dimension.
   * @param {number} dx - The amount to grow (or decrease) in horizontal direction
   * @param {number} dy - The amount to grow (or decrease) in vertical direction
   * @returns {module:AWT.Rectangle}
   */
  grow(dx, dy) {
    this.pos.x -= dx;
    this.pos.y -= dy;
    this.dim.width += 2 * dx;
    this.dim.height += 2 * dy;
    return this;
  }

  /**
   * Gets the {@link module:AWT.Point} corresponding to the lower-right vertex of the Rectangle.
   * @returns {module:AWT.Point}
   */
  getOppositeVertex() {
    return new Point(this.pos.x + this.dim.width, this.pos.y + this.dim.height);
  }

  /**
   * Adds the boundaries of another shape to the current one
   * @param {module:AWT.Shape} shape - The {@link module:AWT.Shape} to be added
   * @returns {module:AWT.Rectangle}
   */
  add(shape) {
    const
      myP2 = this.getOppositeVertex(),
      rectP2 = shape.getBounds().getOppositeVertex();

    this.pos.moveTo(
      Math.min(this.pos.x, shape.getBounds().pos.x),
      Math.min(this.pos.y, shape.getBounds().pos.y));
    this.dim.setDimension(
      Math.max(myP2.x, rectP2.x) - this.pos.x,
      Math.max(myP2.y, rectP2.y) - this.pos.y);
    return this;
  }

  //
  // Inherits the documentation of `contains` in Shape
  contains(p) {
    const p2 = this.getOppositeVertex();
    return p.x >= this.pos.x && p.x <= p2.x && p.y >= this.pos.y && p.y <= p2.y;
  }

  //
  // Inherits the documentation of `intersects` in Shape
  intersects(r) {
    const
      p1 = this.pos, p2 = this.getOppositeVertex(),
      r1 = r.pos, r2 = r.getOppositeVertex();
    return r2.x >= p1.x && r1.x <= p2.x && r2.y >= p1.y && r1.y <= p2.y;
  }

  //
  // Inherits the documentation of `preparePath` in Shape
  preparePath(ctx) {
    ctx.beginPath();
    ctx.rect(this.pos.x, this.pos.y, this.dim.width, this.dim.height);
    return ctx;
  }

  //
  // Inherits the documentation of `getSurface` in Shape
  getSurface() {
    return this.dim.getSurface();
  }

  //
  // Inherits the documentation of `isEmpty` in Shape
  isEmpty() {
    return this.getSurface() === 0;
  }

  //
  // Inherits the documentation of `isRect` in Shape
  isRect() {
    return true;
  }

  //
  // Inherits the documentation of `toString` in Shape
  toString() {
    return `Rectangle ${this.getCoords()}`;
  }

  /**
   * Gets a string with the co-ordinates of the upper-left and lower-right vertexs of this rectangle,
   * (with values rounded to int)
   * @returns {string}
   */
  getCoords() {
    return `[${Math.round(this.pos.x)},${Math.round(this.pos.y)},${Math.round(this.pos.x + this.dim.width)},${Math.round(this.pos.y + this.dim.height)}]`;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return getAttr(this, ['type', 'pos', 'dim']);
  }

  /**
   * Reads the properties of this Rectangle from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:AWT.Rectangle}
   */
  setAttributes(data) {
    return setAttr(this, data, [
      'type',
      { key: 'pos', fn: Point },
      { key: 'dim', fn: Dimension },
    ]);
  }
}

Object.assign(Rectangle.prototype, {
  /**
   * Shape type id
   * @name module:AWT.Rectangle#type
   * @type {string} */
  type: 'rect',
  /**
   * The {@link module:AWT.Dimension Dimension} of the Rectangle
   * @name module:AWT.Rectangle#dim
   * @type {module:AWT.Dimension} */
  dim: new Dimension(),
});

/**
 * The Ellipse shape has the same constructor options as {@link module:AWT.Rectangle Rectangle}
 * @extends module:AWT.Rectangle
 */
export class Ellipse extends Rectangle {
  /**
   * Ellipse constructor
   * @param {Point|Rectangle|number|number[]} pos
   * @param {Dimension|number} [dim]
   * @param {number} [w]
   * @param {number} [h]
   */
  constructor(pos, dim, w, h) {
    super(pos, dim, w, h);
  }

  //
  // Inherits the documentation of `preparePath` in Rectangle
  preparePath(ctx) {

    // Using the solution 'drawEllipseWithBezier' proposed by Steve Tranby in:
    // [http://jsbin.com/sosugenegi/1/edit] as a response to:
    // [http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas]
    // Thanks Steve!!

    const kappa = 0.5522848,
      ox = kappa * this.dim.width / 2, // control point offset horizontal
      oy = kappa * this.dim.height / 2, // control point offset vertical
      xe = this.pos.x + this.dim.width, // x-end
      ye = this.pos.y + this.dim.height, // y-end
      xm = this.pos.x + this.dim.width / 2, // x-middle
      ym = this.pos.y + this.dim.height / 2; // y-middle

    ctx.beginPath();
    ctx.moveTo(this.pos.x, ym);
    ctx.bezierCurveTo(this.pos.x, ym - oy, xm - ox, this.pos.y, xm, this.pos.y);
    ctx.bezierCurveTo(xm + ox, this.pos.y, xe, ym - oy, xe, ym);
    ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
    ctx.bezierCurveTo(xm - ox, ye, this.pos.x, ym + oy, this.pos.x, ym);
    ctx.closePath();
    return ctx;
  }

  //
  // Inherits the documentation of `contains` in Shape
  contains(p) {
    // First check if the point is inside the enclosing rectangle
    let result = super.contains(p);
    if (result) {
      const
        rx = this.dim.width / 2,
        ry = this.dim.height / 2,
        cx = this.pos.x + rx,
        cy = this.pos.y + ry;
      // Apply the general equation of an ellipse
      // See: [http://math.stackexchange.com/questions/76457/check-if-a-point-is-within-an-ellipse]
      // rx and ry are > 0 because we are inside the enclosing rect,
      // so don't care about division by zero
      result = Math.pow(p.x - cx, 2) / Math.pow(rx, 2) + Math.pow(p.y - cy, 2) / Math.pow(ry, 2) <= 1;
    }
    return result;
  }

  //
  // Inherits the documentation of `getSurface` in Rectangle
  getSurface() {
    return Math.PI * this.dim.width / 2 * this.dim.height / 2;
  }

  //
  // Inherits the documentation of `equals` in Rectangle
  equals(e) {
    return e instanceof Ellipse && super.equals(e);
  }

  //
  // Inherits the documentation of `clone` in Rectangle
  clone() {
    return new Ellipse(this.pos, this.dim);
  }

  //
  // Inherits the documentation of `isRect` in Rectangle
  isRect() {
    return false;
  }

  //
  // Inherits the documentation of `toString` in Shape
  toString() {
    return `Ellipse enclosed in ${this.getCoords()}`;
  }
}

Object.assign(Ellipse.prototype, {
  /**
   * Shape type id
   * @name module:AWT.Ellipse#type
   * @type {string} */
  type: 'ellipse',
});

/**
 * A `Path` is a {@link module:AWT.Shape} formed by a serie of strokes, represented by
 * {@link module:AWT.PathStroke} objects
 * @extends module:AWT.Shape
 */
export class Path extends Shape {
  /**
   * Path constructor
   * @param {module:AWT.PathStroke[]} strokes - The array of {@link module:AWT.PathStroke} objects defining this Path.
   */
  constructor(strokes) {
    super();
    // Deep copy of the array of strokes
    if (strokes)
      this.setStrokes(strokes);
  }

  setStrokes(strokes) {
    this.strokes = [];
    // In [Shaper](Shaper.html) objects, strokes have `action` instead of `type` and `data` instead of `points`
    strokes.forEach(str => this.strokes.push(new PathStroke(str.type || str.action, str.points || str.data)));
    // Calculate the enclosing rectangle
    this.enclosing = new Rectangle();
    this.enclosingPoints = [];
    this.calcEnclosingRect();
    this.pos = this.enclosing.pos;
    return this;
  }

  //
  // Inherits the documentation of `clone` in Shape
  clone() {
    return new Path(this.strokes.map(str => str.clone()));
  }

  /**
   * Adds a {@link module:AWT.PathStroke} to `strokes`
   * @param {module:AWT.PathStroke} stroke
   */
  addStroke(stroke) {
    this.strokes.push(stroke);
    return this;
  }

  /**
   * Calculates the polygon and the rectangle that (approximately) encloses this shape
   * @returns {module:AWT.Rectangle}
   */
  calcEnclosingRect() {
    this.enclosingPoints = [];
    let last = new Point();
    this.strokes.forEach(str => {
      str.getEnclosingPoints(last).forEach(pt => {
        last = new Point(pt);
        this.enclosingPoints.push(last);
      });
    });

    let l = this.enclosingPoints.length;
    if (l > 1 && this.enclosingPoints[0].equals(this.enclosingPoints[l - 1])) {
      this.enclosingPoints.pop();
      l--;
    }
    const
      p0 = new Point(this.enclosingPoints[0]),
      p1 = new Point(this.enclosingPoints[0]);

    for (let k = 1; k < l; k++) {
      const p = this.enclosingPoints[k];
      // Check if `p` is at left or above `p0`
      p0.x = Math.min(p.x, p0.x);
      p0.y = Math.min(p.y, p0.y);
      // Check if `p` is at right or below `p1`
      p1.x = Math.max(p.x, p1.x);
      p1.y = Math.max(p.y, p1.y);
    }
    this.enclosing.setBounds(new Rectangle(p0, new Dimension(p0, p1)));
    return this.enclosing;
  }

  //
  // Inherits the documentation of `getBounds` in Shape
  getBounds() {
    return this.enclosing;
  }

  //
  // Inherits the documentation of `moveBy` in Shape
  moveBy(delta) {
    this.strokes.forEach(str => str.moveBy(delta));
    this.enclosingPoints.forEach(pt => pt.moveBy(delta));
    this.enclosing.moveBy(delta);
    return this;
  }

  //
  // Inherits the documentation of `moveTo` in Shape
  moveTo(newPos) {
    return this.moveBy(new Dimension(newPos.x - this.pos.x, newPos.y - this.pos.y));
  }

  //
  // Inherits the documentation of `equals` in Shape
  // TODO: Implement comparision of complex paths
  equals(_p) {
    return false;
  }

  //
  // Inherits the documentation of `scaleBy` in Shape
  scaleBy(delta) {
    this.strokes.forEach(str => str.multBy(delta));
    this.enclosingPoints.forEach(pt => pt.multBy(delta));
    this.enclosing.scaleBy(delta);
    return this;
  }

  //
  // Inherits the documentation of `contains` in Shape
  contains(p) {
    let result = this.enclosing.contains(p);
    if (result) {
      // Let's see if the point really lies inside the polygon formed by enclosingPoints
      // Using the "Ray casting algorithm" described in [https://en.wikipedia.org/wiki/Point_in_polygon]
      const N = this.enclosingPoints.length;
      let
        xinters = 0,
        counter = 0,
        p1 = this.enclosingPoints[0];

      for (let i = 1; i <= N; i++) {
        const p2 = this.enclosingPoints[i % N];
        if (p.y > Math.min(p1.y, p2.y)) {
          if (p.y <= Math.max(p1.y, p2.y)) {
            if (p.x <= Math.max(p1.x, p2.x)) {
              if (p1.y !== p2.y) {
                xinters = (p.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y) + p1.x;
                if (p1.x === p2.x || p.x <= xinters)
                  counter++;
              }
            }
          }
        }
        p1 = p2;
      }
      if (counter % 2 === 0)
        result = false;
    }
    return result;
  }

  //
  // Inherits the documentation of `intersects` in Shape
  // TODO: Implement a check algorithm based on the real shape
  intersects(r) {
    return this.enclosing.intersects(r);
  }

  //
  // Inherits the documentation of `preparePath` in Shape
  preparePath(ctx) {
    // TODO: Implement filling paths
    ctx.beginPath();
    this.strokes.forEach(str => str.stroke(ctx));
    return ctx;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return {
      type: this.type,
      strokes: this.strokes.map(s => s.getAttributes()).join('|'),
    };
  }

  /**
   * Reads the properties of this Path from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:AWT.Path}
   */
  setAttributes(data) {
    const strData = data.strokes.split('|');
    const strokes = strData.map(s => {
      const [type, points] = s.split(':');
      return new PathStroke(type, points ? points.split(',') : []);
    });
    return this.setStrokes(strokes);
  }
}

Object.assign(Path.prototype, {
  /**
   * Shape type id
   * @name module:AWT.Path#type
   * @type {string} */
  type: 'path',
  /**
   * The strokes forming this Path.
   * @name module:AWT.Path#strokes
   * @type {module:AWT.PathStroke[]} */
  strokes: [],
  /**
   * The {@link module:AWT.Rectangle Rectangle} enclosing this Path (when drawing, this Rectangle don't include border width!)
   * @name module:AWT.Path#enclosing
   * @type {module:AWT.Rectangle} */
  enclosing: new Rectangle(),
  /**
   * Set of vertexs of a polygon close to the real path of this shape
   * @name module:AWT.Path#enclosingPoints
   * @type {module:AWT.Point[]} */
  enclosingPoints: [],
});

/**
 * PathStroke is the basic component of {@link module:AWT.Path} objects
 */
export class PathStroke {
  /**
   * PathStroke constructor
   * @param {string} type - The type of stroke. Possible values are: `M` (move to), `L` (line to),
   * `Q` (quadratic to), `B` (bezier to) and `X` (close path).
   * @param {module:AWT.Point[]} points - The array of {@link module:AWT.Point} objects used in this Stroke.
   */
  constructor(type, points) {
    this.type = type;
    // Points are deep cloned, to avoid change the original values
    if (points && points.length > 0) {
      // Check if 'points' is an array of objects of type 'Point'
      if (points[0] instanceof Point)
        this.points = points.map(p => new Point(p));
      // otherwise assume that 'points' contains just numbers
      // to be readed in pairs of x and y co-ordinates
      else {
        this.points = [];
        for (let i = 0; i < points.length; i += 2)
          this.points.push(new Point(points[i], points[i + 1]));
      }
    }
  }

  /**
   * Calculates some of the points included in a quadratic Bézier curve
   * The number of points being calculated is defined in Utils.settings.BEZIER_POINTS
   * @see {@link https://en.wikipedia.org/wiki/B%C3%A9zier_curve}
   * @see {@link https://www.jasondavies.com/animated-bezier/}
   *
   * @param {module:AWT.Point} p0 - Starting point of the quadratic Bézier curve
   * @param {module:AWT.Point} p1 - Control point
   * @param {module:AWT.Point} p2 - Ending point
   * @param {number} [numPoints] - The number of intermediate points to calculate. When not defined,
   * the value will be obtained from {@link module:Utils.settings.BEZIER_POINTS}.
   * @returns {module:AWT.Point[]} - Array with some intermediate points from the resulting Bézier curve
   */
  static getQuadraticPoints(p0, p1, p2, numPoints) {
    if (!numPoints)
      numPoints = settings.BEZIER_POINTS;
    const
      result = [],
      pxa = new Point(),
      pxb = new Point();
    for (let i = 0; i < numPoints; i++) {
      const n = (i + 1) / (numPoints + 1);
      pxa.x = p0.x + (p1.x - p0.x) * n;
      pxa.y = p0.y - (p0.y - p1.y) * n;
      pxb.x = p1.x + (p2.x - p1.x) * n;
      pxb.y = p1.y + (p2.y - p1.y) * n;
      result.push(new Point(pxa.x + (pxb.x - pxa.x) * n, pxa.y - (pxa.y - pxb.y) * n));
    }
    return result;
  }

  /**
   * Calculates some of the points included in a cubic Bézier (curve with two control points)
   * The number of points being calculated is defined in Utils.settings.BEZIER_POINTS
   * @param {module:AWT.Point} p0 - Starting point of the cubic Bézier curve
   * @param {module:AWT.Point} p1 - First control point
   * @param {module:AWT.Point} p2 - Second control point
   * @param {module:AWT.Point} p3 - Ending point
   * @param {number} [numPoints] - The number of intermediate points to calculate. When not defined,
   * the value will be obtained from {@link module:Utils.settings.BEZIER_POINTS}.
   * @returns {module:AWT.Point[]} - Array with some intermediate points from the resulting Bézier curve
   */
  static getCubicPoints(p0, p1, p2, p3, numPoints) {
    const result = [];
    if (!numPoints)
      numPoints = settings.BEZIER_POINTS;
    const pr = PathStroke.getQuadraticPoints(p0, p1, p2, numPoints);
    const pq = PathStroke.getQuadraticPoints(p1, p2, p3, numPoints);
    for (let i = 0; i < numPoints; i++) {
      const n = (i + 1) / (numPoints + 1);
      result.push(new Point(pr[i].x + (pq[i].x - pr[i].x) * n, pr[i].y - (pr[0].y - pq[0].y) * n));
    }
    return result;
  }

  /**
   * Clones this PathStroke
   * @returns {module:AWT.PathStroke}
   */
  clone() {
    // The constructors of PathStroke always make a deep copy of the `points` array
    return new PathStroke(this.type, this.points);
  }

  /**
   * Increments or decrements by `delta` the x and y coordinates of all points
   * @param {Point|Dimension} delta - The amount to add to the `x` and `y`
   * coordinates of each point.
   */
  moveBy(delta) {
    if (this.points)
      this.points.forEach(pt => pt.moveBy(delta));
    return this;
  }

  /**
   * Multiplies each point coordinates by the `x` and `y` (or `w` and `h`) values of the
   * passed {@link module:AWT.Point} or {@link module:AWT.Dimension Dimension}.
   * @param {Point|Dimension} delta
   */
  multBy(delta) {
    if (this.points)
      this.points.forEach(pt => pt.multBy(delta));
    return this;
  }

  /**
   * Draws this PathStroke in the provided HTML canvas context
   * @param {external:CanvasRenderingContext2D} ctx - The HTML canvas 2D rendering context
   */
  stroke(ctx) {
    switch (this.type) {
      case 'M':
        ctx.moveTo(this.points[0].x, this.points[0].y);
        break;
      case 'L':
        ctx.lineTo(this.points[0].x, this.points[0].y);
        break;
      case 'Q':
        ctx.quadraticCurveTo(
          this.points[0].x, this.points[0].y,
          this.points[1].x, this.points[1].y);
        break;
      case 'B':
        ctx.bezierCurveTo(
          this.points[0].x, this.points[0].y,
          this.points[1].x, this.points[1].y,
          this.points[2].x, this.points[2].y);
        break;
      case 'X':
        ctx.closePath();
        break;
    }
    return ctx;
  }

  /**
   * Gets the set of points that will be included as a vertexs on the owner's shape
   * enclosing polygon.
   * @param {module:AWT.Point} from - The starting point for this stroke
   * @returns {module:AWT.Point[]}
   */
  getEnclosingPoints(from) {
    let result = [];
    switch (this.type) {
      case 'M':
      case 'L':
        result.push(this.points[0]);
        break;
      case 'Q':
        result = PathStroke.getQuadraticPoints(from, this.points[0], this.points[1]);
        result.push(this.points[1]);
        break;
      case 'B':
        result = PathStroke.getCubicPoints(from, this.points[0], this.points[1], this.points[2]);
        result.push(this.points[2]);
        break;
    }
    return result;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return `${this.type}:${this.points ? this.points.map(p => `${fx(p.x)},${fx(p.y)}`).join(',') : ''}`;
  }
}

Object.assign(PathStroke.prototype, {
  /**
   * The Stroke type. Possible values are: `M` (move to), `L` (line to), `Q` (quadratic to),
   * `B` (bezier to) and `X` (close path).
   * @name module:AWT.PathStroke#type
   * @type {string} */
  type: 'X',
  /**
   * The array of points used by this stroke. Can be `null`.
   * @name module:AWT.PathStroke#points
   * @type {module:AWT.Point[]} */
  points: null,
});

/**
 * This class encapsulates actions that can be linked to buttons, menus and other active objects
 */
export class Action {
  /**
   * Action constructor
   * @param {string} name - The name of this Action
   * @param {function} actionPerformed - The callback to be triggered by this Action
   */
  constructor(name, actionPerformed) {
    this.name = name;
    this.actionPerformed = actionPerformed;
    this._statusListeners = [];
  }

  /**
   * Here is where subclasses must define the callback to be triggered when
   * this Action object is called
   * @param {module:AWT.Action} _thisAction - Pointer to this Action object
   * @param {object} _event - The original action event that has originated this action
   */
  actionPerformed(_thisAction, _event) {
    return this;
  }

  /**
   * This is the method to be passed to DOM event triggers
   * @example
   * const myFunc = () => { alert('Hello!') }
   * const myAction = new Action('hello', myFunc)
   * $( "#foo" ).bind( "click", myAction.processEvent)
   * @param {object} event - The event object passed by the DOM event trigger
   */
  processEvent(event) {
    return this.actionPerformed(this, event);
  }

  /**
   * Adds a status listener
   * @param {function} listener - The callback method to be called when the status of this
   * Action changes
   */
  addStatusListener(listener) {
    this._statusListeners.push(listener);
  }

  /**
   * Removes a previously registered status listener
   * @param {function} listener - The listener to be removed
   */
  removeStatusListener(listener) {
    this._statusListeners = this._statusListeners.map(l => l !== listener);
  }

  /**
   * Enables or disables this action
   * @param {boolean} enabled
   */
  setEnabled(enabled) {
    this.enabled = enabled;
    this._statusListeners.forEach(listener => listener.call(this, this));
    return this;
  }
}

Object.assign(Action.prototype, {
  /**
   * The action's name
   * @name module:AWT.Action#name
   * @type {string} */
  name: null,
  /**
   * An optional description
   * @name module:AWT.Action#description
   * @type {string} */
  description: null,
  /**
   * Action status. `true` means enabled, `false` disabled
   * @name module:AWT.Action#enabled
   * @type {boolean} */
  enabled: false,
  /**
   * Array of callback functions to be triggered when the `enabled` flag changes
   * @name module:AWT.Action#_statusListeners
   * @private
   * @type {function[]} */
  _statusListeners: null,
});

/**
 * This class provides a timer that will launch a function at specific intervals
 */
export class Timer {
  /**
   * Timer constructor
   * @param {function} actionPerformed - The function to be triggered when the timer is enabled.
   * @param {number} interval - The interval between action calls, specified in milliseconds.
   * @param {boolean} [enabled=false] - Flag to indicate if the timer will be initially enabled.
   */
  constructor(actionPerformed, interval, enabled) {
    this.actionPerformed = actionPerformed;
    this.interval = interval;
    this.setEnabled(enabled === true);
  }

  /**
   * Here is where subclasses must define the function to be performed when this timer ticks.
   * @param {module:AWT.Timer} _thisTimer
   */
  actionPerformed(_thisTimer) {
    return this;
  }

  /**
   * This is the method called by `window.setInterval`
   * @param {external:Event} _event
   */
  processTimer(_event) {
    this.ticks++;
    if (!this.repeats)
      this.stop();
    return this.actionPerformed.call(this);
  }

  /**
   * Enables or disables this timer
   * @param {boolean} enabled - Indicates if the timer should be enabled or disabled
   * @param {boolean} [retainCounter=false] - When `true`, the ticks counter will not be cleared
   */
  setEnabled(enabled, retainCounter) {
    if (!retainCounter)
      this.ticks = 0;
    if (enabled && this.timer !== null) {
      // Timer already running
      return;
    }

    if (enabled) {
      this.timer = window.setInterval(() => this.processTimer(null), this.interval);
    } else {
      if (this.timer !== null) {
        window.clearInterval(this.timer);
        this.timer = null;
      }
    }
    return this;
  }

  /**
   * Checks if this timer is running
   * @returns {boolean}
   */
  isRunning() {
    return this.timer !== null;
  }

  /**
   * Starts this timer
   * @param {boolean} [retainCounter=false] - When `true`, the ticks counter will not be cleared
   */
  start(retainCounter) {
    return this.setEnabled(true, retainCounter);
  }

  /**
   * Stops this timer
   * @param {boolean} [retainCounter=false] - When `true`, the ticks counter will not be cleared
   */
  stop(retainCounter) {
    return this.setEnabled(false, retainCounter);
  }
}

Object.assign(Timer.prototype, {
  /**
   * The timer interval, in milliseconds
   * @name module:AWT.Timer#interval
   * @type {number} */
  interval: 0,
  /**
   * The ticks counter
   * @name module:AWT.Timer#ticks
   * @type {number} */
  ticks: 0,
  /**
   * The object returned by `window.setInterval`
   * @name module:AWT.Timer#timer
   * @type {object} */
  timer: null,
  /**
   * When `true`, the timer should repeat until `stop` is called
   * @name module:AWT.Timer#repeats
   * @type {boolean} */
  repeats: true,
});

/**
 * Logic object that takes care of an "invalidated" rectangle that will be repainted
 * at the next update of a 2D object, usually an HTML Canvas.
 * Container has the same constructor options as {@link module:AWT.Rectangle Rectangle}
 * @extends module:AWT.Rectangle
 */
export class Container extends Rectangle {
  /**
   * Container constructor
   * @param {Point|Rectangle|number|number[]} pos
   * @param {Dimension|number} [dim]
   * @param {number} [w]
   * @param {number} [h]
   */
  constructor(pos, dim, w, h) {
    super(pos, dim, w, h);
  }

  /**
   * Adds the provided rectangle to the invalidated area.
   * @param {module:AWT.Rectangle} rect
   */
  invalidate(rect) {
    if (rect) {
      if (this.invalidatedRect === null)
        this.invalidatedRect = rect.clone();
      else
        this.invalidatedRect.add(rect);
    } else
      this.invalidatedRect = null;
    return this;
  }

  /**
   * Updates the invalidated area
   */
  update() {
    this.updateContent(this.invalidatedRect);
    this.invalidatedRect = null;
    return this;
  }

  /**
   * Containers should implement this method to update its graphic contents. It should
   * be called from {@link module:AWT.Container#update}
   * @param {module:AWT.Shape} _dirtyRegion - Specifies the area to be updated. When `null`, it's the whole
   * Container.
   */
  updateContent(_dirtyRegion) {
    // To be overrided by subclasses. Here does nothing.
    return this;
  }
}

Object.assign(Container.prototype, {
  /**
   * The currently "invalidated" area
   * @name module:AWT.Container#invalidatedRect
   * @type {module:AWT.Rectangle} */
  invalidatedRect: null,
});

/**
 * This object contains utility clases for painting graphics and images,
 * as found in the Java [Abstract Window Toolkit](http://docs.oracle.com/javase/7/docs/api/java/awt/package-summary.html)
 *
 * The objects defined here are: {@link module:AWT.Font Font}, {@link module:AWT.Gradient Gradient}, {@link module:AWT.Stroke Stroke},
 * {@link module:AWT.Point Point}, {@link module:AWT.Dimension Dimension}, {@link module:AWT.Shape Shape}, {@link module:AWT.Rectangle Rectangle},
 * {@link module:AWT.Ellipse Ellipse}, {@link module:AWT.Path Path}, {@link module:AWT.PathStroke PathStroke}, {@link module:AWT.Action Action},
 * {@link module:AWT.Timer Timer} and {@link module:AWT.Container Container}.
 */
export default {
  Font,
  Gradient,
  Stroke,
  Point,
  Dimension,
  Shape,
  Rectangle,
  Ellipse,
  Path,
  PathStroke,
  Action,
  Timer,
  Container
};