/**
 * Path functionality.
 * @module path
 * @license MIT
 *
 * @copyright 2011 Alexis Deveria, 2011 Jeff Schiller
 */

import { NS } from '../common/namespaces.js';
import { ChangeElementCommand } from './history.js';
import {
  transformPoint, getMatrix
} from './math.js';
import {
  assignAttributes, getRotationAngle, isNullish,
  getElem
} from './utilities.js';
import {
  supportsPathInsertItemBefore, supportsPathReplaceItem, isWebkit
} from '../common/browser.js';

let pathMethodsContext_ = null;
let editorContext_ = null;

/**
* @function module:path-actions.init
* @param {module:path-actions.pathMethodsContext_} pathMethodsContext
* @returns {void}
*/
export const init = function (pathMethodsContext) {
  pathMethodsContext_ = pathMethodsContext;
};

/**
* @function module:path.insertItemBefore
* @param {Element} elem
* @param {Segment} newseg
* @param {Integer} index
* @returns {void}
*/
export const insertItemBeforeMethod = function (elem, newseg, index) {
  // Support insertItemBefore on paths for FF2
  const list = elem.pathSegList;

  if (supportsPathInsertItemBefore()) {
    list.insertItemBefore(newseg, index);
    return;
  }
  const len = list.numberOfItems;
  const arr = [];
  for (let i = 0; i < len; i++) {
    const curSeg = list.getItem(i);
    arr.push(curSeg);
  }
  list.clear();
  for (let i = 0; i < len; i++) {
    if (i === index) { // index + 1
      list.appendItem(newseg);
    }
    list.appendItem(arr[i]);
  }
};
/* eslint-disable max-len */
/**
* @function module:path.ptObjToArr
* @todo See if this should just live in `replacePathSeg`
* @param {string} type
* @param {SVGPathSegMovetoAbs|SVGPathSegLinetoAbs|SVGPathSegCurvetoCubicAbs|SVGPathSegCurvetoQuadraticAbs|SVGPathSegArcAbs|SVGPathSegLinetoHorizontalAbs|SVGPathSegLinetoVerticalAbs|SVGPathSegCurvetoCubicSmoothAbs|SVGPathSegCurvetoQuadraticSmoothAbs} segItem
* @returns {ArgumentsArray}
*/
/* eslint-enable max-len */
export const ptObjToArrMethod = function (type, segItem) {
  const segData = pathMethodsContext_.getSegData();
  const props = segData[type];
  return props.map((prop) => {
    return segItem[prop];
  });
};

/**
* @function module:path.getGripPt
* @param {Segment} seg
* @param {module:math.XYObject} altPt
* @returns {module:math.XYObject}
*/
export const getGripPtMethod = function (seg, altPt) {
  const { path: pth } = seg;
  let out = {
    x: altPt ? altPt.x : seg.item.x,
    y: altPt ? altPt.y : seg.item.y
  };

  if (pth.matrix) {
    const pt = transformPoint(out.x, out.y, pth.matrix);
    out = pt;
  }
  editorContext_ = pathMethodsContext_.getEditorContext();
  const currentZoom = editorContext_.getCurrentZoom();
  out.x *= currentZoom;
  out.y *= currentZoom;

  return out;
};
/**
* @function module:path.getPointFromGrip
* @param {module:math.XYObject} pt
* @param {module:path.Path} pth
* @returns {module:math.XYObject}
*/
export const getPointFromGripMethod = function (pt, pth) {
  const out = {
    x: pt.x,
    y: pt.y
  };

  if (pth.matrix) {
    pt = transformPoint(out.x, out.y, pth.imatrix);
    out.x = pt.x;
    out.y = pt.y;
  }
  editorContext_ = pathMethodsContext_.getEditorContext();
  const currentZoom = editorContext_.getCurrentZoom();
  out.x /= currentZoom;
  out.y /= currentZoom;

  return out;
};
/**
* @function module:path.getGripContainer
* @returns {Element}
*/
export const getGripContainerMethod = function () {
  let c = getElem('pathpointgrip_container');
  if (!c) {
    const parentElement = getElem('selectorParentGroup');
    c = document.createElementNS(NS.SVG, 'g');
    parentElement.append(c);
    c.id = 'pathpointgrip_container';
  }
  return c;
};
/**
* Requires prior call to `setUiStrings` if `xlink:title`
*    to be set on the grip.
* @function module:path.addPointGrip
* @param {Integer} index
* @param {Integer} x
* @param {Integer} y
* @returns {SVGCircleElement}
*/
export const addPointGripMethod = function (index, x, y) {
  // create the container of all the point grips
  const pointGripContainer = getGripContainerMethod();

  let pointGrip = getElem('pathpointgrip_' + index);
  // create it
  if (!pointGrip) {
    pointGrip = document.createElementNS(NS.SVG, 'circle');
    const atts = {
      id: 'pathpointgrip_' + index,
      display: 'none',
      r: 4,
      fill: '#0FF',
      stroke: '#00F',
      'stroke-width': 2,
      cursor: 'move',
      style: 'pointer-events:all'
    };
    const uiStrings = pathMethodsContext_.getUIStrings();
    if ('pathNodeTooltip' in uiStrings) { // May be empty if running path.js without svg-editor
      atts['xlink:title'] = uiStrings.pathNodeTooltip;
    }
    assignAttributes(pointGrip, atts);
    pointGripContainer.append(pointGrip);

    const grip = document.getElementById('pathpointgrip_' + index);
    grip?.addEventListener("dblclick", () => {
      const path = pathMethodsContext_.getPathObj();
      if (path) {
        path.setSegType();
      }
    });
  }
  if (x && y) {
    // set up the point grip element and display it
    assignAttributes(pointGrip, {
      cx: x,
      cy: y,
      display: 'inline'
    });
  }
  return pointGrip;
};
/**
* Requires prior call to `setUiStrings` if `xlink:title`
*    to be set on the grip.
* @function module:path.addCtrlGrip
* @param {string} id
* @returns {SVGCircleElement}
*/
export const addCtrlGripMethod = function (id) {
  let pointGrip = getElem('ctrlpointgrip_' + id);
  if (pointGrip) { return pointGrip; }

  pointGrip = document.createElementNS(NS.SVG, 'circle');
  const atts = {
    id: 'ctrlpointgrip_' + id,
    display: 'none',
    r: 4,
    fill: '#0FF',
    stroke: '#55F',
    'stroke-width': 1,
    cursor: 'move',
    style: 'pointer-events:all'
  };
  const uiStrings = pathMethodsContext_.getUIStrings();
  if ('pathCtrlPtTooltip' in uiStrings) { // May be empty if running path.js without svg-editor
    atts['xlink:title'] = uiStrings.pathCtrlPtTooltip;
  }
  assignAttributes(pointGrip, atts);
  getGripContainerMethod().append(pointGrip);
  return pointGrip;
};
/**
* @function module:path.getCtrlLine
* @param {string} id
* @returns {SVGLineElement}
*/
export const getCtrlLineMethod = function (id) {
  let ctrlLine = getElem('ctrlLine_' + id);
  if (ctrlLine) { return ctrlLine; }

  ctrlLine = document.createElementNS(NS.SVG, 'line');
  assignAttributes(ctrlLine, {
    id: 'ctrlLine_' + id,
    stroke: '#555',
    'stroke-width': 1,
    style: 'pointer-events:none'
  });
  getGripContainerMethod().append(ctrlLine);
  return ctrlLine;
};
/**
* @function module:path.getPointGrip
* @param {Segment} seg
* @param {boolean} update
* @returns {SVGCircleElement}
*/
export const getPointGripMethod = function (seg, update) {
  const { index } = seg;
  const pointGrip = addPointGripMethod(index);

  if (update) {
    const pt = getGripPtMethod(seg);
    assignAttributes(pointGrip, {
      cx: pt.x,
      cy: pt.y,
      display: 'inline'
    });
  }

  return pointGrip;
};
/**
* @function module:path.getControlPoints
* @param {Segment} seg
* @returns {PlainObject<string, SVGLineElement|SVGCircleElement>}
*/
export const getControlPointsMethod = function (seg) {
  const { item, index } = seg;
  if (!('x1' in item) || !('x2' in item)) { return null; }
  const cpt = {};
  /* const pointGripContainer = */ getGripContainerMethod();

  // Note that this is intentionally not seg.prev.item
  const path = pathMethodsContext_.getPathObj();
  const prev = path.segs[index - 1].item;

  const segItems = [ prev, item ];

  for (let i = 1; i < 3; i++) {
    const id = index + 'c' + i;

    const ctrlLine = cpt['c' + i + '_line'] = getCtrlLineMethod(id);

    const pt = getGripPtMethod(seg, { x: item['x' + i], y: item['y' + i] });
    const gpt = getGripPtMethod(seg, { x: segItems[i - 1].x, y: segItems[i - 1].y });

    assignAttributes(ctrlLine, {
      x1: pt.x,
      y1: pt.y,
      x2: gpt.x,
      y2: gpt.y,
      display: 'inline'
    });

    cpt['c' + i + '_line'] = ctrlLine;

    // create it
    const pointGrip = cpt['c' + i] = addCtrlGripMethod(id);

    assignAttributes(pointGrip, {
      cx: pt.x,
      cy: pt.y,
      display: 'inline'
    });
    cpt['c' + i] = pointGrip;
  }
  return cpt;
};
/**
* This replaces the segment at the given index. Type is given as number.
* @function module:path.replacePathSeg
* @param {Integer} type Possible values set during {@link module:path.init}
* @param {Integer} index
* @param {ArgumentsArray} pts
* @param {SVGPathElement} elem
* @returns {void}
*/
export const replacePathSegMethod = function (type, index, pts, elem) {
  const path = pathMethodsContext_.getPathObj();
  const pth = elem || path.elem;
  const pathFuncs = pathMethodsContext_.getPathFuncs();
  const func = 'createSVGPathSeg' + pathFuncs[type];
  const seg = pth[func](...pts);

  if (supportsPathReplaceItem()) {
    pth.pathSegList.replaceItem(seg, index);
  } else {
    const segList = pth.pathSegList;
    const len = segList.numberOfItems;
    const arr = [];
    for (let i = 0; i < len; i++) {
      const curSeg = segList.getItem(i);
      arr.push(curSeg);
    }
    segList.clear();
    for (let i = 0; i < len; i++) {
      if (i === index) {
        segList.appendItem(seg);
      } else {
        segList.appendItem(arr[i]);
      }
    }
  }
};
/**
* @function module:path.getSegSelector
* @param {Segment} seg
* @param {boolean} update
* @returns {SVGPathElement}
*/
export const getSegSelectorMethod = function (seg, update) {
  const { index } = seg;
  let segLine = getElem('segline_' + index);
  if (!segLine) {
    const pointGripContainer = getGripContainerMethod();
    // create segline
    segLine = document.createElementNS(NS.SVG, 'path');
    assignAttributes(segLine, {
      id: 'segline_' + index,
      display: 'none',
      fill: 'none',
      stroke: '#0FF',
      'stroke-width': 2,
      style: 'pointer-events:none',
      d: 'M0,0 0,0'
    });
    pointGripContainer.append(segLine);
  }

  if (update) {
    const { prev } = seg;
    if (!prev) {
      segLine.setAttribute('display', 'none');
      return segLine;
    }

    const pt = getGripPtMethod(prev);
    // Set start point
    replacePathSegMethod(2, 0, [ pt.x, pt.y ], segLine);

    const pts = ptObjToArrMethod(seg.type, seg.item); // , true);
    for (let i = 0; i < pts.length; i += 2) {
      const point = getGripPtMethod(seg, { x: pts[i], y: pts[i + 1] });
      pts[i] = point.x;
      pts[i + 1] = point.y;
    }

    replacePathSegMethod(seg.type, 1, pts, segLine);
  }
  return segLine;
};
/**
*
*/
export class Segment {
  /**
  * @param {Integer} index
  * @param {SVGPathSeg} item
  * @todo Is `item` be more constrained here?
  */
  constructor (index, item) {
    this.selected = false;
    this.index = index;
    this.item = item;
    this.type = item.pathSegType;

    this.ctrlpts = [];
    this.ptgrip = null;
    this.segsel = null;
  }

  /**
   * @param {boolean} y
   * @returns {void}
   */
  showCtrlPts (y) {
    for (const i in this.ctrlpts) {
      if ({}.hasOwnProperty.call(this.ctrlpts, i)) {
        this.ctrlpts[i].setAttribute('display', y ? 'inline' : 'none');
      }
    }
  }

  /**
   * @param {boolean} y
   * @returns {void}
   */
  selectCtrls (y) {
    document.getElementById('ctrlpointgrip_' + this.index + 'c1').setAttribute('fill', y ? '#0FF' : '#EEE');
    document.getElementById('ctrlpointgrip_' + this.index + 'c2').setAttribute('fill', y ? '#0FF' : '#EEE');
  }

  /**
   * @param {boolean} y
   * @returns {void}
   */
  show (y) {
    if (this.ptgrip) {
      this.ptgrip.setAttribute('display', y ? 'inline' : 'none');
      this.segsel.setAttribute('display', y ? 'inline' : 'none');
      // Show/hide all control points if available
      this.showCtrlPts(y);
    }
  }

  /**
   * @param {boolean} y
   * @returns {void}
   */
  select (y) {
    if (this.ptgrip) {
      this.ptgrip.setAttribute('stroke', y ? '#0FF' : '#00F');
      this.segsel.setAttribute('display', y ? 'inline' : 'none');
      if (this.ctrlpts) {
        this.selectCtrls(y);
      }
      this.selected = y;
    }
  }

  /**
   * @returns {void}
   */
  addGrip () {
    this.ptgrip = getPointGripMethod(this, true);
    this.ctrlpts = getControlPointsMethod(this); // , true);
    this.segsel = getSegSelectorMethod(this, true);
  }

  /**
   * @param {boolean} full
   * @returns {void}
   */
  update (full) {
    if (this.ptgrip) {
      const pt = getGripPtMethod(this);
      assignAttributes(this.ptgrip, {
        cx: pt.x,
        cy: pt.y
      });

      getSegSelectorMethod(this, true);

      if (this.ctrlpts) {
        if (full) {
          const path = pathMethodsContext_.getPathObj();
          this.item = path.elem.pathSegList.getItem(this.index);
          this.type = this.item.pathSegType;
        }
        getControlPointsMethod(this);
      }
      // this.segsel.setAttribute('display', y ? 'inline' : 'none');
    }
  }

  /**
   * @param {Integer} dx
   * @param {Integer} dy
   * @returns {void}
   */
  move (dx, dy) {
    const { item } = this;

    const curPts = this.ctrlpts
      ? [
        item.x += dx, item.y += dy,
        item.x1, item.y1, item.x2 += dx, item.y2 += dy
      ]
      : [ item.x += dx, item.y += dy ];

    replacePathSegMethod(
      this.type,
      this.index,
      // type 10 means ARC
      this.type === 10 ? ptObjToArrMethod(this.type, item) : curPts
    );

    if (this.next && this.next.ctrlpts) {
      const next = this.next.item;
      const nextPts = [
        next.x, next.y,
        next.x1 += dx, next.y1 += dy, next.x2, next.y2
      ];
      replacePathSegMethod(this.next.type, this.next.index, nextPts);
    }

    if (this.mate) {
      // The last point of a closed subpath has a 'mate',
      // which is the 'M' segment of the subpath
      const { item: itm } = this.mate;
      const pts = [ itm.x += dx, itm.y += dy ];
      replacePathSegMethod(this.mate.type, this.mate.index, pts);
      // Has no grip, so does not need 'updating'?
    }

    this.update(true);
    if (this.next) { this.next.update(true); }
  }

  /**
   * @param {Integer} num
   * @returns {void}
   */
  setLinked (num) {
    let seg, anum, pt;
    if (num === 2) {
      anum = 1;
      seg = this.next;
      if (!seg) { return; }
      pt = this.item;
    } else {
      anum = 2;
      seg = this.prev;
      if (!seg) { return; }
      pt = seg.item;
    }

    const { item } = seg;
    item['x' + anum] = pt.x + (pt.x - this.item['x' + num]);
    item['y' + anum] = pt.y + (pt.y - this.item['y' + num]);

    const pts = [
      item.x, item.y,
      item.x1, item.y1,
      item.x2, item.y2
    ];

    replacePathSegMethod(seg.type, seg.index, pts);
    seg.update(true);
  }

  /**
   * @param {Integer} num
   * @param {Integer} dx
   * @param {Integer} dy
   * @returns {void}
   */
  moveCtrl (num, dx, dy) {
    const { item } = this;
    item['x' + num] += dx;
    item['y' + num] += dy;

    const pts = [
      item.x, item.y,
      item.x1, item.y1, item.x2, item.y2
    ];

    replacePathSegMethod(this.type, this.index, pts);
    this.update(true);
  }

  /**
   * @param {Integer} newType Possible values set during {@link module:path.init}
   * @param {ArgumentsArray} pts
   * @returns {void}
   */
  setType (newType, pts) {
    replacePathSegMethod(newType, this.index, pts);
    this.type = newType;
    const path = pathMethodsContext_.getPathObj();
    this.item = path.elem.pathSegList.getItem(this.index);
    this.showCtrlPts(newType === 6);
    this.ctrlpts = getControlPointsMethod(this);
    this.update(true);
  }
}

/**
*
*/
export class Path {
  /**
  * @param {SVGPathElement} elem
  * @throws {Error} If constructed without a path element
  */
  constructor (elem) {
    if (!elem || elem.tagName !== 'path') {
      throw new Error('svgedit.path.Path constructed without a <path> element');
    }

    this.elem = elem;
    this.segs = [];
    this.selected_pts = [];
    pathMethodsContext_.setPathObj(this);
    // path = this;

    this.init();
  }

  /**
  * Reset path data.
  * @returns {module:path.Path}
  */
  init () {
    // Hide all grips, etc

    // fixed, needed to work on all found elements, not just first
    const pointGripContainer = getGripContainerMethod();
    const elements = pointGripContainer.querySelectorAll('*');
    Array.prototype.forEach.call(elements, function(el){
      el.style.display = 'none';
    });

    const segList = this.elem.pathSegList;
    const len = segList.numberOfItems;
    this.segs = [];
    this.selected_pts = [];
    this.first_seg = null;

    // Set up segs array
    for (let i = 0; i < len; i++) {
      const item = segList.getItem(i);
      const segment = new Segment(i, item);
      segment.path = this;
      this.segs.push(segment);
    }

    const { segs } = this;

    let startI = null;
    for (let i = 0; i < len; i++) {
      const seg = segs[i];
      const nextSeg = (i + 1) >= len ? null : segs[i + 1];
      const prevSeg = (i - 1) < 0 ? null : segs[i - 1];
      if (seg.type === 2) {
        if (prevSeg && prevSeg.type !== 1) {
          // New sub-path, last one is open,
          // so add a grip to last sub-path's first point
          const startSeg = segs[startI];
          startSeg.next = segs[startI + 1];
          startSeg.next.prev = startSeg;
          startSeg.addGrip();
        }
        // Remember that this is a starter seg
        startI = i;
      } else if (nextSeg && nextSeg.type === 1) {
        // This is the last real segment of a closed sub-path
        // Next is first seg after "M"
        seg.next = segs[startI + 1];

        // First seg after "M"'s prev is this
        seg.next.prev = seg;
        seg.mate = segs[startI];
        seg.addGrip();
        if (isNullish(this.first_seg)) {
          this.first_seg = seg;
        }
      } else if (!nextSeg) {
        if (seg.type !== 1) {
          // Last seg, doesn't close so add a grip
          // to last sub-path's first point
          const startSeg = segs[startI];
          startSeg.next = segs[startI + 1];
          startSeg.next.prev = startSeg;
          startSeg.addGrip();
          seg.addGrip();

          if (!this.first_seg) {
            // Open path, so set first as real first and add grip
            this.first_seg = segs[startI];
          }
        }
      } else if (seg.type !== 1) {
        // Regular segment, so add grip and its "next"
        seg.addGrip();

        // Don't set its "next" if it's an "M"
        if (nextSeg && nextSeg.type !== 2) {
          seg.next = nextSeg;
          seg.next.prev = seg;
        }
      }
    }
    return this;
  }

  /**
  * @callback module:path.PathEachSegCallback
  * @this module:path.Segment
  * @param {Integer} i The index of the seg being iterated
  * @returns {boolean|void} Will stop execution of `eachSeg` if returns `false`
  */
  /**
  * @param {module:path.PathEachSegCallback} fn
  * @returns {void}
  */
  eachSeg (fn) {
    const len = this.segs.length;
    for (let i = 0; i < len; i++) {
      const ret = fn.call(this.segs[i], i);
      if (ret === false) { break; }
    }
  }

  /**
  * @param {Integer} index
  * @returns {void}
  */
  addSeg (index) {
    // Adds a new segment
    const seg = this.segs[index];
    if (!seg.prev) { return; }

    const { prev } = seg;
    let newseg, newX, newY;
    switch (seg.item.pathSegType) {
    case 4: {
      newX = (seg.item.x + prev.item.x) / 2;
      newY = (seg.item.y + prev.item.y) / 2;
      newseg = this.elem.createSVGPathSegLinetoAbs(newX, newY);
      break;
    } case 6: { // make it a curved segment to preserve the shape (WRS)
      // https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm#Geometric_interpretation
      const p0x = (prev.item.x + seg.item.x1) / 2;
      const p1x = (seg.item.x1 + seg.item.x2) / 2;
      const p2x = (seg.item.x2 + seg.item.x) / 2;
      const p01x = (p0x + p1x) / 2;
      const p12x = (p1x + p2x) / 2;
      newX = (p01x + p12x) / 2;
      const p0y = (prev.item.y + seg.item.y1) / 2;
      const p1y = (seg.item.y1 + seg.item.y2) / 2;
      const p2y = (seg.item.y2 + seg.item.y) / 2;
      const p01y = (p0y + p1y) / 2;
      const p12y = (p1y + p2y) / 2;
      newY = (p01y + p12y) / 2;
      newseg = this.elem.createSVGPathSegCurvetoCubicAbs(newX, newY, p0x, p0y, p01x, p01y);
      const pts = [ seg.item.x, seg.item.y, p12x, p12y, p2x, p2y ];
      replacePathSegMethod(seg.type, index, pts);
      break;
    }
    }

    insertItemBeforeMethod(this.elem, newseg, index);
  }

  /**
  * @param {Integer} index
  * @returns {void}
  */
  deleteSeg (index) {
    const seg = this.segs[index];
    const list = this.elem.pathSegList;

    seg.show(false);
    const { next } = seg;
    if (seg.mate) {
      // Make the next point be the "M" point
      const pt = [ next.item.x, next.item.y ];
      replacePathSegMethod(2, next.index, pt);

      // Reposition last node
      replacePathSegMethod(4, seg.index, pt);

      list.removeItem(seg.mate.index);
    } else if (!seg.prev) {
      // First node of open path, make next point the M
      // const {item} = seg;
      const pt = [ next.item.x, next.item.y ];
      replacePathSegMethod(2, seg.next.index, pt);
      list.removeItem(index);
    } else {
      list.removeItem(index);
    }
  }

  /**
  * @param {Integer} index
  * @returns {void}
  */
  removePtFromSelection (index) {
    const pos = this.selected_pts.indexOf(index);
    if (pos === -1) {
      return;
    }
    this.segs[index].select(false);
    this.selected_pts.splice(pos, 1);
  }

  /**
  * @returns {void}
  */
  clearSelection () {
    this.eachSeg(function () {
      // 'this' is the segment here
      this.select(false);
    });
    this.selected_pts = [];
  }

  /**
  * @returns {void}
  */
  storeD () {
    this.last_d = this.elem.getAttribute('d');
  }

  /**
  * @param {Integer} y
  * @returns {Path}
  */
  show (y) {
    // Shows this path's segment grips
    this.eachSeg(function () {
      // 'this' is the segment here
      this.show(y);
    });
    if (y) {
      this.selectPt(this.first_seg.index);
    }
    return this;
  }

  /**
  * Move selected points.
  * @param {Integer} dx
  * @param {Integer} dy
  * @returns {void}
  */
  movePts (dx, dy) {
    let i = this.selected_pts.length;
    while (i--) {
      const seg = this.segs[this.selected_pts[i]];
      seg.move(dx, dy);
    }
  }

  /**
  * @param {Integer} dx
  * @param {Integer} dy
  * @returns {void}
  */
  moveCtrl (dx, dy) {
    const seg = this.segs[this.selected_pts[0]];
    seg.moveCtrl(this.dragctrl, dx, dy);
    if (pathMethodsContext_.getLinkControlPts()) {
      seg.setLinked(this.dragctrl);
    }
  }

  /**
  * @param {?Integer} newType See {@link https://www.w3.org/TR/SVG/single-page.html#paths-InterfaceSVGPathSeg}
  * @returns {void}
  */
  setSegType (newType) {
    this.storeD();
    let i = this.selected_pts.length;
    let text;
    while (i--) {
      const selPt = this.selected_pts[i];

      // Selected seg
      const cur = this.segs[selPt];
      const { prev } = cur;
      if (!prev) { continue; }

      if (!newType) { // double-click, so just toggle
        text = 'Toggle Path Segment Type';

        // Toggle segment to curve/straight line
        const oldType = cur.type;

        newType = (oldType === 6) ? 4 : 6;
      }

      newType = Number(newType);

      const curX = cur.item.x;
      const curY = cur.item.y;
      const prevX = prev.item.x;
      const prevY = prev.item.y;
      let points;
      switch (newType) {
      case 6: {
        if (cur.olditem) {
          const old = cur.olditem;
          points = [ curX, curY, old.x1, old.y1, old.x2, old.y2 ];
        } else {
          const diffX = curX - prevX;
          const diffY = curY - prevY;
          // get control points from straight line segment
          /*
          const ct1x = (prevX + (diffY/2));
          const ct1y = (prevY - (diffX/2));
          const ct2x = (curX + (diffY/2));
          const ct2y = (curY - (diffX/2));
          */
          // create control points on the line to preserve the shape (WRS)
          const ct1x = (prevX + (diffX / 3));
          const ct1y = (prevY + (diffY / 3));
          const ct2x = (curX - (diffX / 3));
          const ct2y = (curY - (diffY / 3));
          points = [ curX, curY, ct1x, ct1y, ct2x, ct2y ];
        }
        break;
      } case 4: {
        points = [ curX, curY ];

        // Store original prevve segment nums
        cur.olditem = cur.item;
        break;
      }
      }

      cur.setType(newType, points);
    }
    const path = pathMethodsContext_.getPathObj();
    path.endChanges(text);
  }

  /**
  * @param {Integer} pt
  * @param {Integer} ctrlNum
  * @returns {void}
  */
  selectPt (pt, ctrlNum) {
    this.clearSelection();
    if (isNullish(pt)) {
      this.eachSeg(function (i) {
        // 'this' is the segment here.
        if (this.prev) {
          pt = i;
        }
      });
    }
    this.addPtsToSelection(pt);
    if (ctrlNum) {
      this.dragctrl = ctrlNum;

      if (pathMethodsContext_.getLinkControlPts()) {
        this.segs[pt].setLinked(ctrlNum);
      }
    }
  }

  /**
  * Update position of all points.
  * @returns {Path}
  */
  update () {
    const { elem } = this;
    if (getRotationAngle(elem)) {
      this.matrix = getMatrix(elem);
      this.imatrix = this.matrix.inverse();
    } else {
      this.matrix = null;
      this.imatrix = null;
    }

    this.eachSeg(function (i) {
      this.item = elem.pathSegList.getItem(i);
      this.update();
    });

    return this;
  }

  /**
  * @param {string} text
  * @returns {void}
  */
  endChanges (text) {
    if (isWebkit()) { editorContext_.resetD(this.elem); }
    const cmd = new ChangeElementCommand(this.elem, { d: this.last_d }, text);
    editorContext_.endChanges({ cmd, elem: this.elem });
  }

  /**
  * @param {Integer|Integer[]} indexes
  * @returns {void}
  */
  addPtsToSelection (indexes) {
    if (!Array.isArray(indexes)) { indexes = [ indexes ]; }
    indexes.forEach((index) => {
      const seg = this.segs[index];
      if (seg.ptgrip && !this.selected_pts.includes(index) && index >= 0) {
        this.selected_pts.push(index);
      }
    });
    this.selected_pts.sort();
    let i = this.selected_pts.length;
    const grips = [];
    grips.length = i;
    // Loop through points to be selected and highlight each
    while (i--) {
      const pt = this.selected_pts[i];
      const seg = this.segs[pt];
      seg.select(true);
      grips[i] = seg.ptgrip;
    }

    const closedSubpath = Path.subpathIsClosed(this.selected_pts[0]);
    editorContext_.addPtsToSelection({ grips, closedSubpath });
  }

  // STATIC
  /**
  * @param {Integer} index
  * @returns {boolean}
  */
  static subpathIsClosed (index) {
    let clsd = false;
    // Check if subpath is already open
    const path = pathMethodsContext_.getPathObj();
    path.eachSeg(function (i) {
      if (i <= index) { return true; }
      if (this.type === 2) {
        // Found M first, so open
        return false;
      }
      if (this.type === 1) {
        // Found Z first, so closed
        clsd = true;
        return false;
      }
      return true;
    });

    return clsd;
  }
}
