import {
  qs,
  qsa,
  querySelectorByType,
  filterChildren,
  getParentByTagName,
} from "./utils/core";

/**
 * Navigation Parser
 * @param {document} xml navigation html / xhtml / ncx
 */
class Navigation {
  constructor(xml) {
    this.toc = [];
    this.tocByHref = {};
    this.tocById = {};

    this.landmarks = [];
    this.landmarksByType = {};

    this.length = 0;
    if (xml) {
      this.parse(xml);
    }
  }

  /**
   * Parse out the navigation items
   * @param {document} xml navigation html / xhtml / ncx
   */
  parse(xml) {
    let isXml = xml.nodeType;
    let html;
    let ncx;

    if (isXml) {
      html = qs(xml, "html");
      ncx = qs(xml, "ncx");
    }

    if (!isXml) {
      this.toc = this.load(xml);
    } else if (html) {
      this.toc = this.parseNav(xml);
      this.landmarks = this.parseLandmarks(xml);
    } else if (ncx) {
      this.toc = this.parseNcx(xml);
    }

    this.length = 0;

    this.unpack(this.toc);
  }

  /**
   * Unpack navigation items
   * @private
   * @param  {array} toc
   */
  unpack(toc) {
    var item;

    for (var i = 0; i < toc.length; i++) {
      item = toc[i];

      if (item.href) {
        this.tocByHref[item.href] = i;
      }

      if (item.id) {
        this.tocById[item.id] = i;
      }

      this.length++;

      if (item.children.length) {
        this.unpack(item.children);
      }
    }
  }

  /**
   * Get an item from the navigation
   * @param  {string} target
   * @return {object} navItem
   */
  get(target) {
    var index;

    if (!target) {
      return this.toc;
    }

    if (target.indexOf("#") === 0) {
      index = this.tocById[target.substring(1)];
    } else if (target in this.tocByHref) {
      index = this.tocByHref[target];
    }

    return this.getByIndex(target, index, this.toc);
  }

  /**
   * Get an item from navigation children recursively by index
   * @param  {string} target
   * @param  {number} index
   * @param  {array} navItems
   * @return {object} navItem
   */
  getByIndex(target, index, navItems) {
    if (navItems.length === 0) {
      return;
    }

    const item = navItems[index];
    if (item && (target === item.id || target === item.href)) {
      return item;
    } else {
      let result;
      for (let i = 0; i < navItems.length; ++i) {
        result = this.getByIndex(target, index, navItems[i].children);
        if (result) {
          break;
        }
      }
      return result;
    }
  }

  /**
   * Get a landmark by type
   * List of types: https://idpf.github.io/epub-vocabs/structure/
   * @param  {string} type
   * @return {object} landmarkItem
   */
  landmark(type) {
    var index;

    if (!type) {
      return this.landmarks;
    }

    index = this.landmarksByType[type];

    return this.landmarks[index];
  }

  /**
   * Parse toc from a Epub > 3.0 Nav
   * @private
   * @param  {document} navHtml
   * @return {array} navigation list
   */
  parseNav(navHtml) {
    var navElement = querySelectorByType(navHtml, "nav", "toc");
    var navItems = navElement ? qsa(navElement, "li") : [];
    var length = navItems.length;
    var i;
    var toc = {};
    var list = [];
    var item, parent;

    if (!navItems || length === 0) return list;

    for (i = 0; i < length; ++i) {
      item = this.navItem(navItems[i]);
      if (item) {
        toc[item.id] = item;
        if (!item.parent) {
          list.push(item);
        } else {
          parent = toc[item.parent];
          parent.children.push(item);
        }
      }
    }

    return list;
  }

  /**
   * Create a navItem
   * @private
   * @param  {element} item
   * @return {object} navItem
   */
  navItem(item) {
    let id = item.getAttribute("id") || undefined;
    let content = filterChildren(item, "a", true);

    if (!content) {
      return;
    }

    let src = content.getAttribute("href") || "";

    if (!id) {
      id = src;
    }
    let text = content.textContent || "";
    let children = [];
    let parentItem = getParentByTagName(item, "li");
    let parent;

    if (parentItem) {
      parent = parentItem.getAttribute("id");
      if (!parent) {
        const parentContent = filterChildren(parentItem, "a", true);
        parent = parentContent && parentContent.getAttribute("href");
      }
    }

    while (!parent && parentItem) {
      parentItem = getParentByTagName(parentItem, "li");
      if (parentItem) {
        parent = parentItem.getAttribute("id");
        if (!parent) {
          const parentContent = filterChildren(parentItem, "a", true);
          parent = parentContent && parentContent.getAttribute("href");
        }
      }
    }

    return {
      id: id,
      href: src,
      label: text,
      children: children,
      parent: parent,
    };
  }

  /**
   * Parse landmarks from a Epub > 3.0 Nav
   * @private
   * @param  {document} navHtml
   * @return {array} landmarks list
   */
  parseLandmarks(navHtml) {
    var navElement = querySelectorByType(navHtml, "nav", "landmarks");
    var navItems = navElement ? qsa(navElement, "li") : [];
    var length = navItems.length;
    var i;
    var list = [];
    var item;

    if (!navItems || length === 0) return list;

    for (i = 0; i < length; ++i) {
      item = this.landmarkItem(navItems[i]);
      if (item) {
        list.push(item);
        this.landmarksByType[item.type] = i;
      }
    }

    return list;
  }

  /**
   * Create a landmarkItem
   * @private
   * @param  {element} item
   * @return {object} landmarkItem
   */
  landmarkItem(item) {
    let content = filterChildren(item, "a", true);

    if (!content) {
      return;
    }

    let type =
      content.getAttributeNS("http://www.idpf.org/2007/ops", "type") ||
      undefined;
    let href = content.getAttribute("href") || "";
    let text = content.textContent || "";

    return {
      href: href,
      label: text,
      type: type,
    };
  }

  /**
   * Parse from a Epub > 3.0 NC
   * @private
   * @param  {document} navHtml
   * @return {array} navigation list
   */
  parseNcx(tocXml) {
    var navPoints = qsa(tocXml, "navPoint");
    var length = navPoints.length;
    var i;
    var toc = {};
    var list = [];
    var item, parent;

    if (!navPoints || length === 0) return list;

    for (i = 0; i < length; ++i) {
      item = this.ncxItem(navPoints[i]);
      toc[item.id] = item;
      if (!item.parent) {
        list.push(item);
      } else {
        parent = toc[item.parent];
        parent.children.push(item);
      }
    }

    return list;
  }

  /**
   * Create a ncxItem
   * @private
   * @param  {element} item
   * @return {object} ncxItem
   */
  ncxItem(item) {
    var id = item.getAttribute("id") || false,
      content = qs(item, "content"),
      src = content.getAttribute("src"),
      navLabel = qs(item, "navLabel"),
      text = navLabel.textContent ? navLabel.textContent : "",
      children = [],
      parentNode = item.parentNode,
      parent;

    if (
      parentNode &&
      (parentNode.nodeName === "navPoint" ||
        parentNode.nodeName.split(":").slice(-1)[0] === "navPoint")
    ) {
      parent = parentNode.getAttribute("id");
    }

    return {
      id: id,
      href: src,
      label: text,
      children: children,
      parent: parent,
    };
  }

  /**
   * Load Spine Items
   * @param  {object} json the items to be loaded
   * @return {Array} navItems
   */
  load(json) {
    return json.map((item) => {
      item.label = item.title;
      item.children = item.children ? this.load(item.children) : [];
      return item;
    });
  }

  /**
   * forEach pass through
   * @param  {Function} fn function to run on each item
   * @return {method} forEach loop
   */
  forEach(fn) {
    return this.toc.forEach(fn);
  }
}

export default Navigation;
