//"use strict";

import {Promise} from "es6-promise";
import {DataTypes} from "./DataTypes"
import DT from "./DT";

interface IEWindowClipbardData {
  setData: (key: string, value: string) => boolean;
  getData: (key: string) => string | undefined;
}

interface IEWindow extends Window {
  clipboardData: IEWindowClipbardData
}

export default class ClipboardPolyfill {
  private static DEBUG: boolean = false;
  private static misingPlainTextWarning = true;
  public static DT = DT;

  // TODO: Compile debug logging code out of release builds?
  public static enableDebugLogging() {
    this.DEBUG = true;
  }

  private static suppressMissingPlainTextWarning() {
    this.misingPlainTextWarning = false;
  }

  protected static copyListener(tracker: FallbackTracker, data: DT, e: ClipboardEvent): void {
    if (this.DEBUG) (console.info || console.log).call(console, "listener called");
    tracker.listenerCalled = true;
    data.forEach((value: string, key: string) => {
      e.clipboardData.setData(key, value);
      if (key === DataTypes.TEXT_PLAIN && e.clipboardData.getData(key) != value) {
        if (this.DEBUG) (console.info || console.log).call(console, "Setting text/plain failed.");
        tracker.listenerSetPlainTextFailed = true;
      }
    });
    e.preventDefault();
  }

  protected static execCopy(data: DT): FallbackTracker {
    var tracker = new FallbackTracker();
    var listener = this.copyListener.bind(this, tracker, data);

    document.addEventListener("copy", listener);
    try {
      tracker.execCommandReturnedTrue = document.execCommand("copy");
    } finally {
      document.removeEventListener("copy", listener);
    }
    return tracker;
  }

  // Create a temporary DOM element to select, so that `execCommand()` is not
  // rejected.
  private static copyUsingTempSelection(e: HTMLElement, data: DT): FallbackTracker {
    Selection.select(e);
    var tracker = this.execCopy(data);
    Selection.clear();
    return tracker;
  }

  // Create a temporary DOM element to select, so that `execCommand()` is not
  // rejected.
  private static copyUsingTempElem(data: DT): FallbackTracker {
    var tempElem = document.createElement("div");
    // Place some text in the elem so that Safari has something to select.
    tempElem.textContent = "temporary element";
    document.body.appendChild(tempElem);

    var tracker = this.copyUsingTempSelection(tempElem, data);

    document.body.removeChild(tempElem);
    return tracker;
  }

  // Uses shadow DOM.
  private static copyTextUsingDOM(str: string): boolean {
    if (this.DEBUG) (console.info || console.log).call(console, "attempting to copy text using DOM");

    var tempElem = document.createElement("div");
    var shadowRoot = tempElem.attachShadow({mode: "open"});
    document.body.appendChild(tempElem);

    var span = document.createElement("span");
    span.textContent = str;
    span.style.whiteSpace = "pre-wrap"; // TODO: Use `innerText` above instead?
    shadowRoot.appendChild(span);
    Selection.select(span);

    var result = document.execCommand("copy");

    // Selection.clear();
    document.body.removeChild(tempElem);

    return result;
  }

  public static writeIE(data: DT): boolean {
    // IE supports text or URL, but not HTML: https://msdn.microsoft.com/en-us/library/ms536744(v=vs.85).aspx
    // TODO: Write URLs to `text/uri-list`? https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types
    var text = data.getData("text/plain");
    if (text !== undefined) {
      return (window as IEWindow).clipboardData.setData("Text", text);
    }

    throw ("No `text/plain` value was specified.");
  }

  public static write(data: DT): Promise<void> {
    if (this.misingPlainTextWarning && !data.getData(DataTypes.TEXT_PLAIN)) {
      (console.warn || console.log).call(console,
        "[clipboard.js] clipboard.write() was called without a "+
        "`text/plain` data type. On some platforms, this may result in an "+
        "empty clipboard. Call clipboard.suppressMissingPlainTextWarning() "+
        "to suppress this warning.");
    }

    // TODO: Allow fallback graph other than a single line.

    return new Promise<void>((resolve, reject) => {
      // Internet Explorer
      if (typeof ClipboardEvent === "undefined" &&
          typeof (window as IEWindow).clipboardData !== "undefined" &&
          typeof (window as IEWindow).clipboardData.setData !== "undefined") {
        if (this.writeIE(data)) {
          resolve()
        } else {
          reject(new Error("Copying failed, possibly because the user rejected it."));
        }
        return;
      }

      var tracker = this.execCopy(data);
      if (tracker.listenerCalled && !tracker.listenerSetPlainTextFailed) {
        if (this.DEBUG) (console.info || console.log).call(console, "Regular copy command succeeded.");
        resolve();
        return;
      }

      // Success detection on Edge is not possible, due to bugs in all 4
      // detection mechanisms we could try to use. Assume success.
      if (tracker.listenerCalled && navigator.userAgent.indexOf("Edge") > -1) {
        if (this.DEBUG) (console.info || console.log).call(console, "User agent contains \"Edge\". Blindly assuming success.");
        resolve();
        return;
      }

      // Fallback 1 for desktop Safari.
      tracker = this.copyUsingTempSelection(document.body, data);
      if (tracker.listenerCalled && !tracker.listenerSetPlainTextFailed) {
        if (this.DEBUG) (console.info || console.log).call(console, "Copied using temporary document.body selection.");
        resolve();
        return;
      }

      // Fallback 2 for desktop Safari. 
      tracker = this.copyUsingTempElem(data);
      if (tracker.listenerCalled && !tracker.listenerSetPlainTextFailed) {
        if (this.DEBUG) (console.info || console.log).call(console, "Copied using selection of temporary element added to DOM.");
        resolve();
        return;
      }

      // Fallback for iOS Safari.
      var text = data.getData(DataTypes.TEXT_PLAIN);
      if (text !== undefined) {
        if (this.DEBUG) (console.info || console.log).call(console, "Copied text using DOM.");
        resolve();
        return;
      }

      reject(new Error("Copy command failed."));
    });
  }

  static writeText(s: string): Promise<void> {
    var dt = new DT();
    dt.setData(DataTypes.TEXT_PLAIN, s);
    return this.write(dt);
  }

  static read(): Promise<DT> {
    return new Promise((resolve, reject) => reject("Cannot read in any modern browsers. IE11 pasting is not implemented yet."));
  }

  static readText(): Promise<string> {
    return new Promise((resolve, reject) => reject("Cannot read in any modern browsers. IE11 pasting is not implemented yet."));
  }

  // Legacy v1 API.
  static copy(obj: string|{[key:string]:string}|HTMLElement): Promise<void> {
    (console.warn || console.log).call(console, "[clipboard.js] The clipboard.copy() API is deprecated and may be removed in a future version. Please switch to clipboard.write() or clipboard.writeText().");

    return new Promise((resolve, reject) => {
      var data: DT;
      if (typeof obj === "string") {
        data = DT.fromText(obj);
      } else if (obj instanceof HTMLElement) {
        data = DT.fromElement(obj);
      } else if (obj instanceof Object) {
        data = DT.fromObject(obj);
      } else {
        reject("Invalid data type. Must be string, DOM node, or an object mapping MIME types to strings.");
        return;
      }
      this.write(data);
    });
  }

  // Legacy v1 API.
  static paste(): Promise<string> {
    (console.warn || console.log).call(console, "[clipboard.js] The clipboard.paste() API is deprecated and may be removed in a future version. Please switch to clipboard.read() or clipboard.readText().");
    return new Promise((resolve, reject) => reject("Cannot read in any modern browsers. IE11 pasting is not implemented yet."));
  }
}

class Selection {
  static select(elem: Element): void {
    var sel = document.getSelection();
    var range = document.createRange();
    range.selectNodeContents(elem);
    sel.removeAllRanges();
    sel.addRange(range);
  }

  static clear(): void {
    var sel = document.getSelection();
    sel.removeAllRanges();
  }
}

class FallbackTracker {
  public execCommandReturnedTrue: boolean = false;
  public listenerCalled: boolean = false;
  public listenerSetPlainTextFailed: boolean = false;
}

// TODO: Figure out how to expose ClipboardPolyfill as self.clipboard using
// WebPack?
declare var module: any;
module.exports = ClipboardPolyfill;
