text: /**
   * @author NHN Ent. FE Development Team <dl_javascript@nhn.com>
   * @fileoverview Graphics module
   */
  import snippet from 'tui-code-snippet';
  import fabric from 'fabric';
  import ImageLoader from './component/imageLoader';
  import Cropper from './component/cropper';
  import Flip from './component/flip';
  import Rotation from './component/rotation';
  import FreeDrawing from './component/freeDrawing';
  import Line from './component/line';
  import Text from './component/text';
  import Icon from './component/icon';
  import Filter from './component/filter';
  import Shape from './component/shape';
  import CropperDrawingMode from './drawingMode/cropper';
  import FreeDrawingMode from './drawingMode/freeDrawing';
  import LineDrawingMode from './drawingMode/lineDrawing';
  import ShapeDrawingMode from './drawingMode/shape';
  import TextDrawingMode from './drawingMode/text';
  import {getProperties, includes, isShape, Promise} from './util';
  import {componentNames as components, eventNames as events, drawingModes, fObjectOptions} from './consts';
  
  const {extend, stamp, isArray, isString, forEachArray, forEachOwnProperties, CustomEvents} = snippet;
  const DEFAULT_CSS_MAX_WIDTH = 1000;
  const DEFAULT_CSS_MAX_HEIGHT = 800;
  const EXTRA_PX_FOR_PASTE = 10;
  
  const cssOnly = {
      cssOnly: true
  };
  const backstoreOnly = {
      backstoreOnly: true
  };
  
  /**
   * Graphics class
   * @class
   * @param {string|HTMLElement} wrapper - Wrapper's element or selector
   * @param {Object} [option] - Canvas max width & height of css
   *  @param {number} option.cssMaxWidth - Canvas css-max-width
   *  @param {number} option.cssMaxHeight - Canvas css-max-height
   *  @param {boolean} option.useDragAddIcon - Use dragable add in icon mode
   * @ignore
   */
  class Graphics {
      constructor(element, {
          cssMaxWidth,
          cssMaxHeight,
          useDragAddIcon = false
      } = {}) {
          /**
           * Fabric image instance
           * @type {fabric.Image}
           */
          this.canvasImage = null;
  
          /**
           * Max width of canvas elements
           * @type {number}
           */
          this.cssMaxWidth = cssMaxWidth || DEFAULT_CSS_MAX_WIDTH;
  
          /**
           * Max height of canvas elements
           * @type {number}
           */
          this.cssMaxHeight = cssMaxHeight || DEFAULT_CSS_MAX_HEIGHT;
  
          /**
           * Use add drag icon mode for icon component
           * @type {boolean}
           */
          this.useDragAddIcon = useDragAddIcon;
  
          /**
           * cropper Selection Style
           * @type {Object}
           */
          this.cropSelectionStyle = {};
  
          /**
           * target fabric object for copy paste feature
           * @type {fabric.Object}
           * @private
           */
          this.targetObjectForCopyPaste = null;
  
          /**
           * Image name
           * @type {string}
           */
          this.imageName = '';
  
          /**
           * Object Map
           * @type {Object}
           * @private
           */
          this._objects = {};
  
          /**
           * Fabric-Canvas instance
           * @type {fabric.Canvas}
           * @private
           */
          this._canvas = null;
  
          /**
           * Drawing mode
           * @type {string}
           * @private
           */
          this._drawingMode = drawingModes.NORMAL;
  
          /**
           * DrawingMode map
           * @type {Object.<string, DrawingMode>}
           * @private
           */
          this._drawingModeMap = {};
  
          /**
           * Component map
           * @type {Object.<string, Component>}
           * @private
           */
          this._componentMap = {};
  
          /**
           * fabric event handlers
           * @type {Object.<string, function>}
           * @private
           */
          this._handler = {
              onMouseDown: this._onMouseDown.bind(this),
              onObjectAdded: this._onObjectAdded.bind(this),
              onObjectRemoved: this._onObjectRemoved.bind(this),
              onObjectMoved: this._onObjectMoved.bind(this),
              onObjectScaled: this._onObjectScaled.bind(this),
              onObjectModified: this._onObjectModified.bind(this),
              onObjectRotated: this._onObjectRotated.bind(this),
              onObjectSelected: this._onObjectSelected.bind(this),
              onPathCreated: this._onPathCreated.bind(this),
              onSelectionCleared: this._onSelectionCleared.bind(this),
              onSelectionCreated: this._onSelectionCreated.bind(this)
          };
  
          this._setObjectCachingToFalse();
          this._setCanvasElement(element);
          this._createDrawingModeInstances();
          this._createComponents();
          this._attachCanvasEvents();
      }
  
      /**
       * Destroy canvas element
       */
      destroy() {
          const {wrapperEl} = this._canvas;
  
          this._canvas.clear();
  
          wrapperEl.parentNode.removeChild(wrapperEl);
      }
  
      /**
       * Deactivates all objects on canvas
       * @returns {Graphics} this
       */
      deactivateAll() {
          this._canvas.discardActiveObject();
  
          return this;
      }
  
      /**
       * Renders all objects on canvas
       * @returns {Graphics} this
       */
      renderAll() {
          this._canvas.renderAll();
  
          return this;
      }
  
      /**
       * Adds objects on canvas
       * @param {Object|Array} objects - objects
       */
      add(objects) {
          let theArgs = [];
          if (isArray(objects)) {
              theArgs = objects;
          } else {
              theArgs.push(objects);
          }
  
          this._canvas.add(...theArgs);
      }
  
      /**
       * Removes the object or group
       * @param {Object} target - graphics object or group
       * @returns {boolean} true if contains or false
       */
      contains(target) {
          return this._canvas.contains(target);
      }
  
      /**
       * Gets all objects or group
       * @returns {Array} all objects, shallow copy
       */
      getObjects() {
          return this._canvas.getObjects().slice();
      }
  
      /**
       * Get an object by id
       * @param {number} id - object id
       * @returns {fabric.Object} object corresponding id
       */
      getObject(id) {
          return this._objects[id];
      }
  
      /**
       * Removes the object or group
       * @param {Object} target - graphics object or group
       */
      remove(target) {
          this._canvas.remove(target);
      }
  
      /**
       * Removes all object or group
       * @param {boolean} includesBackground - remove the background image or not
       * @returns {Array} all objects array which is removed
       */
      removeAll(includesBackground) {
          const canvas = this._canvas;
          const objects = canvas.getObjects().slice();
          canvas.remove(...this._canvas.getObjects());
  
          if (includesBackground) {
              canvas.clear();
          }
  
          return objects;
      }
  
      /**
       * Removes an object or group by id
       * @param {number} id - object id
       * @returns {Array} removed objects
       */
      removeObjectById(id) {
          const objects = [];
          const canvas = this._canvas;
          const target = this.getObject(id);
          const isValidGroup = target && target.isType('group') && !target.isEmpty();
  
          if (isValidGroup) {
              canvas.discardActiveObject(); // restore states for each objects
              target.forEachObject(obj => {
                  objects.push(obj);
                  canvas.remove(obj);
              });
          } else if (canvas.contains(target)) {
              objects.push(target);
              canvas.remove(target);
          }
  
          return objects;
      }
  
      /**
       * Get an id by object instance
       * @param {fabric.Object} object object
       * @returns {number} object id if it exists or null
       */
      getObjectId(object) {
          let key = null;
          for (key in this._objects) {
              if (this._objects.hasOwnProperty(key)) {
                  if (object === this._objects[key]) {
                      return key;
                  }
              }
          }
  
          return null;
      }
  
      /**
       * Gets an active object or group
       * @returns {Object} active object or group instance
       */
      getActiveObject() {
          return this._canvas._activeObject;
      }
  
      /**
       * Returns the object ID to delete the object.
       * @returns {number} object id for remove
       */
      getActiveObjectIdForRemove() {
          const activeObject = this.getActiveObject();
          const {type, left, top} = activeObject;
          const isSelection = type === 'activeSelection';
  
          if (isSelection) {
              const group = new fabric.Group([...activeObject.getObjects()], {
                  left,
                  top
              });
  
              return this._addFabricObject(group);
          }
  
          return this.getObjectId(activeObject);
      }
  
      /**
       * Verify that you are ready to erase the object.
       * @returns {boolean} ready for object remove
       */
      isReadyRemoveObject() {
          const activeObject = this.getActiveObject();
  
          return activeObject && !activeObject.isEditing;
      }
  
      /**
       * Gets an active group object
       * @returns {Object} active group object instance
       */
      getActiveObjects() {
          const activeObject = this._canvas._activeObject;
  
          return activeObject && activeObject.type === 'activeSelection' ? activeObject : null;
      }
  
      /**
       * Get Active object Selection from object ids
       * @param {Array.<Object>} objects - fabric objects
       * @returns {Object} target - target object group
       */
      getActiveSelectionFromObjects(objects) {
          const canvas = this.getCanvas();
  
          return new fabric.ActiveSelection(objects, {canvas});
      }
  
      /**
       * Activates an object or group
       * @param {Object} target - target object or group
       */
      setActiveObject(target) {
          this._canvas.setActiveObject(target);
      }
  
      /**
       * Set Crop selection style
       * @param {Object} style - Selection styles
       */
      setCropSelectionStyle(style) {
          this.cropSelectionStyle = style;
      }
  
      /**
       * Get component
       * @param {string} name - Component name
       * @returns {Component}
       */
      getComponent(name) {
          return this._componentMap[name];
      }
  
      /**
       * Get current drawing mode
       * @returns {string}
       */
      getDrawingMode() {
          return this._drawingMode;
      }
  
      /**
       * Start a drawing mode. If the current mode is not 'NORMAL', 'stopDrawingMode()' will be called first.
       * @param {String} mode Can be one of <I>'CROPPER', 'FREE_DRAWING', 'LINE', 'TEXT', 'SHAPE'</I>
       * @param {Object} [option] parameters of drawing mode, it's available with 'FREE_DRAWING', 'LINE_DRAWING'
       *  @param {Number} [option.width] brush width
       *  @param {String} [option.color] brush color
       * @returns {boolean} true if success or false
       */
      startDrawingMode(mode, option) {
          if (this._isSameDrawingMode(mode)) {
              return true;
          }
  
          // If the current mode is not 'NORMAL', 'stopDrawingMode()' will be called first.
          this.stopDrawingMode();
  
          const drawingModeInstance = this._getDrawingModeInstance(mode);
          if (drawingModeInstance && drawingModeInstance.start) {
              drawingModeInstance.start(this, option);
  
              this._drawingMode = mode;
          }
  
          return !!drawingModeInstance;
      }
  
      /**
       * Stop the current drawing mode and back to the 'NORMAL' mode
       */
      stopDrawingMode() {
          if (this._isSameDrawingMode(drawingModes.NORMAL)) {
              return;
          }
  
          const drawingModeInstance = this._getDrawingModeInstance(this.getDrawingMode());
          if (drawingModeInstance && drawingModeInstance.end) {
              drawingModeInstance.end(this);
          }
          this._drawingMode = drawingModes.NORMAL;
      }
  
      /**
       * To data url from canvas
       * @param {Object} options - options for toDataURL
       *   @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png"
       *   @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg.
       *   @param {Number} [options.multiplier=1] Multiplier to scale by
       *   @param {Number} [options.left] Cropping left offset. Introduced in fabric v1.2.14
       *   @param {Number} [options.top] Cropping top offset. Introduced in fabric v1.2.14
       *   @param {Number} [options.width] Cropping width. Introduced in fabric v1.2.14
       *   @param {Number} [options.height] Cropping height. Introduced in fabric v1.2.14
       * @returns {string} A DOMString containing the requested data URI.
       */
      toDataURL(options) {
          const cropper = this.getComponent(components.CROPPER);
          cropper.changeVisibility(false);
  
          const dataUrl = this._canvas && this._canvas.toDataURL(options);
          cropper.changeVisibility(true);
  
          return dataUrl;
      }
  
      /**
       * Save image(background) of canvas
       * @param {string} name - Name of image
       * @param {?fabric.Image} canvasImage - Fabric image instance
       */
      setCanvasImage(name, canvasImage) {
          if (canvasImage) {
              stamp(canvasImage);
          }
          this.imageName = name;
          this.canvasImage = canvasImage;
      }
  
      /**
       * Set css max dimension
       * @param {{width: number, height: number}} maxDimension - Max width & Max height
       */
      setCssMaxDimension(maxDimension) {
          this.cssMaxWidth = maxDimension.width || this.cssMaxWidth;
          this.cssMaxHeight = maxDimension.height || this.cssMaxHeight;
      }
  
      /**
       * Adjust canvas dimension with scaling image
       */
      adjustCanvasDimension() {
          const canvasImage = this.canvasImage.scale(1);
          const {width, height} = canvasImage.getBoundingRect();
          const maxDimension = this._calcMaxDimension(width, height);
  
          this.setCanvasCssDimension({
              width: '100%',
              height: '100%', // Set height '' for IE9
              'max-width': `${maxDimension.width}px`,
              'max-height': `${maxDimension.height}px`
          });
  
          this.setCanvasBackstoreDimension({
              width,
              height
          });
          this._canvas.centerObject(canvasImage);
      }
  
      /**
       * Set canvas dimension - css only
       *  {@link http://fabricjs.com/docs/fabric.Canvas.html#setDimensions}
       * @param {Object} dimension - Canvas css dimension
       */
      setCanvasCssDimension(dimension) {
          this._canvas.setDimensions(dimension, cssOnly);
      }
  
      /**
       * Set canvas dimension - backstore only
       *  {@link http://fabricjs.com/docs/fabric.Canvas.html#setDimensions}
       * @param {Object} dimension - Canvas backstore dimension
       */
      setCanvasBackstoreDimension(dimension) {
          this._canvas.setDimensions(dimension, backstoreOnly);
      }
  
      /**
       * Set image properties
       * {@link http://fabricjs.com/docs/fabric.Image.html#set}
       * @param {Object} setting - Image properties
       * @param {boolean} [withRendering] - If true, The changed image will be reflected in the canvas
       */
      setImageProperties(setting, withRendering) {
          const {canvasImage} = this;
  
          if (!canvasImage) {
              return;
          }
  
          canvasImage.set(setting).setCoords();
          if (withRendering) {
              this._canvas.renderAll();
          }
      }
  
      /**
       * Returns canvas element of fabric.Canvas[[lower-canvas]]
       * @returns {HTMLCanvasElement}
       */
      getCanvasElement() {
          return this._canvas.getElement();
      }
  
      /**
       * Get fabric.Canvas instance
       * @returns {fabric.Canvas}
       * @private
       */
      getCanvas() {
          return this._canvas;
      }
  
      /**
       * Get canvasImage (fabric.Image instance)
       * @returns {fabric.Image}
       */
      getCanvasImage() {
          return this.canvasImage;
      }
  
      /**
       * Get image name
       * @returns {string}
       */
      getImageName() {
          return this.imageName;
      }
  
      /**
       * Add image object on canvas
       * @param {string} imgUrl - Image url to make object
       * @returns {Promise}
       */
      addImageObject(imgUrl) {
          const callback = this._callbackAfterLoadingImageObject.bind(this);
  
          return new Promise(resolve => {
              fabric.Image.fromURL(imgUrl, image => {
                  callback(image);
                  resolve(this.createObjectProperties(image));
              }, {
                  crossOrigin: 'Anonymous'
              }
              );
          });
      }
  
      /**
       * Get center position of canvas
       * @returns {Object} {left, top}
       */
      getCenter() {
          return this._canvas.getCenter();
      }
  
      /**
       * Get cropped rect
       * @returns {Object} rect
       */
      getCropzoneRect() {
          return this.getComponent(components.CROPPER).getCropzoneRect();
      }
  
      /**
       * Get cropped rect
       * @param {number} [mode] cropzone rect mode
       */
      setCropzoneRect(mode) {
          this.getComponent(components.CROPPER).setCropzoneRect(mode);
      }
  
      /**
       * Get cropped image data
       * @param {Object} cropRect cropzone rect
       *  @param {Number} cropRect.left left position
       *  @param {Number} cropRect.top top position
       *  @param {Number} cropRect.width width
       *  @param {Number} cropRect.height height
       * @returns {?{imageName: string, url: string}} cropped Image data
       */
      getCroppedImageData(cropRect) {
          return this.getComponent(components.CROPPER).getCroppedImageData(cropRect);
      }
  
      /**
       * Set brush option
       * @param {Object} option brush option
       *  @param {Number} option.width width
       *  @param {String} option.color color like 'FFFFFF', 'rgba(0, 0, 0, 0.5)'
       */
      setBrush(option) {
          const drawingMode = this._drawingMode;
          let compName = components.FREE_DRAWING;
  
          if (drawingMode === drawingModes.LINE_DRAWING) {
              compName = components.LINE;
          }
  
          this.getComponent(compName).setBrush(option);
      }
  
      /**
       * Set states of current drawing shape
       * @param {string} type - Shape type (ex: 'rect', 'circle', 'triangle')
       * @param {Object} [options] - Shape options
       *      @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or 
       *        Shape foreground color (ex: '#fff', 'transparent')
       *      @param {string} [options.stoke] - Shape outline color
       *      @param {number} [options.strokeWidth] - Shape outline width
       *      @param {number} [options.width] - Width value (When type option is 'rect', this options can use)
       *      @param {number} [options.height] - Height value (When type option is 'rect', this options can use)
       *      @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use)
       *      @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use)
       *      @param {number} [options.isRegular] - Whether resizing shape has 1:1 ratio or not
       */
      setDrawingShape(type, options) {
          this.getComponent(components.SHAPE).setStates(type, options);
      }
  
      /**
       * Register icon paths
       * @param {Object} pathInfos - Path infos
       *  @param {string} pathInfos.key - key
       *  @param {string} pathInfos.value - value
       */
      registerPaths(pathInfos) {
          this.getComponent(components.ICON).registerPaths(pathInfos);
      }
  
      /**
       * Change cursor style
       * @param {string} cursorType - cursor type
       */
      changeCursor(cursorType) {
          const canvas = this.getCanvas();
          canvas.defaultCursor = cursorType;
          canvas.renderAll();
      }
  
      /**
       * Whether it has the filter or not
       * @param {string} type - Filter type
       * @returns {boolean} true if it has the filter
       */
      hasFilter(type) {
          return this.getComponent(components.FILTER).hasFilter(type);
      }
  
      /**
       * Set selection style of fabric object by init option
       * @param {Object} styles - Selection styles
       */
      setSelectionStyle(styles) {
          extend(fObjectOptions.SELECTION_STYLE, styles);
      }
  
      /**
       * Set object properties
       * @param {number} id - object id
       * @param {Object} props - props
       *     @param {string} [props.fill] Color
       *     @param {string} [props.fontFamily] Font type for text
       *     @param {number} [props.fontSize] Size
       *     @param {string} [props.fontStyle] Type of inclination (normal / italic)
       *     @param {string} [props.fontWeight] Type of thicker or thinner looking (normal / bold)
       *     @param {string} [props.textAlign] Type of text align (left / center / right)
       *     @param {string} [props.textDecoration] Type of line (underline / line-through / overline)
       * @returns {Object} applied properties
       */
      setObjectProperties(id, props) {
          const object = this.getObject(id);
          const clone = extend({}, props);
  
          object.set(clone);
  
          object.setCoords();
  
          this.getCanvas().renderAll();
  
          return clone;
      }
  
      /**
       * Get object properties corresponding key
       * @param {number} id - object id
       * @param {Array<string>|ObjectProps|string} keys - property's key
       * @returns {Object} properties
       */
      getObjectProperties(id, keys) {
          const object = this.getObject(id);
          const props = {};
  
          if (isString(keys)) {
              props[keys] = object[keys];
          } else if (isArray(keys)) {
              forEachArray(keys, value => {
                  props[value] = object[value];
              });
          } else {
              forEachOwnProperties(keys, (value, key) => {
                  props[key] = object[key];
              });
          }
  
          return props;
      }
  
      /**
       * Get object position by originX, originY
       * @param {number} id - object id
       * @param {string} originX - can be 'left', 'center', 'right'
       * @param {string} originY - can be 'top', 'center', 'bottom'
       * @returns {Object} {{x:number, y: number}} position by origin if id is valid, or null
       */
      getObjectPosition(id, originX, originY) {
          const targetObj = this.getObject(id);
          if (!targetObj) {
              return null;
          }
  
          return targetObj.getPointByOrigin(originX, originY);
      }
  
      /**
       * Set object position  by originX, originY
       * @param {number} id - object id
       * @param {Object} posInfo - position object
       *  @param {number} posInfo.x - x position
       *  @param {number} posInfo.y - y position
       *  @param {string} posInfo.originX - can be 'left', 'center', 'right'
       *  @param {string} posInfo.originY - can be 'top', 'center', 'bottom'
       * @returns {boolean} true if target id is valid or false
       */
      setObjectPosition(id, posInfo) {
          const targetObj = this.getObject(id);
          const {x, y, originX, originY} = posInfo;
          if (!targetObj) {
              return false;
          }
  
          const targetOrigin = targetObj.getPointByOrigin(originX, originY);
          const centerOrigin = targetObj.getPointByOrigin('center', 'center');
          const diffX = centerOrigin.x - targetOrigin.x;
          const diffY = centerOrigin.y - targetOrigin.y;
  
          targetObj.set({
              left: x + diffX,
              top: y + diffY
          });
  
          targetObj.setCoords();
  
          return true;
      }
  
      /**
       * Get the canvas size
       * @returns {Object} {{width: number, height: number}} image size
       */
      getCanvasSize() {
          const image = this.getCanvasImage();
  
          return {
              width: image ? image.width : 0,
              height: image ? image.height : 0
          };
      }
  
      /**
       * Create fabric static canvas
       * @returns {Object} {{width: number, height: number}} image size
       */
      createStaticCanvas() {
          const staticCanvas = new fabric.StaticCanvas();
  
          staticCanvas.set({
              enableRetinaScaling: false
          });
  
          return staticCanvas;
      }
  
      /**
       * Get a DrawingMode instance
       * @param {string} modeName - DrawingMode Class Name
       * @returns {DrawingMode} DrawingMode instance
       * @private
       */
      _getDrawingModeInstance(modeName) {
          return this._drawingModeMap[modeName];
      }
  
      /**
       * Set object caching to false. This brought many bugs when draw Shape & cropzone
       * @see http://fabricjs.com/fabric-object-caching
       * @private
       */
      _setObjectCachingToFalse() {
          fabric.Object.prototype.objectCaching = false;
      }
  
      /**
       * Set canvas element to fabric.Canvas
       * @param {Element|string} element - Wrapper or canvas element or selector
       * @private
       */
      _setCanvasElement(element) {
          let selectedElement;
          let canvasElement;
  
          if (element.nodeType) {
              selectedElement = element;
          } else {
              selectedElement = document.querySelector(element);
          }
  
          if (selectedElement.nodeName.toUpperCase() !== 'CANVAS') {
              canvasElement = document.createElement('canvas');
              selectedElement.appendChild(canvasElement);
          }
  
          this._canvas = new fabric.Canvas(canvasElement, {
              containerClass: 'tui-image-editor-canvas-container',
              enableRetinaScaling: false
          });
      }
  
      /**
       * Creates DrawingMode instances
       * @private
       */
      _createDrawingModeInstances() {
          this._register(this._drawingModeMap, new CropperDrawingMode());
          this._register(this._drawingModeMap, new FreeDrawingMode());
          this._register(this._drawingModeMap, new LineDrawingMode());
          this._register(this._drawingModeMap, new ShapeDrawingMode());
          this._register(this._drawingModeMap, new TextDrawingMode());
      }
  
      /**
       * Create components
       * @private
       */
      _createComponents() {
          this._register(this._componentMap, new ImageLoader(this));
          this._register(this._componentMap, new Cropper(this));
          this._register(this._componentMap, new Flip(this));
          this._register(this._componentMap, new Rotation(this));
          this._register(this._componentMap, new FreeDrawing(this));
          this._register(this._componentMap, new Line(this));
          this._register(this._componentMap, new Text(this));
          this._register(this._componentMap, new Icon(this));
          this._register(this._componentMap, new Filter(this));
          this._register(this._componentMap, new Shape(this));
      }
  
      /**
       * Register component
       * @param {Object} map - map object
       * @param {Object} module - module which has getName method
       * @private
       */
      _register(map, module) {
          map[module.getName()] = module;
      }
  
      /**
       * Get the current drawing mode is same with given mode
       * @param {string} mode drawing mode
       * @returns {boolean} true if same or false
       */
      _isSameDrawingMode(mode) {
          return this.getDrawingMode() === mode;
      }
  
      /**
       * Calculate max dimension of canvas
       * The css-max dimension is dynamically decided with maintaining image ratio
       * The css-max dimension is lower than canvas dimension (attribute of canvas, not css)
       * @param {number} width - Canvas width
       * @param {number} height - Canvas height
       * @returns {{width: number, height: number}} - Max width & Max height
       * @private
       */
      _calcMaxDimension(width, height) {
          const wScaleFactor = this.cssMaxWidth / width;
          const hScaleFactor = this.cssMaxHeight / height;
          let cssMaxWidth = Math.min(width, this.cssMaxWidth);
          let cssMaxHeight = Math.min(height, this.cssMaxHeight);
  
          if (wScaleFactor < 1 && wScaleFactor < hScaleFactor) {
              cssMaxWidth = width * wScaleFactor;
              cssMaxHeight = height * wScaleFactor;
          } else if (hScaleFactor < 1 && hScaleFactor < wScaleFactor) {
              cssMaxWidth = width * hScaleFactor;
              cssMaxHeight = height * hScaleFactor;
          }
  
          return {
              width: Math.floor(cssMaxWidth),
              height: Math.floor(cssMaxHeight)
          };
      }
  
      /**
       * Callback function after loading image
       * @param {fabric.Image} obj - Fabric image object
       * @private
       */
      _callbackAfterLoadingImageObject(obj) {
          const centerPos = this.getCanvasImage().getCenterPoint();
  
          obj.set(fObjectOptions.SELECTION_STYLE);
          obj.set({
              left: centerPos.x,
              top: centerPos.y,
              crossOrigin: 'Anonymous'
          });
  
          this.getCanvas().add(obj).setActiveObject(obj);
      }
  
      /**
       * Attach canvas's events
       */
      _attachCanvasEvents() {
          const canvas = this._canvas;
          const handler = this._handler;
          canvas.on({
              'mouse:down': handler.onMouseDown,
              'object:added': handler.onObjectAdded,
              'object:removed': handler.onObjectRemoved,
              'object:moving': handler.onObjectMoved,
              'object:scaling': handler.onObjectScaled,
              'object:modified': handler.onObjectModified,
              'object:rotating': handler.onObjectRotated,
              'object:selected': handler.onObjectSelected,
              'path:created': handler.onPathCreated,
              'selection:cleared': handler.onSelectionCleared,
              'selection:created': handler.onSelectionCreated,
              'selection:updated': handler.onObjectSelected
          });
      }
  
      /**
       * "mouse:down" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onMouseDown(fEvent) {
          const originPointer = this._canvas.getPointer(fEvent.e);
          this.fire(events.MOUSE_DOWN, fEvent.e, originPointer);
      }
  
      /**
       * "object:added" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onObjectAdded(fEvent) {
          const obj = fEvent.target;
          if (obj.isType('cropzone')) {
              return;
          }
  
          this._addFabricObject(obj);
      }
  
      /**
       * "object:removed" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onObjectRemoved(fEvent) {
          const obj = fEvent.target;
  
          this._removeFabricObject(stamp(obj));
      }
  
      /**
       * "object:moving" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onObjectMoved(fEvent) {
          this._lazyFire(events.OBJECT_MOVED, object => this.createObjectProperties(object), fEvent.target);
      }
  
      /**
       * "object:scaling" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onObjectScaled(fEvent) {
          this._lazyFire(events.OBJECT_SCALED, object => this.createObjectProperties(object), fEvent.target);
      }
  
      /**
       * "object:modified" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onObjectModified(fEvent) {
          const {target} = fEvent;
          if (target.type === 'activeSelection') {
              const items = target.getObjects();
  
              items.forEach(item => item.fire('modifiedInGroup', target));
          }
      }
  
      /**
       * "object:rotating" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onObjectRotated(fEvent) {
          this._lazyFire(events.OBJECT_ROTATED, object => this.createObjectProperties(object), fEvent.target);
      }
  
      /**
       * Lazy event emitter
       * @param {string} eventName - event name
       * @param {Function} paramsMaker - make param function
       * @param {Object} [target] - Object of the event owner.
       * @private
       */
      _lazyFire(eventName, paramsMaker, target) {
          const existEventDelegation = target && target.canvasEventDelegation;
          const delegationState = existEventDelegation ? target.canvasEventDelegation(eventName) : 'none';
  
          if (delegationState === 'unregisted') {
              target.canvasEventRegister(eventName, object => {
                  this.fire(eventName, paramsMaker(object));
              });
          }
  
          if (delegationState === 'none') {
              this.fire(eventName, paramsMaker(target));
          }
      }
  
      /**
       * "object:selected" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onObjectSelected(fEvent) {
          const {target} = fEvent;
          const params = this.createObjectProperties(target);
  
          this.fire(events.OBJECT_ACTIVATED, params);
      }
  
      /**
       * "path:created" canvas event handler
       * @param {{path: fabric.Path}} obj - Path object
       * @private
       */
      _onPathCreated(obj) {
          const {x: left, y: top} = obj.path.getCenterPoint();
          obj.path.set(extend({
              left,
              top
          }, fObjectOptions.SELECTION_STYLE));
  
          const params = this.createObjectProperties(obj.path);
  
          this.fire(events.ADD_OBJECT, params);
      }
  
      /**
       * "selction:cleared" canvas event handler
       * @private
       */
      _onSelectionCleared() {
          this.fire(events.SELECTION_CLEARED);
      }
  
      /**
       * "selction:created" canvas event handler
       * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
       * @private
       */
      _onSelectionCreated(fEvent) {
          this.fire(events.SELECTION_CREATED, fEvent.target);
      }
  
      /**
       * Canvas discard selection all
       */
      discardSelection() {
          this._canvas.discardActiveObject();
          this._canvas.renderAll();
      }
  
      /**
       * Canvas Selectable status change
       * @param {boolean} selectable - expect status
       */
      changeSelectableAll(selectable) {
          this._canvas.forEachObject(obj => {
              obj.selectable = selectable;
              obj.hoverCursor = selectable ? 'move' : 'crosshair';
          });
      }
  
      /**
       * Return object's properties
       * @param {fabric.Object} obj - fabric object
       * @returns {Object} properties object
       */
      createObjectProperties(obj) {
          const predefinedKeys = [
              'left',
              'top',
              'width',
              'height',
              'fill',
              'stroke',
              'strokeWidth',
              'opacity',
              'angle'
          ];
          const props = {
              id: stamp(obj),
              type: obj.type
          };
  
          extend(props, getProperties(obj, predefinedKeys));
  
          if (includes(['i-text', 'text'], obj.type)) {
              extend(props, this._createTextProperties(obj, props));
          } else if (includes(['rect', 'triangle', 'circle'], obj.type)) {
              const shapeComp = this.getComponent(components.SHAPE);
              extend(props, {
                  fill: shapeComp.makeFillPropertyForUserEvent(obj)
              });
          }
  
          return props;
      }
  
      /**
       * Get text object's properties
       * @param {fabric.Object} obj - fabric text object
       * @param {Object} props - properties
       * @returns {Object} properties object
       */
      _createTextProperties(obj) {
          const predefinedKeys = [
              'text',
              'fontFamily',
              'fontSize',
              'fontStyle',
              'textAlign',
              'textDecoration',
              'fontWeight'
          ];
          const props = {};
          extend(props, getProperties(obj, predefinedKeys));
  
          return props;
      }
  
      /**
       * Add object array by id
       * @param {fabric.Object} obj - fabric object
       * @returns {number} object id
       */
      _addFabricObject(obj) {
          const id = stamp(obj);
          this._objects[id] = obj;
  
          return id;
      }
  
      /**
       * Remove an object in array yb id
       * @param {number} id - object id
       */
      _removeFabricObject(id) {
          delete this._objects[id];
      }
  
      /**
       * Reset targetObjectForCopyPaste value from activeObject
       */
      resetTargetObjectForCopyPaste() {
          const activeObject = this.getActiveObject();
  
          if (activeObject) {
              this.targetObjectForCopyPaste = activeObject;
          }
      }
  
      /**
       * Paste fabric object
       * @returns {Promise}
       */
      pasteObject() {
          if (!this.targetObjectForCopyPaste) {
              return Promise.resolve([]);
          }
  
          const targetObject = this.targetObjectForCopyPaste;
          const isGroupSelect = targetObject.type === 'activeSelection';
          const targetObjects = isGroupSelect ? targetObject.getObjects() : [targetObject];
          let newTargetObject = null;
  
          this.discardSelection();
  
          return this._cloneObject(targetObjects).then(addedObjects => {
              if (addedObjects.length > 1) {
                  newTargetObject = this.getActiveSelectionFromObjects(addedObjects);
              } else {
                  ([newTargetObject] = addedObjects);
              }
              this.targetObjectForCopyPaste = newTargetObject;
              this.setActiveObject(newTargetObject);
          });
      }
  
      /**
       * Clone object
       * @param {fabric.Object} targetObjects - fabric object
       * @returns {Promise}
       * @private
       */
      _cloneObject(targetObjects) {
          const addedObjects = snippet.map(targetObjects, targetObject => (
              this._cloneObjectItem(targetObject)
          ));
  
          return Promise.all(addedObjects);
      }
  
      /**
       * Clone object one item
       * @param {fabric.Object} targetObject - fabric object
       * @returns {Promise}
       * @private
       */
      _cloneObjectItem(targetObject) {
          return this._copyFabricObjectForPaste(targetObject).then(clonedObject => {
              const objectProperties = this.createObjectProperties(clonedObject);
              this.add(clonedObject);
  
              this.fire(events.ADD_OBJECT, objectProperties);
  
              return clonedObject;
          });
      }
  
      /**
       * Copy fabric object with Changed position for copy and paste
       * @param {fabric.Object} targetObject - fabric object
       * @returns {Promise}
       * @private
       */
      _copyFabricObjectForPaste(targetObject) {
          const addExtraPx = (value, isReverse) => isReverse ? value - EXTRA_PX_FOR_PASTE : value + EXTRA_PX_FOR_PASTE;
  
          return this._copyFabricObject(targetObject).then(clonedObject => {
              const {left, top, width, height} = clonedObject;
              const {width: canvasWidth, height: canvasHeight} = this.getCanvasSize();
              const rightEdge = left + (width / 2);
              const bottomEdge = top + (height / 2);
  
              clonedObject.set(snippet.extend({
                  left: addExtraPx(left, rightEdge + EXTRA_PX_FOR_PASTE > canvasWidth),
                  top: addExtraPx(top, bottomEdge + EXTRA_PX_FOR_PASTE > canvasHeight)
              }, fObjectOptions.SELECTION_STYLE));
  
              return clonedObject;
          });
      }
  
      /**
       * Copy fabric object
       * @param {fabric.Object} targetObject - fabric object
       * @returns {Promise}
       * @private
       */
      _copyFabricObject(targetObject) {
          return new Promise(resolve => {
              targetObject.clone(cloned => {
                  const shapeComp = this.getComponent(components.SHAPE);
                  if (isShape(cloned)) {
                      shapeComp.processForCopiedObject(cloned, targetObject);
                  }
  
                  resolve(cloned);
              });
          });
      }
  }
  
  CustomEvents.mixin(Graphics);
  
  export default Graphics;
  