part of html_common;

convertDartToNative_PrepareForStructuredClone(value) =>
    new _StructuredCloneDartium()
        .convertDartToNative_PrepareForStructuredClone(value);

convertNativeToDart_AcceptStructuredClone(object, {mustCopy: false}) =>
    new _AcceptStructuredCloneDartium()
        .convertNativeToDart_AcceptStructuredClone(object, mustCopy: mustCopy);

class _StructuredCloneDartium extends _StructuredClone {
  newJsMap() => js.JsNative.newObject();
  putIntoMap(map, key, value) => js.JsNative.setProperty(map, key, value);
  newJsList(length) => js.JsNative.newArray()..length = length;
  cloneNotRequired(e) => e is js.JSObject || e is TypedData || e is ByteBuffer;
}

/// A version of _AcceptStructuredClone, but using a different algorithm
/// so we can take advantage of an identity HashMap on Dartium without
/// the bad side-effect of modifying the JS source objects if we do the same in
/// dart2js.
///
/// This no longer inherits anything from _AcceptStructuredClone
/// and is never used polymorphically with it, so it doesn't inherit.
class _AcceptStructuredCloneDartium {
  newDartList(length) => new List(length);

  // As long as we stick to JSObject instead of intermingling legacy JsObject,
  // we can simply use identical.
  bool identicalInJs(a, b) => identical(a, b);

  void forEachJsField(jsObject, action) {
    var keys = js.JsNative.callMethod(_object, "keys", [jsObject]);
    for (var key in keys) {
      action(key, js.JsNative.getProperty(jsObject, key));
    }
  }

  // Keep track of the clones, keyed by the original object. If we're
  // not copying, these may be the same.
  var clones = new HashMap.identity();
  bool mustCopy = false;

  Object findSlot(value) {
    return clones.putIfAbsent(value, () => null);
  }

  writeSlot(original, x) {
    clones[original] = x;
  }

  walk(e) {
    if (e == null) return e;
    if (e is bool) return e;
    if (e is num) return e;
    if (e is String) return e;
    if (e is DateTime) return e;

    if (isJavaScriptRegExp(e)) {
      // TODO(sra).
      throw new UnimplementedError('structured clone of RegExp');
    }

    if (isJavaScriptPromise(e)) {
      return convertNativePromiseToDartFuture(e);
    }

    if (isJavaScriptSimpleObject(e)) {
      // TODO(sra): If mustCopy is false, swizzle the prototype for one of a Map
      // implementation that uses the properies as storage.
      var copy = findSlot(e);
      if (copy != null) return copy;
      copy = {};

      writeSlot(e, copy);
      forEachJsField(e, (key, value) => copy[key] = walk(value));
      return copy;
    }

    if (isJavaScriptArray(e)) {
      var copy = findSlot(e);
      if (copy != null) return copy;

      int length = e.length;
      // Since a JavaScript Array is an instance of Dart List, we can modify it
      // in-place unless we must copy.
      copy = mustCopy ? newDartList(length) : e;
      writeSlot(e, copy);

      for (int i = 0; i < length; i++) {
        copy[i] = walk(e[i]);
      }
      return copy;
    }

    // Assume anything else is already a valid Dart object, either by having
    // already been processed, or e.g. a clonable native class.
    return e;
  }

  convertNativeToDart_AcceptStructuredClone(object, {mustCopy: false}) {
    this.mustCopy = mustCopy;
    var copy = walk(object);
    return copy;
  }
}

final _dateConstructor = js.JsNative.getProperty(window, "Date");
final _regexConstructor = js.JsNative.getProperty(window, "RegExp");

bool isJavaScriptDate(value) =>
    value is js.JSObject && js.JsNative.instanceof(value, _dateConstructor);
bool isJavaScriptRegExp(value) =>
    value is js.JSObject && js.JsNative.instanceof(value, _regexConstructor);
bool isJavaScriptArray(value) => value is js.JSArray;

final _object = js.JsNative.getProperty(window, "Object");
final _getPrototypeOf = js.JsNative.getProperty(_object, "getPrototypeOf");
_getProto(object) {
  return _getPrototypeOf(object);
}

final _objectProto = js.JsNative.getProperty(_object, "prototype");

bool isJavaScriptSimpleObject(value) {
  if (value is! js.JSObject) return false;
  var proto = _getProto(value);
  return proto == _objectProto || proto == null;
}

// TODO(jacobr): this makes little sense unless we are doing something
// ambitious to make Dartium and Dart2Js interop well with each other.
bool isImmutableJavaScriptArray(value) =>
    isJavaScriptArray(value) &&
    js.JsNative.getProperty(value, "immutable$list") != null;

final _promiseConstructor = js.JsNative.getProperty(window, 'Promise');
bool isJavaScriptPromise(value) =>
    value is js.JSObject &&
    identical(
        js.JsNative.getProperty(value, 'constructor'), _promiseConstructor);

Future convertNativePromiseToDartFuture(js.JSObject promise) {
  var completer = new Completer();
  var newPromise = js.JsNative.callMethod(
      js.JsNative.callMethod(promise, "then",
          [js.allowInterop((result) => completer.complete(result))]),
      "catch",
      [js.allowInterop((result) => completer.completeError(result))]);
  return completer.future;
}

convertDartToNative_DateTime(DateTime date) {
  return date;
}

/// Creates a Dart Rectangle from a Javascript object with properties
/// left, top, width and height or a 4 element array of integers. Used internally in Dartium.
Rectangle make_dart_rectangle(r) {
  if (r == null) return null;
  if (r is List) {
    return new Rectangle(r[0], r[1], r[2], r[3]);
  }

  return new Rectangle(
      js.JsNative.getProperty(r, 'left'),
      js.JsNative.getProperty(r, 'top'),
      js.JsNative.getProperty(r, 'width'),
      js.JsNative.getProperty(r, 'height'));
}

// Converts a flat Dart map into a JavaScript object with properties this is
// is the Dartium only version it uses dart:js.
// TODO(alanknight): This could probably be unified with the dart2js conversions
// code in html_common and be more general.
convertDartToNative_Dictionary(Map dict) {
  if (dict == null) return null;
  var jsObject = js.JsNative.newObject();
  dict.forEach((String key, value) {
    if (value is List) {
      var jsArray = js.JsNative.newArray();
      value.forEach((elem) {
        jsArray.add(elem is Map ? convertDartToNative_Dictionary(elem) : elem);
      });
      js.JsNative.setProperty(jsObject, key, jsArray);
    } else {
      js.JsNative.setProperty(jsObject, key, value);
    }
  });
  return jsObject;
}

// Creates a Dart class to allow members of the Map to be fetched (as if getters exist).
// TODO(terry): Need to use package:js but that's a problem in dart:html. Talk to
//              Jacob about how to do this properly using dart:js.
class _ReturnedDictionary {
  Map _values;

  noSuchMethod(Invocation invocation) {
    var key = MirrorSystem.getName(invocation.memberName);
    if (invocation.isGetter) {
      return _values[key];
    } else if (invocation.isSetter && key.endsWith('=')) {
      key = key.substring(0, key.length - 1);
      _values[key] = invocation.positionalArguments[0];
    }
  }

  Map get toMap => _values;

  _ReturnedDictionary(Map value) : _values = value != null ? value : {};
}

// Helper function to wrapped a returned dictionary from blink to a Dart looking
// class.
convertNativeDictionaryToDartDictionary(values) {
  if (values is! Map) {
    // TODO(jacobr): wish wwe didn't have to do this.
    values = convertNativeToDart_SerializedScriptValue(values);
  }
  return values != null ? new _ReturnedDictionary(values) : null;
}

convertNativeToDart_Dictionary(values) =>
    convertNativeToDart_SerializedScriptValue(values);

// Conversion function place holder (currently not used in dart2js or dartium).
List convertDartToNative_StringArray(List<String> input) => input;

// Converts a Dart list into a JsArray. For the Dartium version only.
convertDartToNative_List(List input) => new js.JsArray()..addAll(input);

// Incredibly slow implementation to lookup the runtime type for an object.
// Fortunately, performance doesn't matter much as the results are cached
// as long as the object being looked up has a valid prototype.
// TODO(jacobr): we should track the # of lookups to ensure that things aren't
// going off the rails due to objects with null prototypes, etc.
// Note: unlike all other methods in this class, here we intentionally use
// the old JsObject types to bootstrap the new typed bindings.
Type lookupType(js.JsObject jsObject, bool isElement) {
  try {
    // TODO(jacobr): add static methods that return the runtime type of the patch
    // class so that this code works as expected.
    if (jsObject is js.JsArray) {
      return js.JSArray.instanceRuntimeType;
    }
    if (jsObject is js.JsFunction) {
      return js.JSFunction.instanceRuntimeType;
    }

    var constructor = js.JsNative.getProperty(jsObject, 'constructor');
    if (constructor == null) {
      // Perfectly valid case for JavaScript objects where __proto__ has
      // intentionally been set to null.
      // We should track and warn about this case as peformance will be poor.
      return js.JSObject.instanceRuntimeType;
    }
    var jsTypeName = js.JsNative.getProperty(constructor, 'name');
    if (jsTypeName is! String || jsTypeName.length == 0) {
      // Not an html type.
      return js.JSObject.instanceRuntimeType;
    }

    var dartClass_instance;
    var customElementClass = null;
    var extendsTag = "";

    Type type = getHtmlCreateType(jsTypeName);
    if (type != null) return type;

    // Start walking the prototype chain looking for a JS class.
    var prototype = js.JsNative.getProperty(jsObject, '__proto__');
    while (prototype != null) {
      // We're a Dart class that's pointing to a JS class.
      var constructor = js.JsNative.getProperty(prototype, 'constructor');
      if (constructor != null) {
        jsTypeName = js.JsNative.getProperty(constructor, 'name');
        type = getHtmlCreateType(jsTypeName);
        if (type != null) return type;
      }
      prototype = js.JsNative.getProperty(prototype, '__proto__');
    }
  } catch (e) {
    // This case can happen for cross frame objects.
    if (js.JsNative.hasProperty(e, "postMessage")) {
      // assume this is a Window. To match Dart2JS, separate conversion code
      // in dart:html will switch the wrapper to a cross frame window as
      // required.
      // TODO(jacobr): we could consider removing this code completely.
      return Window.instanceRuntimeType;
    }
  }
  return js.JSObject.instanceRuntimeType;
}
