/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

module.metadata = {
  "stability": "stable",
  "engines": {
    // TODO Fennec Support 789757
    "Firefox": "*"
  }
};

const { Cc, Ci } = require("chrome");
const { DataURL } = require("./url");
const errors = require("./deprecated/errors");
const apiUtils = require("./deprecated/api-utils");
/*
While these data flavors resemble Internet media types, they do
no directly map to them.
*/
const kAllowableFlavors = [
  "text/unicode",
  "text/html",
  "image/png"
  /* CURRENTLY UNSUPPORTED FLAVORS
  "text/plain",
  "image/jpg",
  "image/jpeg",
  "image/gif",
  "text/x-moz-text-internal",
  "AOLMAIL",
  "application/x-moz-file",
  "text/x-moz-url",
  "text/x-moz-url-data",
  "text/x-moz-url-desc",
  "text/x-moz-url-priv",
  "application/x-moz-nativeimage",
  "application/x-moz-nativehtml",
  "application/x-moz-file-promise-url",
  "application/x-moz-file-promise-dest-filename",
  "application/x-moz-file-promise",
  "application/x-moz-file-promise-dir"
  */
];

/*
Aliases for common flavors. Not all flavors will
get an alias. New aliases must be approved by a
Jetpack API druid.
*/
const kFlavorMap = [
  { short: "text", long: "text/unicode" },
  { short: "html", long: "text/html" },
  { short: "image", long: "image/png" }
];

let clipboardService = Cc["@mozilla.org/widget/clipboard;1"].
                       getService(Ci.nsIClipboard);

let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].
                      getService(Ci.nsIClipboardHelper);

let imageTools = Cc["@mozilla.org/image/tools;1"].
               getService(Ci.imgITools);

exports.set = function(aData, aDataType) {

  let options = {
    data: aData,
    datatype: aDataType || "text"
  };

  // If `aDataType` is not given or if it's "image", the data is parsed as
  // data URL to detect a better datatype
  if (aData && (!aDataType || aDataType === "image")) {
    try {
      let dataURL = new DataURL(aData);

      options.datatype = dataURL.mimeType;
      options.data = dataURL.data;
    }
    catch (e) {
      // Ignore invalid URIs
      if (e.name !== "URIError") {
        throw e;
      }
    }
  }

  options = apiUtils.validateOptions(options, {
    data: {
      is: ["string"]
    },
    datatype: {
      is: ["string"]
    }
  });

  let flavor = fromJetpackFlavor(options.datatype);

  if (!flavor)
    throw new Error("Invalid flavor for " + options.datatype);

  // Additional checks for using the simple case
  if (flavor == "text/unicode") {
    clipboardHelper.copyString(options.data);
    return true;
  }

  // Below are the more complex cases where we actually have to work with a
  // nsITransferable object
  var xferable = Cc["@mozilla.org/widget/transferable;1"].
                 createInstance(Ci.nsITransferable);
  if (!xferable)
    throw new Error("Couldn't set the clipboard due to an internal error " +
                    "(couldn't create a Transferable object).");
  // Bug 769440: Starting with FF16, transferable have to be inited
  if ("init" in xferable)
    xferable.init(null);

  switch (flavor) {
    case "text/html":
      // add text/html flavor
      let (str = Cc["@mozilla.org/supports-string;1"].
                 createInstance(Ci.nsISupportsString))
      {
        str.data = options.data;
        xferable.addDataFlavor(flavor);
        xferable.setTransferData(flavor, str, str.data.length * 2);
      }

      // add a text/unicode flavor (html converted to plain text)
      let (str = Cc["@mozilla.org/supports-string;1"].
                 createInstance(Ci.nsISupportsString),
           converter = Cc["@mozilla.org/feed-textconstruct;1"].
                       createInstance(Ci.nsIFeedTextConstruct))
      {
        converter.type = "html";
        converter.text = options.data;
        str.data = converter.plainText();
        xferable.addDataFlavor("text/unicode");
        xferable.setTransferData("text/unicode", str, str.data.length * 2);
      }
      break;

    // Set images to the clipboard is not straightforward, to have an idea how
    // it works on platform side, see:
    // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530
    case "image/png":
      let image = options.data;

      let container = {};

      try {
        let input = Cc["@mozilla.org/io/string-input-stream;1"].
                      createInstance(Ci.nsIStringInputStream);

        input.setData(image, image.length);

        imageTools.decodeImageData(input, flavor, container);
      }
      catch (e) {
        throw new Error("Unable to decode data given in a valid image.");
      }

      // Store directly the input stream makes the cliboard's data available
      // for Firefox but not to the others application or to the OS. Therefore,
      // a `nsISupportsInterfacePointer` object that reference an `imgIContainer`
      // with the image is needed.
      var imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].
                     createInstance(Ci.nsISupportsInterfacePointer);

      imgPtr.data = container.value;

      xferable.addDataFlavor(flavor);
      xferable.setTransferData(flavor, imgPtr, -1);

      break;
    default:
      throw new Error("Unable to handle the flavor " + flavor + ".");
  }

  // TODO: Not sure if this will ever actually throw. -zpao
  try {
    clipboardService.setData(
      xferable,
      null,
      clipboardService.kGlobalClipboard
    );
  } catch (e) {
    throw new Error("Couldn't set clipboard data due to an internal error: " + e);
  }
  return true;
};


exports.get = function(aDataType) {
  let options = {
    datatype: aDataType
  };

  // Figure out the best data type for the clipboard's data, if omitted
  if (!aDataType) {
    if (~currentFlavors().indexOf("image"))
      options.datatype = "image";
    else
      options.datatype = "text";
  }

  options = apiUtils.validateOptions(options, {
    datatype: {
      is: ["string"]
    }
  });

  var xferable = Cc["@mozilla.org/widget/transferable;1"].
                 createInstance(Ci.nsITransferable);
  if (!xferable)
    throw new Error("Couldn't set the clipboard due to an internal error " +
                    "(couldn't create a Transferable object).");
  // Bug 769440: Starting with FF16, transferable have to be inited
  if ("init" in xferable)
    xferable.init(null);

  var flavor = fromJetpackFlavor(options.datatype);

  // Ensure that the user hasn't requested a flavor that we don't support.
  if (!flavor)
    throw new Error("Getting the clipboard with the flavor '" + flavor +
                    "' is not supported.");

  // TODO: Check for matching flavor first? Probably not worth it.

  xferable.addDataFlavor(flavor);
  // Get the data into our transferable.
  clipboardService.getData(
    xferable,
    clipboardService.kGlobalClipboard
  );

  var data = {};
  var dataLen = {};
  try {
    xferable.getTransferData(flavor, data, dataLen);
  } catch (e) {
    // Clipboard doesn't contain data in flavor, return null.
    return null;
  }

  // There's no data available, return.
  if (data.value === null)
    return null;

  // TODO: Add flavors here as we support more in kAllowableFlavors.
  switch (flavor) {
    case "text/unicode":
    case "text/html":
      data = data.value.QueryInterface(Ci.nsISupportsString).data;
      break;
    case "image/png":
      let dataURL = new DataURL();

      dataURL.mimeType = flavor;
      dataURL.base64 = true;

      let image = data.value;

      // Due to the differences in how images could be stored in the clipboard
      // the checks below are needed. The clipboard could already provide the
      // image as byte streams, but also as pointer, or as image container.
      // If it's not possible obtain a byte stream, the function returns `null`.
      if (image instanceof Ci.nsISupportsInterfacePointer)
        image = image.data;

      if (image instanceof Ci.imgIContainer)
        image = imageTools.encodeImage(image, flavor);

      if (image instanceof Ci.nsIInputStream) {
        let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
                              createInstance(Ci.nsIBinaryInputStream);

        binaryStream.setInputStream(image);

        dataURL.data = binaryStream.readBytes(binaryStream.available());

        data = dataURL.toString();
      }
      else
        data = null;

      break;
    default:
      data = null;
  }

  return data;
};

function currentFlavors() {
  // Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each.
  // This doesn't seem like the most efficient way, but we can't get
  // confirmation for specific flavors any other way. This is supposed to be
  // an inexpensive call, so performance shouldn't be impacted (much).
  var currentFlavors = [];
  for each (var flavor in kAllowableFlavors) {
    var matches = clipboardService.hasDataMatchingFlavors(
      [flavor],
      1,
      clipboardService.kGlobalClipboard
    );
    if (matches)
      currentFlavors.push(toJetpackFlavor(flavor));
  }
  return currentFlavors;
};

Object.defineProperty(exports, "currentFlavors", { get : currentFlavors });

// SUPPORT FUNCTIONS ////////////////////////////////////////////////////////

function toJetpackFlavor(aFlavor) {
  for each (let flavorMap in kFlavorMap)
    if (flavorMap.long == aFlavor)
      return flavorMap.short;
  // Return null in the case where we don't match
  return null;
}

function fromJetpackFlavor(aJetpackFlavor) {
  // TODO: Handle proper flavors better
  for each (let flavorMap in kFlavorMap)
    if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor)
      return flavorMap.long;
  // Return null in the case where we don't match.
  return null;
}
