import EpubCFI from "./epubcfi";
import Hook from "./utils/hook";
import Section from "./section";
import {
  replaceBase,
  replaceCanonical,
  replaceMeta,
} from "./utils/replacements";

/**
 * A collection of Spine Items
 */
class Spine {
  constructor() {
    this.spineItems = [];
    this.spineByHref = {};
    this.spineById = {};

    this.hooks = {};
    this.hooks.serialize = new Hook();
    this.hooks.content = new Hook();

    // Register replacements
    this.hooks.content.register(replaceBase);
    this.hooks.content.register(replaceCanonical);
    this.hooks.content.register(replaceMeta);

    this.epubcfi = new EpubCFI();

    this.loaded = false;

    this.items = undefined;
    this.manifest = undefined;
    this.spineNodeIndex = undefined;
    this.baseUrl = undefined;
    this.length = undefined;
  }

  /**
   * Unpack items from a opf into spine items
   * @param  {Packaging} _package
   * @param  {method} resolver URL resolver
   * @param  {method} canonical Resolve canonical url
   */
  unpack(_package, resolver, canonical) {
    this.items = _package.spine;
    this.manifest = _package.manifest;
    this.spineNodeIndex = _package.spineNodeIndex;
    this.baseUrl = _package.baseUrl || _package.basePath || "";
    this.length = this.items.length;

    this.items.forEach((item, index) => {
      var manifestItem = this.manifest[item.idref];
      var spineItem;

      item.index = index;
      item.cfiBase = this.epubcfi.generateChapterComponent(
        this.spineNodeIndex,
        item.index,
        item.idref
      );

      if (item.href) {
        item.url = resolver(item.href, true);
        item.canonical = canonical(item.href);
      }

      if (manifestItem) {
        item.href = manifestItem.href;
        item.url = resolver(item.href, true);
        item.canonical = canonical(item.href);

        if (manifestItem.properties.length) {
          item.properties.push.apply(item.properties, manifestItem.properties);
        }
      }

      if (item.linear === "yes") {
        item.prev = function () {
          let prevIndex = item.index;
          while (prevIndex > 0) {
            let prev = this.get(prevIndex - 1);
            if (prev && prev.linear) {
              return prev;
            }
            prevIndex -= 1;
          }
          return;
        }.bind(this);
        item.next = function () {
          let nextIndex = item.index;
          while (nextIndex < this.spineItems.length - 1) {
            let next = this.get(nextIndex + 1);
            if (next && next.linear) {
              return next;
            }
            nextIndex += 1;
          }
          return;
        }.bind(this);
      } else {
        item.prev = function () {
          return;
        };
        item.next = function () {
          return;
        };
      }

      spineItem = new Section(item, this.hooks);

      this.append(spineItem);
    });

    this.loaded = true;
  }

  /**
   * Get an item from the spine
   * @param  {string|number} [target]
   * @return {Section} section
   * @example spine.get();
   * @example spine.get(1);
   * @example spine.get("chap1.html");
   * @example spine.get("#id1234");
   */
  get(target) {
    var index = 0;

    if (typeof target === "undefined") {
      while (index < this.spineItems.length) {
        let next = this.spineItems[index];
        if (next && next.linear) {
          break;
        }
        index += 1;
      }
    } else if (this.epubcfi.isCfiString(target)) {
      let cfi = new EpubCFI(target);
      index = cfi.spinePos;
    } else if (typeof target === "number" || isNaN(target) === false) {
      index = target;
    } else if (typeof target === "string" && target.indexOf("#") === 0) {
      index = this.spineById[target.substring(1)];
    } else if (typeof target === "string") {
      // Remove fragments
      target = target.split("#")[0];
      index = this.spineByHref[target] || this.spineByHref[encodeURI(target)];
    }

    return this.spineItems[index] || null;
  }

  /**
   * Append a Section to the Spine
   * @private
   * @param  {Section} section
   */
  append(section) {
    var index = this.spineItems.length;
    section.index = index;

    this.spineItems.push(section);

    // Encode and Decode href lookups
    // see pr for details: https://github.com/futurepress/epub.js/pull/358
    this.spineByHref[decodeURI(section.href)] = index;
    this.spineByHref[encodeURI(section.href)] = index;
    this.spineByHref[section.href] = index;

    this.spineById[section.idref] = index;

    return index;
  }

  /**
   * Prepend a Section to the Spine
   * @private
   * @param  {Section} section
   */
  prepend(section) {
    // var index = this.spineItems.unshift(section);
    this.spineByHref[section.href] = 0;
    this.spineById[section.idref] = 0;

    // Re-index
    this.spineItems.forEach(function (item, index) {
      item.index = index;
    });

    return 0;
  }

  // insert(section, index) {
  //
  // };

  /**
   * Remove a Section from the Spine
   * @private
   * @param  {Section} section
   */
  remove(section) {
    var index = this.spineItems.indexOf(section);

    if (index > -1) {
      delete this.spineByHref[section.href];
      delete this.spineById[section.idref];

      return this.spineItems.splice(index, 1);
    }
  }

  /**
   * Loop over the Sections in the Spine
   * @return {method} forEach
   */
  each() {
    return this.spineItems.forEach.apply(this.spineItems, arguments);
  }

  /**
   * Find the first Section in the Spine
   * @return {Section} first section
   */
  first() {
    let index = 0;

    do {
      let next = this.get(index);

      if (next && next.linear) {
        return next;
      }
      index += 1;
    } while (index < this.spineItems.length);
  }

  /**
   * Find the last Section in the Spine
   * @return {Section} last section
   */
  last() {
    let index = this.spineItems.length - 1;

    do {
      let prev = this.get(index);
      if (prev && prev.linear) {
        return prev;
      }
      index -= 1;
    } while (index >= 0);
  }

  destroy() {
    this.each((section) => section.destroy());

    this.spineItems = undefined;
    this.spineByHref = undefined;
    this.spineById = undefined;

    this.hooks.serialize.clear();
    this.hooks.content.clear();
    this.hooks = undefined;

    this.epubcfi = undefined;

    this.loaded = false;

    this.items = undefined;
    this.manifest = undefined;
    this.spineNodeIndex = undefined;
    this.baseUrl = undefined;
    this.length = undefined;
  }
}

export default Spine;
