/**
 * @license
 * Copyright (c) 2018 Erik Koopmans
 * Released under the MIT License.
 *
 * Licensed under the MIT License.
 * http://opensource.org/licenses/mit-license
 */

import { jsPDF } from "../jspdf.js";
import { normalizeFontFace } from "../libs/fontFace.js";
import { globalObject } from "../libs/globalObject.js";

/**
 * jsPDF html PlugIn
 *
 * @name html
 * @module
 */
(function(jsPDFAPI) {
  "use strict";

  function loadHtml2Canvas() {
    return (function() {
      if (globalObject["html2canvas"]) {
        return Promise.resolve(globalObject["html2canvas"]);
      }

      // @if MODULE_FORMAT='es'
      return import("html2canvas");
      // @endif

      // @if MODULE_FORMAT!='es'
      if (typeof exports === "object" && typeof module !== "undefined") {
        return new Promise(function(resolve, reject) {
          try {
            resolve(require("html2canvas"));
          } catch (e) {
            reject(e);
          }
        });
      }
      if (typeof define === "function" && define.amd) {
        return new Promise(function(resolve, reject) {
          try {
            require(["html2canvas"], resolve);
          } catch (e) {
            reject(e);
          }
        });
      }
      return Promise.reject(new Error("Could not load html2canvas"));
      // @endif
    })()
      .catch(function(e) {
        return Promise.reject(new Error("Could not load html2canvas: " + e));
      })
      .then(function(html2canvas) {
        return html2canvas.default ? html2canvas.default : html2canvas;
      });
  }

  function loadDomPurify() {
    return (function() {
      if (globalObject["DOMPurify"]) {
        return Promise.resolve(globalObject["DOMPurify"]);
      }

      // @if MODULE_FORMAT='es'
      return import("dompurify");
      // @endif

      // @if MODULE_FORMAT!='es'
      if (typeof exports === "object" && typeof module !== "undefined") {
        return new Promise(function(resolve, reject) {
          try {
            resolve(require("dompurify"));
          } catch (e) {
            reject(e);
          }
        });
      }
      if (typeof define === "function" && define.amd) {
        return new Promise(function(resolve, reject) {
          try {
            require(["dompurify"], resolve);
          } catch (e) {
            reject(e);
          }
        });
      }
      return Promise.reject(new Error("Could not load dompurify"));
      // @endif
    })()
      .catch(function(e) {
        return Promise.reject(new Error("Could not load dompurify: " + e));
      })
      .then(function(dompurify) {
        return dompurify.default ? dompurify.default : dompurify;
      });
  }

  /**
   * Determine the type of a variable/object.
   *
   * @private
   * @ignore
   */
  var objType = function(obj) {
    var type = typeof obj;
    if (type === "undefined") return "undefined";
    else if (type === "string" || obj instanceof String) return "string";
    else if (type === "number" || obj instanceof Number) return "number";
    else if (type === "function" || obj instanceof Function) return "function";
    else if (!!obj && obj.constructor === Array) return "array";
    else if (obj && obj.nodeType === 1) return "element";
    else if (type === "object") return "object";
    else return "unknown";
  };

  /**
   * Create an HTML element with optional className, innerHTML, and style.
   *
   * @private
   * @ignore
   */
  var createElement = function(tagName, opt) {
    var el = document.createElement(tagName);
    if (opt.className) el.className = opt.className;
    if (opt.innerHTML && opt.dompurify) {
      el.innerHTML = opt.dompurify.sanitize(opt.innerHTML);
    }
    for (var key in opt.style) {
      el.style[key] = opt.style[key];
    }
    return el;
  };

  /**
   * Deep-clone a node and preserve contents/properties.
   *
   * @private
   * @ignore
   */
  var cloneNode = function(node, javascriptEnabled) {
    // Recursively clone the node.
    var clone =
      node.nodeType === 3
        ? document.createTextNode(node.nodeValue)
        : node.cloneNode(false);
    for (var child = node.firstChild; child; child = child.nextSibling) {
      if (
        javascriptEnabled === true ||
        child.nodeType !== 1 ||
        child.nodeName !== "SCRIPT"
      ) {
        clone.appendChild(cloneNode(child, javascriptEnabled));
      }
    }

    if (node.nodeType === 1) {
      // Preserve contents/properties of special nodes.
      if (node.nodeName === "CANVAS") {
        clone.width = node.width;
        clone.height = node.height;
        clone.getContext("2d").drawImage(node, 0, 0);
      } else if (node.nodeName === "TEXTAREA" || node.nodeName === "SELECT") {
        clone.value = node.value;
      }

      // Preserve the node's scroll position when it loads.
      clone.addEventListener(
        "load",
        function() {
          clone.scrollTop = node.scrollTop;
          clone.scrollLeft = node.scrollLeft;
        },
        true
      );
    }

    // Return the cloned node.
    return clone;
  };

  /* ----- CONSTRUCTOR ----- */

  var Worker = function Worker(opt) {
    // Create the root parent for the proto chain, and the starting Worker.
    var root = Object.assign(
      Worker.convert(Promise.resolve()),
      JSON.parse(JSON.stringify(Worker.template))
    );
    var self = Worker.convert(Promise.resolve(), root);

    // Set progress, optional settings, and return.
    self = self.setProgress(1, Worker, 1, [Worker]);
    self = self.set(opt);
    return self;
  };

  // Boilerplate for subclassing Promise.
  Worker.prototype = Object.create(Promise.prototype);
  Worker.prototype.constructor = Worker;

  // Converts/casts promises into Workers.
  Worker.convert = function convert(promise, inherit) {
    // Uses prototypal inheritance to receive changes made to ancestors' properties.
    promise.__proto__ = inherit || Worker.prototype;
    return promise;
  };

  Worker.template = {
    prop: {
      src: null,
      container: null,
      overlay: null,
      canvas: null,
      img: null,
      pdf: null,
      pageSize: null,
      callback: function() {}
    },
    progress: {
      val: 0,
      state: null,
      n: 0,
      stack: []
    },
    opt: {
      filename: "file.pdf",
      margin: [0, 0, 0, 0],
      enableLinks: true,
      x: 0,
      y: 0,
      html2canvas: {},
      jsPDF: {},
      backgroundColor: "transparent"
    }
  };

  /* ----- FROM / TO ----- */

  Worker.prototype.from = function from(src, type) {
    function getType(src) {
      switch (objType(src)) {
        case "string":
          return "string";
        case "element":
          return src.nodeName.toLowerCase() === "canvas" ? "canvas" : "element";
        default:
          return "unknown";
      }
    }

    return this.then(function from_main() {
      type = type || getType(src);
      switch (type) {
        case "string":
          return this.then(loadDomPurify).then(function(dompurify) {
            return this.set({
              src: createElement("div", {
                innerHTML: src,
                dompurify: dompurify
              })
            });
          });
        case "element":
          return this.set({ src: src });
        case "canvas":
          return this.set({ canvas: src });
        case "img":
          return this.set({ img: src });
        default:
          return this.error("Unknown source type.");
      }
    });
  };

  Worker.prototype.to = function to(target) {
    // Route the 'to' request to the appropriate method.
    switch (target) {
      case "container":
        return this.toContainer();
      case "canvas":
        return this.toCanvas();
      case "img":
        return this.toImg();
      case "pdf":
        return this.toPdf();
      default:
        return this.error("Invalid target.");
    }
  };

  Worker.prototype.toContainer = function toContainer() {
    // Set up function prerequisites.
    var prereqs = [
      function checkSrc() {
        return (
          this.prop.src || this.error("Cannot duplicate - no source HTML.")
        );
      },
      function checkPageSize() {
        return this.prop.pageSize || this.setPageSize();
      }
    ];
    return this.thenList(prereqs).then(function toContainer_main() {
      // Define the CSS styles for the container and its overlay parent.
      var overlayCSS = {
        position: "fixed",
        overflow: "hidden",
        zIndex: 1000,
        left: "-100000px",
        right: 0,
        bottom: 0,
        top: 0
      };
      var containerCSS = {
        position: "relative",
        display: "inline-block",
        width:
          (typeof this.opt.width === "number" &&
          !isNaN(this.opt.width) &&
          typeof this.opt.windowWidth === "number" &&
          !isNaN(this.opt.windowWidth)
            ? this.opt.windowWidth
            : Math.max(
                this.prop.src.clientWidth,
                this.prop.src.scrollWidth,
                this.prop.src.offsetWidth
              )) + "px",
        left: 0,
        right: 0,
        top: 0,
        margin: "auto",
        backgroundColor: this.opt.backgroundColor
      }; // Set the overlay to hidden (could be changed in the future to provide a print preview).

      var source = cloneNode(
        this.prop.src,
        this.opt.html2canvas.javascriptEnabled
      );

      if (source.tagName === "BODY") {
        containerCSS.height =
          Math.max(
            document.body.scrollHeight,
            document.body.offsetHeight,
            document.documentElement.clientHeight,
            document.documentElement.scrollHeight,
            document.documentElement.offsetHeight
          ) + "px";
      }

      this.prop.overlay = createElement("div", {
        className: "html2pdf__overlay",
        style: overlayCSS
      });
      this.prop.container = createElement("div", {
        className: "html2pdf__container",
        style: containerCSS
      });
      this.prop.container.appendChild(source);
      this.prop.container.firstChild.appendChild(
        createElement("div", {
          style: {
            clear: "both",
            border: "0 none transparent",
            margin: 0,
            padding: 0,
            height: 0
          }
        })
      );
      this.prop.container.style.float = "none";
      this.prop.overlay.appendChild(this.prop.container);
      document.body.appendChild(this.prop.overlay);
      this.prop.container.firstChild.style.position = "relative";
      this.prop.container.height =
        Math.max(
          this.prop.container.firstChild.clientHeight,
          this.prop.container.firstChild.scrollHeight,
          this.prop.container.firstChild.offsetHeight
        ) + "px";
    });
  };

  Worker.prototype.toCanvas = function toCanvas() {
    // Set up function prerequisites.
    var prereqs = [
      function checkContainer() {
        return (
          document.body.contains(this.prop.container) || this.toContainer()
        );
      }
    ];

    // Fulfill prereqs then create the canvas.
    return this.thenList(prereqs)
      .then(loadHtml2Canvas)
      .then(function toCanvas_main(html2canvas) {
        // Handle old-fashioned 'onrendered' argument.
        var options = Object.assign({}, this.opt.html2canvas);
        delete options.onrendered;

        return html2canvas(this.prop.container, options);
      })
      .then(function toCanvas_post(canvas) {
        // Handle old-fashioned 'onrendered' argument.
        var onRendered = this.opt.html2canvas.onrendered || function() {};
        onRendered(canvas);

        this.prop.canvas = canvas;
        document.body.removeChild(this.prop.overlay);
      });
  };

  Worker.prototype.toContext2d = function toContext2d() {
    // Set up function prerequisites.
    var prereqs = [
      function checkContainer() {
        return (
          document.body.contains(this.prop.container) || this.toContainer()
        );
      }
    ];

    // Fulfill prereqs then create the canvas.
    return this.thenList(prereqs)
      .then(loadHtml2Canvas)
      .then(function toContext2d_main(html2canvas) {
        // Handle old-fashioned 'onrendered' argument.

        var pdf = this.opt.jsPDF;
        var fontFaces = this.opt.fontFaces;

        var scale =
          typeof this.opt.width === "number" &&
          !isNaN(this.opt.width) &&
          typeof this.opt.windowWidth === "number" &&
          !isNaN(this.opt.windowWidth)
            ? this.opt.width / this.opt.windowWidth
            : 1;

        var options = Object.assign(
          {
            async: true,
            allowTaint: true,
            scale: scale,
            scrollX: this.opt.scrollX || 0,
            scrollY: this.opt.scrollY || 0,
            backgroundColor: "#ffffff",
            imageTimeout: 15000,
            logging: true,
            proxy: null,
            removeContainer: true,
            foreignObjectRendering: false,
            useCORS: false
          },
          this.opt.html2canvas
        );
        delete options.onrendered;

        pdf.context2d.autoPaging =
          typeof this.opt.autoPaging === "undefined"
            ? true
            : this.opt.autoPaging;
        pdf.context2d.posX = this.opt.x;
        pdf.context2d.posY = this.opt.y;
        pdf.context2d.margin = this.opt.margin;
        pdf.context2d.fontFaces = fontFaces;

        if (fontFaces) {
          for (var i = 0; i < fontFaces.length; ++i) {
            var font = fontFaces[i];
            var src = font.src.find(function(src) {
              return src.format === "truetype";
            });

            if (src) {
              pdf.addFont(src.url, font.ref.name, font.ref.style);
            }
          }
        }

        options.windowHeight = options.windowHeight || 0;
        options.windowHeight =
          options.windowHeight == 0
            ? Math.max(
                this.prop.container.clientHeight,
                this.prop.container.scrollHeight,
                this.prop.container.offsetHeight
              )
            : options.windowHeight;

        pdf.context2d.save(true);
        return html2canvas(this.prop.container, options);
      })
      .then(function toContext2d_post(canvas) {
        this.opt.jsPDF.context2d.restore(true);

        // Handle old-fashioned 'onrendered' argument.
        var onRendered = this.opt.html2canvas.onrendered || function() {};
        onRendered(canvas);

        this.prop.canvas = canvas;
        document.body.removeChild(this.prop.overlay);
      });
  };

  Worker.prototype.toImg = function toImg() {
    // Set up function prerequisites.
    var prereqs = [
      function checkCanvas() {
        return this.prop.canvas || this.toCanvas();
      }
    ];

    // Fulfill prereqs then create the image.
    return this.thenList(prereqs).then(function toImg_main() {
      var imgData = this.prop.canvas.toDataURL(
        "image/" + this.opt.image.type,
        this.opt.image.quality
      );
      this.prop.img = document.createElement("img");
      this.prop.img.src = imgData;
    });
  };

  Worker.prototype.toPdf = function toPdf() {
    // Set up function prerequisites.
    var prereqs = [
      function checkContext2d() {
        return this.toContext2d();
      }
      //function checkCanvas() { return this.prop.canvas || this.toCanvas(); }
    ];

    // Fulfill prereqs then create the image.
    return this.thenList(prereqs).then(function toPdf_main() {
      // Create local copies of frequently used properties.
      this.prop.pdf = this.prop.pdf || this.opt.jsPDF;
    });
  };

  /* ----- OUTPUT / SAVE ----- */

  Worker.prototype.output = function output(type, options, src) {
    // Redirect requests to the correct function (outputPdf / outputImg).
    src = src || "pdf";
    if (src.toLowerCase() === "img" || src.toLowerCase() === "image") {
      return this.outputImg(type, options);
    } else {
      return this.outputPdf(type, options);
    }
  };

  Worker.prototype.outputPdf = function outputPdf(type, options) {
    // Set up function prerequisites.
    var prereqs = [
      function checkPdf() {
        return this.prop.pdf || this.toPdf();
      }
    ];

    // Fulfill prereqs then perform the appropriate output.
    return this.thenList(prereqs).then(function outputPdf_main() {
      /* Currently implemented output types:
       *    https://rawgit.com/MrRio/jsPDF/master/docs/jspdf.js.html#line992
       *  save(options), arraybuffer, blob, bloburi/bloburl,
       *  datauristring/dataurlstring, dataurlnewwindow, datauri/dataurl
       */
      return this.prop.pdf.output(type, options);
    });
  };

  Worker.prototype.outputImg = function outputImg(type) {
    // Set up function prerequisites.
    var prereqs = [
      function checkImg() {
        return this.prop.img || this.toImg();
      }
    ];

    // Fulfill prereqs then perform the appropriate output.
    return this.thenList(prereqs).then(function outputImg_main() {
      switch (type) {
        case undefined:
        case "img":
          return this.prop.img;
        case "datauristring":
        case "dataurlstring":
          return this.prop.img.src;
        case "datauri":
        case "dataurl":
          return (document.location.href = this.prop.img.src);
        default:
          throw 'Image output type "' + type + '" is not supported.';
      }
    });
  };

  Worker.prototype.save = function save(filename) {
    // Set up function prerequisites.
    var prereqs = [
      function checkPdf() {
        return this.prop.pdf || this.toPdf();
      }
    ];

    // Fulfill prereqs, update the filename (if provided), and save the PDF.
    return this.thenList(prereqs)
      .set(filename ? { filename: filename } : null)
      .then(function save_main() {
        this.prop.pdf.save(this.opt.filename);
      });
  };

  Worker.prototype.doCallback = function doCallback() {
    // Set up function prerequisites.
    var prereqs = [
      function checkPdf() {
        return this.prop.pdf || this.toPdf();
      }
    ];

    // Fulfill prereqs, update the filename (if provided), and save the PDF.
    return this.thenList(prereqs).then(function doCallback_main() {
      this.prop.callback(this.prop.pdf);
    });
  };

  /* ----- SET / GET ----- */

  Worker.prototype.set = function set(opt) {
    // TODO: Implement ordered pairs?

    // Silently ignore invalid or empty input.
    if (objType(opt) !== "object") {
      return this;
    }

    // Build an array of setter functions to queue.
    var fns = Object.keys(opt || {}).map(function(key) {
      if (key in Worker.template.prop) {
        // Set pre-defined properties.
        return function set_prop() {
          this.prop[key] = opt[key];
        };
      } else {
        switch (key) {
          case "margin":
            return this.setMargin.bind(this, opt.margin);
          case "jsPDF":
            return function set_jsPDF() {
              this.opt.jsPDF = opt.jsPDF;
              return this.setPageSize();
            };
          case "pageSize":
            return this.setPageSize.bind(this, opt.pageSize);
          default:
            // Set any other properties in opt.
            return function set_opt() {
              this.opt[key] = opt[key];
            };
        }
      }
    }, this);

    // Set properties within the promise chain.
    return this.then(function set_main() {
      return this.thenList(fns);
    });
  };

  Worker.prototype.get = function get(key, cbk) {
    return this.then(function get_main() {
      // Fetch the requested property, either as a predefined prop or in opt.
      var val = key in Worker.template.prop ? this.prop[key] : this.opt[key];
      return cbk ? cbk(val) : val;
    });
  };

  Worker.prototype.setMargin = function setMargin(margin) {
    return this.then(function setMargin_main() {
      // Parse the margin property.
      switch (objType(margin)) {
        case "number":
          margin = [margin, margin, margin, margin];
        // eslint-disable-next-line no-fallthrough
        case "array":
          if (margin.length === 2) {
            margin = [margin[0], margin[1], margin[0], margin[1]];
          }
          if (margin.length === 4) {
            break;
          }
        // eslint-disable-next-line no-fallthrough
        default:
          return this.error("Invalid margin array.");
      }

      // Set the margin property, then update pageSize.
      this.opt.margin = margin;
    }).then(this.setPageSize);
  };

  Worker.prototype.setPageSize = function setPageSize(pageSize) {
    function toPx(val, k) {
      return Math.floor(((val * k) / 72) * 96);
    }

    return this.then(function setPageSize_main() {
      // Retrieve page-size based on jsPDF settings, if not explicitly provided.
      pageSize = pageSize || jsPDF.getPageSize(this.opt.jsPDF);

      // Add 'inner' field if not present.
      if (!pageSize.hasOwnProperty("inner")) {
        pageSize.inner = {
          width: pageSize.width - this.opt.margin[1] - this.opt.margin[3],
          height: pageSize.height - this.opt.margin[0] - this.opt.margin[2]
        };
        pageSize.inner.px = {
          width: toPx(pageSize.inner.width, pageSize.k),
          height: toPx(pageSize.inner.height, pageSize.k)
        };
        pageSize.inner.ratio = pageSize.inner.height / pageSize.inner.width;
      }

      // Attach pageSize to this.
      this.prop.pageSize = pageSize;
    });
  };

  Worker.prototype.setProgress = function setProgress(val, state, n, stack) {
    // Immediately update all progress values.
    if (val != null) this.progress.val = val;
    if (state != null) this.progress.state = state;
    if (n != null) this.progress.n = n;
    if (stack != null) this.progress.stack = stack;
    this.progress.ratio = this.progress.val / this.progress.state;

    // Return this for command chaining.
    return this;
  };

  Worker.prototype.updateProgress = function updateProgress(
    val,
    state,
    n,
    stack
  ) {
    // Immediately update all progress values, using setProgress.
    return this.setProgress(
      val ? this.progress.val + val : null,
      state ? state : null,
      n ? this.progress.n + n : null,
      stack ? this.progress.stack.concat(stack) : null
    );
  };

  /* ----- PROMISE MAPPING ----- */

  Worker.prototype.then = function then(onFulfilled, onRejected) {
    // Wrap `this` for encapsulation.
    var self = this;

    return this.thenCore(onFulfilled, onRejected, function then_main(
      onFulfilled,
      onRejected
    ) {
      // Update progress while queuing, calling, and resolving `then`.
      self.updateProgress(null, null, 1, [onFulfilled]);
      return Promise.prototype.then
        .call(this, function then_pre(val) {
          self.updateProgress(null, onFulfilled);
          return val;
        })
        .then(onFulfilled, onRejected)
        .then(function then_post(val) {
          self.updateProgress(1);
          return val;
        });
    });
  };

  Worker.prototype.thenCore = function thenCore(
    onFulfilled,
    onRejected,
    thenBase
  ) {
    // Handle optional thenBase parameter.
    thenBase = thenBase || Promise.prototype.then;

    // Wrap `this` for encapsulation and bind it to the promise handlers.
    var self = this;
    if (onFulfilled) {
      onFulfilled = onFulfilled.bind(self);
    }
    if (onRejected) {
      onRejected = onRejected.bind(self);
    }

    // Cast self into a Promise to avoid polyfills recursively defining `then`.
    var isNative =
      Promise.toString().indexOf("[native code]") !== -1 &&
      Promise.name === "Promise";
    var selfPromise = isNative
      ? self
      : Worker.convert(Object.assign({}, self), Promise.prototype);

    // Return the promise, after casting it into a Worker and preserving props.
    var returnVal = thenBase.call(selfPromise, onFulfilled, onRejected);
    return Worker.convert(returnVal, self.__proto__);
  };

  Worker.prototype.thenExternal = function thenExternal(
    onFulfilled,
    onRejected
  ) {
    // Call `then` and return a standard promise (exits the Worker chain).
    return Promise.prototype.then.call(this, onFulfilled, onRejected);
  };

  Worker.prototype.thenList = function thenList(fns) {
    // Queue a series of promise 'factories' into the promise chain.
    var self = this;
    fns.forEach(function thenList_forEach(fn) {
      self = self.thenCore(fn);
    });
    return self;
  };

  Worker.prototype["catch"] = function(onRejected) {
    // Bind `this` to the promise handler, call `catch`, and return a Worker.
    if (onRejected) {
      onRejected = onRejected.bind(this);
    }
    var returnVal = Promise.prototype["catch"].call(this, onRejected);
    return Worker.convert(returnVal, this);
  };

  Worker.prototype.catchExternal = function catchExternal(onRejected) {
    // Call `catch` and return a standard promise (exits the Worker chain).
    return Promise.prototype["catch"].call(this, onRejected);
  };

  Worker.prototype.error = function error(msg) {
    // Throw the error in the Promise chain.
    return this.then(function error_main() {
      throw new Error(msg);
    });
  };

  /* ----- ALIASES ----- */

  Worker.prototype.using = Worker.prototype.set;
  Worker.prototype.saveAs = Worker.prototype.save;
  Worker.prototype.export = Worker.prototype.output;
  Worker.prototype.run = Worker.prototype.then;

  // Get dimensions of a PDF page, as determined by jsPDF.
  jsPDF.getPageSize = function(orientation, unit, format) {
    // Decode options object
    if (typeof orientation === "object") {
      var options = orientation;
      orientation = options.orientation;
      unit = options.unit || unit;
      format = options.format || format;
    }

    // Default options
    unit = unit || "mm";
    format = format || "a4";
    orientation = ("" + (orientation || "P")).toLowerCase();
    var format_as_string = ("" + format).toLowerCase();

    // Size in pt of various paper formats
    var pageFormats = {
      a0: [2383.94, 3370.39],
      a1: [1683.78, 2383.94],
      a2: [1190.55, 1683.78],
      a3: [841.89, 1190.55],
      a4: [595.28, 841.89],
      a5: [419.53, 595.28],
      a6: [297.64, 419.53],
      a7: [209.76, 297.64],
      a8: [147.4, 209.76],
      a9: [104.88, 147.4],
      a10: [73.7, 104.88],
      b0: [2834.65, 4008.19],
      b1: [2004.09, 2834.65],
      b2: [1417.32, 2004.09],
      b3: [1000.63, 1417.32],
      b4: [708.66, 1000.63],
      b5: [498.9, 708.66],
      b6: [354.33, 498.9],
      b7: [249.45, 354.33],
      b8: [175.75, 249.45],
      b9: [124.72, 175.75],
      b10: [87.87, 124.72],
      c0: [2599.37, 3676.54],
      c1: [1836.85, 2599.37],
      c2: [1298.27, 1836.85],
      c3: [918.43, 1298.27],
      c4: [649.13, 918.43],
      c5: [459.21, 649.13],
      c6: [323.15, 459.21],
      c7: [229.61, 323.15],
      c8: [161.57, 229.61],
      c9: [113.39, 161.57],
      c10: [79.37, 113.39],
      dl: [311.81, 623.62],
      letter: [612, 792],
      "government-letter": [576, 756],
      legal: [612, 1008],
      "junior-legal": [576, 360],
      ledger: [1224, 792],
      tabloid: [792, 1224],
      "credit-card": [153, 243]
    };

    var k;
    // Unit conversion
    switch (unit) {
      case "pt":
        k = 1;
        break;
      case "mm":
        k = 72 / 25.4;
        break;
      case "cm":
        k = 72 / 2.54;
        break;
      case "in":
        k = 72;
        break;
      case "px":
        k = 72 / 96;
        break;
      case "pc":
        k = 12;
        break;
      case "em":
        k = 12;
        break;
      case "ex":
        k = 6;
        break;
      default:
        throw "Invalid unit: " + unit;
    }
    var pageHeight = 0;
    var pageWidth = 0;

    // Dimensions are stored as user units and converted to points on output
    if (pageFormats.hasOwnProperty(format_as_string)) {
      pageHeight = pageFormats[format_as_string][1] / k;
      pageWidth = pageFormats[format_as_string][0] / k;
    } else {
      try {
        pageHeight = format[1];
        pageWidth = format[0];
      } catch (err) {
        throw new Error("Invalid format: " + format);
      }
    }

    var tmp;
    // Handle page orientation
    if (orientation === "p" || orientation === "portrait") {
      orientation = "p";
      if (pageWidth > pageHeight) {
        tmp = pageWidth;
        pageWidth = pageHeight;
        pageHeight = tmp;
      }
    } else if (orientation === "l" || orientation === "landscape") {
      orientation = "l";
      if (pageHeight > pageWidth) {
        tmp = pageWidth;
        pageWidth = pageHeight;
        pageHeight = tmp;
      }
    } else {
      throw "Invalid orientation: " + orientation;
    }

    // Return information (k is the unit conversion ratio from pts)
    var info = {
      width: pageWidth,
      height: pageHeight,
      unit: unit,
      k: k,
      orientation: orientation
    };
    return info;
  };

  /**
   * @typedef FontFace
   *
   * The font-face type implements an interface similar to that of the font-face CSS rule,
   * and is used by jsPDF to match fonts when the font property of CanvasRenderingContext2D
   * is updated.
   *
   * All properties expect values similar to those in the font-face CSS rule. A difference
   * is the font-family, which do not need to be enclosed in double-quotes when containing
   * spaces like in CSS.
   *
   * @property {string} family The name of the font-family.
   * @property {string|undefined} style The style that this font-face defines, e.g. 'italic'.
   * @property {string|number|undefined} weight The weight of the font, either as a string or a number (400, 500, 600, e.g.)
   * @property {string|undefined} stretch The stretch of the font, e.g. condensed, normal, expanded.
   * @property {Object[]} src A list of URLs from where fonts of various formats can be fetched.
   * @property {string} [src] url A URL to a font of a specific format.
   * @property {string} [src] format Format of the font referenced by the URL.
   */

  /**
   * Generate a PDF from an HTML element or string using.
   *
   * @name html
   * @function
   * @param {HTMLElement|string} source The source HTMLElement or a string containing HTML.
   * @param {Object} [options] Collection of settings
   * @param {function} [options.callback] The mandatory callback-function gets as first parameter the current jsPDF instance
   * @param {(number|number[])=} [options.margin] Page margins [top, right, bottom, left]. Default is 0.
   * @param {(boolean|'slice'|'text')=} [options.autoPaging] The auto paging mode.
   * <ul>
   * <li>
   *   <code>false</code>: Auto paging is disabled.
   * </li>
   * <li>
   *   <code>true</code> or <code>'slice'</code>: Will cut shapes or text chunks across page breaks. Will possibly
   *   slice text in half, making it difficult to read.
   * </li>
   * <li>
   *   <code>'text'</code>: Trys not to cut text in half across page breaks. Works best for documents consisting
   *   mostly of a single column of text.
   * </li>
   * </ul>
   * Default is <code>true</code>.
   * @param {string} [options.filename] name of the file
   * @param {HTMLOptionImage} [options.image] image settings when converting HTML to image
   * @param {Html2CanvasOptions} [options.html2canvas] html2canvas options
   * @param {FontFace[]} [options.fontFaces] A list of font-faces to match when resolving fonts. Fonts will be added to the PDF based on the specified URL. If omitted, the font match algorithm falls back to old algorithm.
   * @param {jsPDF} [options.jsPDF] jsPDF instance
   * @param {number=} [options.x] x position on the PDF document in jsPDF units.
   * @param {number=} [options.y] y position on the PDF document in jsPDF units.
   * @param {number=} [options.width] The target width in the PDF document in jsPDF units. The rendered element will be
   * scaled such that it fits into the specified width. Has no effect if either the <code>html2canvas.scale<code> is
   * specified or the <code>windowWidth</code> option is NOT specified.
   * @param {number=} [options.windowWidth] The window width in CSS pixels. In contrast to the
   * <code>html2canvas.windowWidth</code> option, this option affects the actual container size while rendering and
   * does NOT affect CSS media queries. This option only has an effect, if the <code>width<code> option is also specified.
   *
   * @example
   * var doc = new jsPDF();
   *
   * doc.html(document.body, {
   *    callback: function (doc) {
   *      doc.save();
   *    },
   *    x: 10,
   *    y: 10
   * });
   */
  jsPDFAPI.html = function(src, options) {
    "use strict";

    options = options || {};
    options.callback = options.callback || function() {};
    options.html2canvas = options.html2canvas || {};
    options.html2canvas.canvas = options.html2canvas.canvas || this.canvas;
    options.jsPDF = options.jsPDF || this;
    options.fontFaces = options.fontFaces
      ? options.fontFaces.map(normalizeFontFace)
      : null;

    // Create a new worker with the given options.
    var worker = new Worker(options);

    if (!options.worker) {
      // If worker is not set to true, perform the traditional 'simple' operation.
      return worker.from(src).doCallback();
    } else {
      // Otherwise, return the worker for new Promise-based operation.
      return worker;
    }
  };
})(jsPDF.API);
