# @interactjs/actions/resize.ts

    import { Interaction } from '@interactjs/core/Interaction'
    import Scope from '@interactjs/core/scope'
    import * as dom from '@interactjs/utils/domUtils'
    import extend from '@interactjs/utils/extend'
    import * as is from '@interactjs/utils/is'

    export type EdgeName = 'top' | 'left' | 'bottom' | 'right'

    export type ResizableMethod = Interact.ActionMethod<Interact.ResizableOptions>

    declare module '@interactjs/core/Interactable' {
      interface Interactable {
        resizable: ResizableMethod
      }
    }

    declare module '@interactjs/core/Interaction' {
      interface Interaction {
        resizeAxes: 'x' | 'y' | 'xy'
        resizeStartAspectRatio: number
      }
    }

    declare module '@interactjs/core/defaultOptions' {
      interface ActionDefaults {
        resize: Interact.ResizableOptions
      }
    }

    declare module '@interactjs/core/scope' {
      interface ActionMap {
        resize?: typeof resize
      }
    }

    export interface ResizeEvent<P extends Interact.EventPhase = Interact.EventPhase> extends Interact.InteractEvent<'resize', P> {
      deltaRect?: Interact.FullRect
      edges?: Interact.ActionProps['edges']
    }

    function install (scope: Scope) {
      const {
        actions,
        browser,
        /** @lends Interactable */
        Interactable, // tslint:disable-line no-shadowed-variable
        defaults,
      } = scope

      // Less Precision with touch input

      resize.cursors = initCursors(browser)
      resize.defaultMargin = browser.supportsTouch || browser.supportsPointerEvent ? 20 : 10

      /**
       * ```js
       * interact(element).resizable({
       *   onstart: function (event) {},
       *   onmove : function (event) {},
       *   onend  : function (event) {},
       *
       *   edges: {
       *     top   : true,       // Use pointer coords to check for resize.
       *     left  : false,      // Disable resizing from left edge.
       *     bottom: '.resize-s',// Resize if pointer target matches selector
       *     right : handleEl    // Resize if pointer target is the given Element
       *   },
       *
       *     // Width and height can be adjusted independently. When `true`, width and
       *     // height are adjusted at a 1:1 ratio.
       *     square: false,
       *
       *     // Width and height can be adjusted independently. When `true`, width and
       *     // height maintain the aspect ratio they had when resizing started.
       *     preserveAspectRatio: false,
       *
       *   // a value of 'none' will limit the resize rect to a minimum of 0x0
       *   // 'negate' will allow the rect to have negative width/height
       *   // 'reposition' will keep the width/height positive by swapping
       *   // the top and bottom edges and/or swapping the left and right edges
       *   invert: 'none' || 'negate' || 'reposition'
       *
       *   // limit multiple resizes.
       *   // See the explanation in the {@link Interactable.draggable} example
       *   max: Infinity,
       *   maxPerElement: 1,
       * })
       *
       * var isResizeable = interact(element).resizable()
       * ```
       *
       * Gets or sets whether resize actions can be performed on the target
       *
       * @param {boolean | object} [options] true/false or An object with event
       * listeners to be fired on resize events (object makes the Interactable
       * resizable)
       * @return {boolean | Interactable} A boolean indicating if this can be the
       * target of resize elements, or this Interactable
       */
      Interactable.prototype.resizable = function (this: Interact.Interactable, options: Interact.ResizableOptions | boolean) {
        return resizable(this, options, scope)
      } as ResizableMethod

      actions.map.resize = resize
      actions.methodDict.resize = 'resizable'

      defaults.actions.resize = resize.defaults
    }

    function resizeChecker (arg) {
      const { interaction, interactable, element, rect, buttons } = arg

      if (!rect) { return undefined }

      const page = extend({}, interaction.coords.cur.page)
      const resizeOptions = interactable.options.resize

      if (
        !(resizeOptions && resizeOptions.enabled) ||
        // check mouseButton setting if the pointer is down
        (interaction.pointerIsDown &&
         /mouse|pointer/.test(interaction.pointerType) &&
       (buttons & resizeOptions.mouseButtons) === 0)
      ) {
        return undefined
      }

      // if using resize.edges
      if (is.object(resizeOptions.edges)) {
        const resizeEdges = {
          left: false,
          right: false,
          top: false,
          bottom: false,
        }

        for (const edge in resizeEdges) {
          resizeEdges[edge] = checkResizeEdge(edge,
            resizeOptions.edges[edge],
            page,
            interaction._latestPointer.eventTarget,
            element,
            rect,
            resizeOptions.margin || resize.defaultMargin)
        }

        resizeEdges.left = resizeEdges.left && !resizeEdges.right
        resizeEdges.top  = resizeEdges.top  && !resizeEdges.bottom

        if (resizeEdges.left || resizeEdges.right || resizeEdges.top || resizeEdges.bottom) {
          arg.action = {
            name: 'resize',
            edges: resizeEdges,
          }
        }
      }
      else {
        const right  = resizeOptions.axis !== 'y' && page.x > (rect.right  - resize.defaultMargin)
        const bottom = resizeOptions.axis !== 'x' && page.y > (rect.bottom - resize.defaultMargin)

        if (right || bottom) {
          arg.action = {
            name: 'resize',
            axes: (right ? 'x' : '') + (bottom ? 'y' : ''),
          }
        }
      }

      return arg.action ? false : undefined
    }

    function resizable (interactable: Interact.Interactable, options: Interact.OrBoolean<Interact.ResizableOptions> | boolean, scope: Scope) {
      if (is.object(options)) {
        interactable.options.resize.enabled = options.enabled !== false
        interactable.setPerAction('resize', options)
        interactable.setOnEvents('resize', options)

        if (is.string(options.axis) && /^x$|^y$|^xy$/.test(options.axis)) {
          interactable.options.resize.axis = options.axis
        }
        else if (options.axis === null) {
          interactable.options.resize.axis = scope.defaults.actions.resize.axis
        }

        if (is.bool(options.preserveAspectRatio)) {
          interactable.options.resize.preserveAspectRatio = options.preserveAspectRatio
        }
        else if (is.bool(options.square)) {
          interactable.options.resize.square = options.square
        }

        return interactable
      }
      if (is.bool(options)) {
        interactable.options.resize.enabled = options

        return interactable
      }
      return interactable.options.resize
    }

    function checkResizeEdge (
      name: string,
      value: any,
      page: Interact.Point,
      element: Node,
      interactableElement: Interact.Element,
      rect: Interact.Rect,
      margin: number,
    ) {
      // false, '', undefined, null
      if (!value) { return false }

      // true value, use pointer coords and element rect
      if (value === true) {
        // if dimensions are negative, "switch" edges
        const width  = is.number(rect.width) ? rect.width  : rect.right  - rect.left
        const height = is.number(rect.height) ? rect.height : rect.bottom - rect.top

        // don't use margin greater than half the relevent dimension
        margin = Math.min(margin, (name === 'left' || name === 'right' ? width : height) / 2)

        if (width < 0) {
          if      (name === 'left')  { name = 'right' }
          else if (name === 'right') { name = 'left'  }
        }
        if (height < 0) {
          if      (name === 'top')    { name = 'bottom' }
          else if (name === 'bottom') { name = 'top'    }
        }

        if (name === 'left') { return page.x < ((width  >= 0 ? rect.left : rect.right) + margin) }
        if (name === 'top') { return page.y < ((height >= 0 ? rect.top : rect.bottom) + margin) }

        if (name === 'right') { return page.x > ((width  >= 0 ? rect.right : rect.left) - margin) }
        if (name === 'bottom') { return page.y > ((height >= 0 ? rect.bottom : rect.top) - margin) }
      }

      // the remaining checks require an element
      if (!is.element(element)) { return false }

      return is.element(value)
      // the value is an element to use as a resize handle
        ? value === element
        // otherwise check if element matches value as selector
        : dom.matchesUpTo(element, value, interactableElement)
    }

    function initCursors (browser: typeof import ('@interactjs/utils/browser').default) {
      return (browser.isIe9 ? {
        x : 'e-resize',
        y : 's-resize',
        xy: 'se-resize',

        top        : 'n-resize',
        left       : 'w-resize',
        bottom     : 's-resize',
        right      : 'e-resize',
        topleft    : 'se-resize',
        bottomright: 'se-resize',
        topright   : 'ne-resize',
        bottomleft : 'ne-resize',
      } : {
        x : 'ew-resize',
        y : 'ns-resize',
        xy: 'nwse-resize',

        top        : 'ns-resize',
        left       : 'ew-resize',
        bottom     : 'ns-resize',
        right      : 'ew-resize',
        topleft    : 'nwse-resize',
        bottomright: 'nwse-resize',
        topright   : 'nesw-resize',
        bottomleft : 'nesw-resize',
      })
    }

    function start ({ iEvent, interaction }: { iEvent: Interact.InteractEvent<any, any>, interaction: Interaction }) {
      if (interaction.prepared.name !== 'resize' || !interaction.prepared.edges) {
        return
      }

      const resizeEvent = iEvent as ResizeEvent
      const rect = interaction.rect

      interaction._rects = {
        start: extend({}, rect),
        corrected: extend({}, rect),
        previous: extend({}, rect),
        delta: {
          left: 0,
          right : 0,
          width : 0,
          top : 0,
          bottom: 0,
          height: 0,
        },
      }

      resizeEvent.edges = interaction.prepared.edges
      resizeEvent.rect = interaction._rects.corrected
      resizeEvent.deltaRect = interaction._rects.delta
    }

    function move ({ iEvent, interaction }: { iEvent: Interact.InteractEvent<any, any>, interaction: Interaction }) {
      if (interaction.prepared.name !== 'resize' || !interaction.prepared.edges) { return }

      const resizeEvent = iEvent as ResizeEvent
      const resizeOptions = interaction.interactable.options.resize
      const invert = resizeOptions.invert
      const invertible = invert === 'reposition' || invert === 'negate'

      // eslint-disable-next-line no-shadow
      const current = interaction.rect
      const { start: startRect, corrected, delta: deltaRect, previous } = interaction._rects

      extend(previous, corrected)

      if (invertible) {
        // if invertible, copy the current rect
        extend(corrected, current)

        if (invert === 'reposition') {
          // swap edge values if necessary to keep width/height positive
          if (corrected.top > corrected.bottom) {
            const swap = corrected.top

            corrected.top = corrected.bottom
            corrected.bottom = swap
          }
          if (corrected.left > corrected.right) {
            const swap = corrected.left

            corrected.left = corrected.right
            corrected.right = swap
          }
        }
      }
      else {
        // if not invertible, restrict to minimum of 0x0 rect
        corrected.top    = Math.min(current.top, startRect.bottom)
        corrected.bottom = Math.max(current.bottom, startRect.top)
        corrected.left   = Math.min(current.left, startRect.right)
        corrected.right  = Math.max(current.right, startRect.left)
      }

      corrected.width  = corrected.right  - corrected.left
      corrected.height = corrected.bottom - corrected.top

      for (const edge in corrected) {
        deltaRect[edge] = corrected[edge] - previous[edge]
      }

      resizeEvent.edges = interaction.prepared.edges
      resizeEvent.rect = corrected
      resizeEvent.deltaRect = deltaRect
    }

    function end ({ iEvent, interaction }: { iEvent: Interact.InteractEvent<any, any>, interaction: Interaction }) {
      if (interaction.prepared.name !== 'resize' || !interaction.prepared.edges) { return }

      const resizeEvent = iEvent as ResizeEvent

      resizeEvent.edges = interaction.prepared.edges
      resizeEvent.rect = interaction._rects.corrected
      resizeEvent.deltaRect = interaction._rects.delta
    }

    function updateEventAxes ({ iEvent, interaction }: { iEvent: Interact.InteractEvent<any, any>, interaction: Interaction }) {
      if (interaction.prepared.name !== 'resize' || !interaction.resizeAxes) { return }

      const options = interaction.interactable.options
      const resizeEvent = iEvent as ResizeEvent

      if (options.resize.square) {
        if (interaction.resizeAxes === 'y') {
          resizeEvent.delta.x = resizeEvent.delta.y
        }
        else {
          resizeEvent.delta.y = resizeEvent.delta.x
        }
        resizeEvent.axes = 'xy'
      }
      else {
        resizeEvent.axes = interaction.resizeAxes

        if (interaction.resizeAxes === 'x') {
          resizeEvent.delta.y = 0
        }
        else if (interaction.resizeAxes === 'y') {
          resizeEvent.delta.x = 0
        }
      }
    }

    const resize: Interact.Plugin = {
      id: 'actions/resize',
      before: ['actions/drag'],
      install,
      listeners: {
        'interactions:new': ({ interaction }) => {
          interaction.resizeAxes = 'xy'
        },

        'interactions:action-start': arg => {
          start(arg)
          updateEventAxes(arg)
        },
        'interactions:action-move': arg => {
          move(arg)
          updateEventAxes(arg)
        },
        'interactions:action-end': end,
        'auto-start:check': resizeChecker,
      },

      defaults: {
        square: false,
        preserveAspectRatio: false,
        axis: 'xy',

        // use default margin
        margin: NaN,

        // object with props left, right, top, bottom which are
        // true/false values to resize when the pointer is over that edge,
        // CSS selectors to match the handles for each direction
        // or the Elements for each handle
        edges: null,

        // a value of 'none' will limit the resize rect to a minimum of 0x0
        // 'negate' will alow the rect to have negative width/height
        // 'reposition' will keep the width/height positive by swapping
        // the top and bottom edges and/or swapping the left and right edges
        invert: 'none',
      } as Interact.ResizableOptions,

      cursors: null as ReturnType<typeof initCursors>,

      getCursor ({ edges, axis, name }: Interact.ActionProps) {
        const cursors = resize.cursors
        let result: string = null

        if (axis) {
          result = cursors[name + axis]
        }
        else if (edges) {
          let cursorKey = ''

          for (const edge of ['top', 'bottom', 'left', 'right']) {
            if (edges[edge]) {
              cursorKey += edge
            }
          }

          result = cursors[cursorKey]
        }

        return result
      },

      defaultMargin: null as number,
    }

    export default resize