import {
  extend,
  type,
  findChildren,
  RangeObject,
  isNumber,
} from "./utils/core";

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const COMMENT_NODE = 8;
const DOCUMENT_NODE = 9;

/**
	* Parsing and creation of EpubCFIs: http://www.idpf.org/epub/linking/cfi/epub-cfi.html

	* Implements:
	* - Character Offset: epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3)
	* - Simple Ranges : epubcfi(/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4)

	* Does Not Implement:
	* - Temporal Offset (~)
	* - Spatial Offset (@)
	* - Temporal-Spatial Offset (~ + @)
	* - Text Location Assertion ([)
	* @class
	@param {string | Range | Node } [cfiFrom]
	@param {string | object} [base]
	@param {string} [ignoreClass] class to ignore when parsing DOM
*/
class EpubCFI {
  constructor(cfiFrom, base, ignoreClass) {
    var type;

    this.str = "";

    this.base = {};
    this.spinePos = 0; // For compatibility

    this.range = false; // true || false;

    this.path = {};
    this.start = null;
    this.end = null;

    // Allow instantiation without the "new" keyword
    if (!(this instanceof EpubCFI)) {
      return new EpubCFI(cfiFrom, base, ignoreClass);
    }

    if (typeof base === "string") {
      this.base = this.parseComponent(base);
    } else if (typeof base === "object" && base.steps) {
      this.base = base;
    }

    type = this.checkType(cfiFrom);

    if (type === "string") {
      this.str = cfiFrom;
      return extend(this, this.parse(cfiFrom));
    } else if (type === "range") {
      return extend(this, this.fromRange(cfiFrom, this.base, ignoreClass));
    } else if (type === "node") {
      return extend(this, this.fromNode(cfiFrom, this.base, ignoreClass));
    } else if (type === "EpubCFI" && cfiFrom.path) {
      return cfiFrom;
    } else if (!cfiFrom) {
      return this;
    } else {
      throw new TypeError("not a valid argument for EpubCFI");
    }
  }

  /**
   * Check the type of constructor input
   * @private
   */
  checkType(cfi) {
    if (this.isCfiString(cfi)) {
      return "string";
      // Is a range object
    } else if (
      cfi &&
      typeof cfi === "object" &&
      (type(cfi) === "Range" || typeof cfi.startContainer != "undefined")
    ) {
      return "range";
    } else if (
      cfi &&
      typeof cfi === "object" &&
      typeof cfi.nodeType != "undefined"
    ) {
      // || typeof cfi === "function"
      return "node";
    } else if (cfi && typeof cfi === "object" && cfi instanceof EpubCFI) {
      return "EpubCFI";
    } else {
      return false;
    }
  }

  /**
   * Parse a cfi string to a CFI object representation
   * @param {string} cfiStr
   * @returns {object} cfi
   */
  parse(cfiStr) {
    var cfi = {
      spinePos: -1,
      range: false,
      base: {},
      path: {},
      start: null,
      end: null,
    };
    var baseComponent, pathComponent, range;

    if (typeof cfiStr !== "string") {
      return { spinePos: -1 };
    }

    if (cfiStr.indexOf("epubcfi(") === 0 && cfiStr[cfiStr.length - 1] === ")") {
      // Remove intial epubcfi( and ending )
      cfiStr = cfiStr.slice(8, cfiStr.length - 1);
    }

    baseComponent = this.getChapterComponent(cfiStr);

    // Make sure this is a valid cfi or return
    if (!baseComponent) {
      return { spinePos: -1 };
    }

    cfi.base = this.parseComponent(baseComponent);

    pathComponent = this.getPathComponent(cfiStr);
    cfi.path = this.parseComponent(pathComponent);

    range = this.getRange(cfiStr);

    if (range) {
      cfi.range = true;
      cfi.start = this.parseComponent(range[0]);
      cfi.end = this.parseComponent(range[1]);
    }

    // Get spine node position
    // cfi.spineSegment = cfi.base.steps[1];

    // Chapter segment is always the second step
    cfi.spinePos = cfi.base.steps[1].index;

    return cfi;
  }

  parseComponent(componentStr) {
    var component = {
      steps: [],
      terminal: {
        offset: null,
        assertion: null,
      },
    };
    var parts = componentStr.split(":");
    var steps = parts[0].split("/");
    var terminal;

    if (parts.length > 1) {
      terminal = parts[1];
      component.terminal = this.parseTerminal(terminal);
    }

    if (steps[0] === "") {
      steps.shift(); // Ignore the first slash
    }

    component.steps = steps.map(
      function (step) {
        return this.parseStep(step);
      }.bind(this)
    );

    return component;
  }

  parseStep(stepStr) {
    var type, num, index, has_brackets, id;

    has_brackets = stepStr.match(/\[(.*)\]/);
    if (has_brackets && has_brackets[1]) {
      id = has_brackets[1];
    }

    //-- Check if step is a text node or element
    num = parseInt(stepStr);

    if (isNaN(num)) {
      return;
    }

    if (num % 2 === 0) {
      // Even = is an element
      type = "element";
      index = num / 2 - 1;
    } else {
      type = "text";
      index = (num - 1) / 2;
    }

    return {
      type: type,
      index: index,
      id: id || null,
    };
  }

  parseTerminal(termialStr) {
    var characterOffset, textLocationAssertion;
    var assertion = termialStr.match(/\[(.*)\]/);

    if (assertion && assertion[1]) {
      characterOffset = parseInt(termialStr.split("[")[0]);
      textLocationAssertion = assertion[1];
    } else {
      characterOffset = parseInt(termialStr);
    }

    if (!isNumber(characterOffset)) {
      characterOffset = null;
    }

    return {
      offset: characterOffset,
      assertion: textLocationAssertion,
    };
  }

  getChapterComponent(cfiStr) {
    var indirection = cfiStr.split("!");

    return indirection[0];
  }

  getPathComponent(cfiStr) {
    var indirection = cfiStr.split("!");

    if (indirection[1]) {
      let ranges = indirection[1].split(",");
      return ranges[0];
    }
  }

  getRange(cfiStr) {
    var ranges = cfiStr.split(",");

    if (ranges.length === 3) {
      return [ranges[1], ranges[2]];
    }

    return false;
  }

  getCharecterOffsetComponent(cfiStr) {
    var splitStr = cfiStr.split(":");
    return splitStr[1] || "";
  }

  joinSteps(steps) {
    if (!steps) {
      return "";
    }

    return steps
      .map(function (part) {
        var segment = "";

        if (part.type === "element") {
          segment += (part.index + 1) * 2;
        }

        if (part.type === "text") {
          segment += 1 + 2 * part.index; // TODO: double check that this is odd
        }

        if (part.id) {
          segment += "[" + part.id + "]";
        }

        return segment;
      })
      .join("/");
  }

  segmentString(segment) {
    var segmentString = "/";

    segmentString += this.joinSteps(segment.steps);

    if (segment.terminal && segment.terminal.offset != null) {
      segmentString += ":" + segment.terminal.offset;
    }

    if (segment.terminal && segment.terminal.assertion != null) {
      segmentString += "[" + segment.terminal.assertion + "]";
    }

    return segmentString;
  }

  /**
   * Convert CFI to a epubcfi(...) string
   * @returns {string} epubcfi
   */
  toString() {
    var cfiString = "epubcfi(";

    cfiString += this.segmentString(this.base);

    cfiString += "!";
    cfiString += this.segmentString(this.path);

    // Add Range, if present
    if (this.range && this.start) {
      cfiString += ",";
      cfiString += this.segmentString(this.start);
    }

    if (this.range && this.end) {
      cfiString += ",";
      cfiString += this.segmentString(this.end);
    }

    cfiString += ")";

    return cfiString;
  }

  /**
   * Compare which of two CFIs is earlier in the text
   * @returns {number} First is earlier = -1, Second is earlier = 1, They are equal = 0
   */
  compare(cfiOne, cfiTwo) {
    var stepsA, stepsB;
    var terminalA, terminalB;

    var rangeAStartSteps, rangeAEndSteps;
    var rangeBEndSteps, rangeBEndSteps;
    var rangeAStartTerminal, rangeAEndTerminal;
    var rangeBStartTerminal, rangeBEndTerminal;

    if (typeof cfiOne === "string") {
      cfiOne = new EpubCFI(cfiOne);
    }
    if (typeof cfiTwo === "string") {
      cfiTwo = new EpubCFI(cfiTwo);
    }
    // Compare Spine Positions
    if (cfiOne.spinePos > cfiTwo.spinePos) {
      return 1;
    }
    if (cfiOne.spinePos < cfiTwo.spinePos) {
      return -1;
    }

    if (cfiOne.range) {
      stepsA = cfiOne.path.steps.concat(cfiOne.start.steps);
      terminalA = cfiOne.start.terminal;
    } else {
      stepsA = cfiOne.path.steps;
      terminalA = cfiOne.path.terminal;
    }

    if (cfiTwo.range) {
      stepsB = cfiTwo.path.steps.concat(cfiTwo.start.steps);
      terminalB = cfiTwo.start.terminal;
    } else {
      stepsB = cfiTwo.path.steps;
      terminalB = cfiTwo.path.terminal;
    }

    // Compare Each Step in the First item
    for (var i = 0; i < stepsA.length; i++) {
      if (!stepsA[i]) {
        return -1;
      }
      if (!stepsB[i]) {
        return 1;
      }
      if (stepsA[i].index > stepsB[i].index) {
        return 1;
      }
      if (stepsA[i].index < stepsB[i].index) {
        return -1;
      }
      // Otherwise continue checking
    }

    // All steps in First equal to Second and First is Less Specific
    if (stepsA.length < stepsB.length) {
      return -1;
    }

    // Compare the charecter offset of the text node
    if (terminalA.offset > terminalB.offset) {
      return 1;
    }
    if (terminalA.offset < terminalB.offset) {
      return -1;
    }

    // CFI's are equal
    return 0;
  }

  step(node) {
    var nodeType = node.nodeType === TEXT_NODE ? "text" : "element";

    return {
      id: node.id,
      tagName: node.tagName,
      type: nodeType,
      index: this.position(node),
    };
  }

  filteredStep(node, ignoreClass) {
    var filteredNode = this.filter(node, ignoreClass);
    var nodeType;

    // Node filtered, so ignore
    if (!filteredNode) {
      return;
    }

    // Otherwise add the filter node in
    nodeType = filteredNode.nodeType === TEXT_NODE ? "text" : "element";

    return {
      id: filteredNode.id,
      tagName: filteredNode.tagName,
      type: nodeType,
      index: this.filteredPosition(filteredNode, ignoreClass),
    };
  }

  pathTo(node, offset, ignoreClass) {
    var segment = {
      steps: [],
      terminal: {
        offset: null,
        assertion: null,
      },
    };
    var currentNode = node;
    var step;

    while (
      currentNode &&
      currentNode.parentNode &&
      // currentNode.parentNode.id != DOCUMENT_NODE &&
      currentNode.tagName != "EVE-VIEW-HTML"
    ) {
      if (ignoreClass) {
        step = this.filteredStep(currentNode, ignoreClass);
      } else {
        step = this.step(currentNode);
      }

      if (step) {
        segment.steps.unshift(step);
      }

      currentNode = currentNode.parentNode;
    }

    if (offset != null && offset >= 0) {
      segment.terminal.offset = offset;

      // Make sure we are getting to a textNode if there is an offset
      if (segment.steps[segment.steps.length - 1].type != "text") {
        segment.steps.push({
          type: "text",
          index: 0,
        });
      }
    }

    return segment;
  }

  equalStep(stepA, stepB) {
    if (!stepA || !stepB) {
      return false;
    }

    if (
      stepA.index === stepB.index &&
      stepA.id === stepB.id &&
      stepA.type === stepB.type
    ) {
      return true;
    }

    return false;
  }

  /**
   * Create a CFI object from a Range
   * @param {Range} range
   * @param {string | object} base
   * @param {string} [ignoreClass]
   * @returns {object} cfi
   */
  fromRange(range, base, ignoreClass) {
    var cfi = {
      range: false,
      base: {},
      path: {},
      start: null,
      end: null,
    };

    var start = range.startContainer;
    var end = range.endContainer;

    var startOffset = range.startOffset;
    var endOffset = range.endOffset;

    var needsIgnoring = false;

    if (ignoreClass) {
      // Tell pathTo if / what to ignore
      needsIgnoring =
        start.ownerDocument.querySelector("." + ignoreClass) != null;
    }

    if (typeof base === "string") {
      cfi.base = this.parseComponent(base);
      cfi.spinePos = cfi.base.steps[1].index;
    } else if (typeof base === "object") {
      cfi.base = base;
    }

    if (range.collapsed) {
      if (needsIgnoring) {
        startOffset = this.patchOffset(start, startOffset, ignoreClass);
      }
      cfi.path = this.pathTo(start, startOffset, ignoreClass);
    } else {
      cfi.range = true;

      if (needsIgnoring) {
        startOffset = this.patchOffset(start, startOffset, ignoreClass);
      }

      cfi.start = this.pathTo(start, startOffset, ignoreClass);
      if (needsIgnoring) {
        endOffset = this.patchOffset(end, endOffset, ignoreClass);
      }

      cfi.end = this.pathTo(end, endOffset, ignoreClass);

      // Create a new empty path
      cfi.path = {
        steps: [],
        terminal: null,
      };

      // Push steps that are shared between start and end to the common path
      var len = cfi.start.steps.length;
      var i;

      for (i = 0; i < len; i++) {
        if (this.equalStep(cfi.start.steps[i], cfi.end.steps[i])) {
          if (i === len - 1) {
            // Last step is equal, check terminals
            if (cfi.start.terminal === cfi.end.terminal) {
              // CFI's are equal
              cfi.path.steps.push(cfi.start.steps[i]);
              // Not a range
              cfi.range = false;
            }
          } else {
            cfi.path.steps.push(cfi.start.steps[i]);
          }
        } else {
          break;
        }
      }

      cfi.start.steps = cfi.start.steps.slice(cfi.path.steps.length);
      cfi.end.steps = cfi.end.steps.slice(cfi.path.steps.length);

      // TODO: Add Sanity check to make sure that the end if greater than the start
    }

    return cfi;
  }

  /**
   * Create a CFI object from a Node
   * @param {Node} anchor
   * @param {string | object} base
   * @param {string} [ignoreClass]
   * @returns {object} cfi
   */
  fromNode(anchor, base, ignoreClass) {
    var cfi = {
      range: false,
      base: {},
      path: {},
      start: null,
      end: null,
    };

    if (typeof base === "string") {
      cfi.base = this.parseComponent(base);
      cfi.spinePos = cfi.base.steps[1].index;
    } else if (typeof base === "object") {
      cfi.base = base;
    }

    cfi.path = this.pathTo(anchor, null, ignoreClass);

    return cfi;
  }

  filter(anchor, ignoreClass) {
    var needsIgnoring;
    var sibling; // to join with
    var parent, previousSibling, nextSibling;
    var isText = false;

    if (anchor.nodeType === TEXT_NODE) {
      isText = true;
      parent = anchor.parentNode;
      needsIgnoring = anchor.parentNode.classList.contains(ignoreClass);
    } else {
      isText = false;
      needsIgnoring = anchor.classList.contains(ignoreClass);
    }

    if (needsIgnoring && isText) {
      previousSibling = parent.previousSibling;
      nextSibling = parent.nextSibling;

      // If the sibling is a text node, join the nodes
      if (previousSibling && previousSibling.nodeType === TEXT_NODE) {
        sibling = previousSibling;
      } else if (nextSibling && nextSibling.nodeType === TEXT_NODE) {
        sibling = nextSibling;
      }

      if (sibling) {
        return sibling;
      } else {
        // Parent will be ignored on next step
        return anchor;
      }
    } else if (needsIgnoring && !isText) {
      // Otherwise just skip the element node
      return false;
    } else {
      // No need to filter
      return anchor;
    }
  }

  patchOffset(anchor, offset, ignoreClass) {
    if (anchor.nodeType != TEXT_NODE) {
      throw new Error("Anchor must be a text node");
    }

    var curr = anchor;
    var totalOffset = offset;

    // If the parent is a ignored node, get offset from it's start
    if (anchor.parentNode.classList.contains(ignoreClass)) {
      curr = anchor.parentNode;
    }

    while (curr.previousSibling) {
      if (curr.previousSibling.nodeType === ELEMENT_NODE) {
        // Originally a text node, so join
        if (curr.previousSibling.classList.contains(ignoreClass)) {
          totalOffset += curr.previousSibling.textContent.length;
        } else {
          break; // Normal node, dont join
        }
      } else {
        // If the previous sibling is a text node, join the nodes
        totalOffset += curr.previousSibling.textContent.length;
      }

      curr = curr.previousSibling;
    }

    return totalOffset;
  }

  normalizedMap(children, nodeType, ignoreClass) {
    var output = {};
    var prevIndex = -1;
    var i,
      len = children.length;
    var currNodeType;
    var prevNodeType;

    for (i = 0; i < len; i++) {
      currNodeType = children[i].nodeType;

      // Check if needs ignoring
      if (
        currNodeType === ELEMENT_NODE &&
        children[i].classList.contains(ignoreClass)
      ) {
        currNodeType = TEXT_NODE;
      }

      if (i > 0 && currNodeType === TEXT_NODE && prevNodeType === TEXT_NODE) {
        // join text nodes
        output[i] = prevIndex;
      } else if (nodeType === currNodeType) {
        prevIndex = prevIndex + 1;
        output[i] = prevIndex;
      }

      prevNodeType = currNodeType;
    }

    return output;
  }

  position(anchor) {
    var children, index;
    if (anchor.nodeType === ELEMENT_NODE) {
      children = anchor.parentNode.children;
      if (!children) {
        children = findChildren(anchor.parentNode);
      }
      index = Array.prototype.indexOf.call(children, anchor);
    } else {
      children = this.textNodes(anchor.parentNode);
      index = children.indexOf(anchor);
    }

    return index;
  }

  filteredPosition(anchor, ignoreClass) {
    var children, index, map;

    if (anchor.nodeType === ELEMENT_NODE) {
      children = anchor.parentNode.children;
      map = this.normalizedMap(children, ELEMENT_NODE, ignoreClass);
    } else {
      children = anchor.parentNode.childNodes;
      // Inside an ignored node
      if (anchor.parentNode.classList.contains(ignoreClass)) {
        anchor = anchor.parentNode;
        children = anchor.parentNode.childNodes;
      }
      map = this.normalizedMap(children, TEXT_NODE, ignoreClass);
    }

    index = Array.prototype.indexOf.call(children, anchor);

    return map[index];
  }

  stepsToXpath(steps) {
    var xpath = [".", "*"];

    steps.forEach(function (step) {
      var position = step.index + 1;

      if (step.id) {
        xpath.push("*[position()=" + position + " and @id='" + step.id + "']");
      } else if (step.type === "text") {
        xpath.push("text()[" + position + "]");
      } else {
        xpath.push("*[" + position + "]");
      }
    });

    return xpath.join("/");
  }

  /*

	To get the last step if needed:

	// Get the terminal step
	lastStep = steps[steps.length-1];
	// Get the query string
	query = this.stepsToQuery(steps);
	// Find the containing element
	startContainerParent = doc.querySelector(query);
	// Find the text node within that element
	if(startContainerParent && lastStep.type == "text") {
		container = startContainerParent.childNodes[lastStep.index];
	}
	*/
  stepsToQuerySelector(steps) {
    var query = ["html"];

    steps.forEach(function (step) {
      var position = step.index + 1;

      if (step.id) {
        query.push("#" + step.id);
      } else if (step.type === "text") {
        // unsupported in querySelector
        // query.push("text()[" + position + "]");
      } else {
        query.push("*:nth-child(" + position + ")");
      }
    });

    return query.join(">");
  }

  textNodes(container, ignoreClass) {
    return Array.prototype.slice
      .call(container.childNodes)
      .filter(function (node) {
        if (node.nodeType === TEXT_NODE) {
          return true;
        } else if (ignoreClass && node.classList.contains(ignoreClass)) {
          return true;
        }
        return false;
      });
  }

  walkToNode(steps, node, ignoreClass) {
    var doc = document;
    var container = node;
    var children;
    var step;
    var len = steps.length;
    var i;

    for (i = 0; i < len; i++) {
      step = steps[i];

      if (step.type === "element") {
        //better to get a container using id as some times step.index may not be correct
        //For ex.https://github.com/futurepress/epub.js/issues/561
        if (step.id) {
          container = doc.getElementById(step.id);
        } else {
          children = findChildren(container);
          container = children[step.index];
        }
      } else if (step.type === "text") {
        container = this.textNodes(container, ignoreClass)[step.index];
      }
      if (!container) {
        //Break the for loop as due to incorrect index we can get error if
        //container is undefined so that other functionailties works fine
        //like navigation
        break;
      }
    }

    return container;
  }

  findNode(steps, _doc, ignoreClass) {
    var doc = _doc || document;
    var container;
    var xpath;

    if (!ignoreClass && typeof doc.evaluate != "undefined") {
      xpath = this.stepsToXpath(steps);
      container = doc.evaluate(
        xpath,
        doc,
        null,
        XPathResult.FIRST_ORDERED_NODE_TYPE,
        null
      ).singleNodeValue;
    } else if (ignoreClass) {
      container = this.walkToNode(steps, doc, ignoreClass);
    } else {
      container = this.walkToNode(steps, doc);
    }

    return container;
  }

  fixMiss(steps, offset, _doc, ignoreClass) {
    var container = this.findNode(steps.slice(0, -1), _doc, ignoreClass);
    var children = container.childNodes;
    var map = this.normalizedMap(children, TEXT_NODE, ignoreClass);
    var child;
    var len;
    var lastStepIndex = steps[steps.length - 1].index;

    for (let childIndex in map) {
      if (!map.hasOwnProperty(childIndex)) return;

      if (map[childIndex] === lastStepIndex) {
        child = children[childIndex];
        len = child.textContent.length;
        if (offset > len) {
          offset = offset - len;
        } else {
          if (child.nodeType === ELEMENT_NODE) {
            container = child.childNodes[0];
          } else {
            container = child;
          }
          break;
        }
      }
    }

    return {
      container: container,
      offset: offset,
    };
  }

  /**
   * Creates a DOM range representing a CFI
   * @param {document} _doc document referenced in the base
   * @param {string} [ignoreClass]
   * @return {Range}
   */
  toRange(node, ignoreClass) {
    let range;
    let start, end, startContainer, endContainer;
    var cfi = this;
    var startSteps, endSteps;
    var needsIgnoring = ignoreClass
      ? doc.querySelector("." + ignoreClass) != null
      : false;
    var missed;

    // if (typeof(doc.createRange) !== "undefined") {
    // range = doc.createRange();
    // } else {
    range = document.createRange();
    // }
    // console.log(cfi)

    if (cfi.range) {
      start = cfi.start;
      startSteps = cfi.path.steps.concat(start.steps);
      startContainer = this.findNode(
        startSteps,
        node,
        needsIgnoring ? ignoreClass : null
      );
      end = cfi.end;
      endSteps = cfi.path.steps.concat(end.steps);
      endContainer = this.findNode(
        endSteps,
        node,
        needsIgnoring ? ignoreClass : null
      );
    } else {
      start = cfi.path;
      startSteps = cfi.path.steps;
      startContainer = this.findNode(
        cfi.path.steps,
        node,
        needsIgnoring ? ignoreClass : null
      );
    }

    if (startContainer) {
      try {
        if (start.terminal.offset != null) {
          range.setStart(startContainer, start.terminal.offset);
        } else {
          range.setStart(startContainer, 0);
        }
      } catch (e) {
        missed = this.fixMiss(
          startSteps,
          start.terminal.offset,
          node,
          needsIgnoring ? ignoreClass : null
        );
        range.setStart(missed.container, missed.offset);
      }
    } else {
      console.log("No startContainer found for", this.toString());
      // No start found
      return null;
    }

    if (endContainer) {
      try {
        if (end.terminal.offset != null) {
          range.setEnd(endContainer, end.terminal.offset);
        } else {
          range.setEnd(endContainer, 0);
        }
      } catch (e) {
        missed = this.fixMiss(
          endSteps,
          cfi.end.terminal.offset,
          doc,
          needsIgnoring ? ignoreClass : null
        );
        range.setEnd(missed.container, missed.offset);
      }
    }

    // doc.defaultView.getSelection().addRange(range);
    return range;
  }

  /**
   * Check if a string is wrapped with "epubcfi()"
   * @param {string} str
   * @returns {boolean}
   */
  isCfiString(str) {
    if (
      typeof str === "string" &&
      str.indexOf("epubcfi(") === 0 &&
      str[str.length - 1] === ")"
    ) {
      return true;
    }

    return false;
  }

  generateChapterComponent(_spineNodeIndex, _pos, id) {
    var pos = parseInt(_pos),
      spineNodeIndex = (_spineNodeIndex + 1) * 2,
      cfi = "/" + spineNodeIndex + "/";

    cfi += (pos + 1) * 2;

    if (id) {
      cfi += "[" + id + "]";
    }

    return cfi;
  }

  /**
   * Collapse a CFI Range to a single CFI Position
   * @param {boolean} [toStart=false]
   */
  collapse(toStart) {
    if (!this.range) {
      return;
    }

    this.range = false;

    if (toStart) {
      this.path.steps = this.path.steps.concat(this.start.steps);
      this.path.terminal = this.start.terminal;
    } else {
      this.path.steps = this.path.steps.concat(this.end.steps);
      this.path.terminal = this.end.terminal;
    }
  }
}

export default EpubCFI;
