/* Dictionary types are object types that include an indexer, which specifies a
 * key type and a value type. The presence of an indexer makes the object type
 * unsealed, but all added properties must be consistent with the indexer
 * signature.
 *
 * Dictionaries can be used to represent the common idiom of objects used as
 * maps. They can also be used to represent array-like objects, e.g., NodeList
 * from the DOM API.
 *
 * A dictionary is assumed to have every property described by it's key type.
 * This behavior is similar to the behavior of arrays, which are assumed to have
 * a value at every index.
 *
 * @flow
 */

// Some logic is variance-sensitive.
class A {}
class B extends A {}
class C extends B {}

// Just a couple of short type names. Compare to string/number.
class X {}
class Y {}

// Any property can be set on a dict with string keys.
function set_prop_to_string_key(o: {[k: string]: any}) {
  o.prop = 'ok';
}

// **UNSOUND**
// This is allowed by design. We don't track get/set and we don't wrap the
// return type in a maybe.
function unsound_dict_has_every_key(o: {[k: string]: X}) {
  o.p as X; // ok
  o['p'] as X; // ok
}

// As with any object type, we can assign subtypes to properties.
function set_prop_covariant(o: {[k: string]: B}) {
  o.p = new A(); // error, A ~> B
  o.p = new B(); // ok
  o.p = new C(); // ok
}

// This isn't specific behavior to dictionaries, but for completeness...
function get_prop_contravariant(o: {[k: string]: B}) {
  o.p as A; // ok
  o.p as B; // ok
  o.p as C; // error, C ~> B
}

// Dot-notation can not be used to add properties to dictionaries with
// non-string keys, because keys are strings.
function add_prop_to_nonstring_key_dot(o: {[k: number]: any}) {
  o.prop = 'err'; // error: string ~> number
}

// Bracket notation can be used to add properties to dictionaries with
// non-string keys, even though all keys are strings. This is a convenient
// affordance.
function add_prop_to_nonstring_key_bracket(o: {[k: number]: any}) {
  o[0] = 'ok';
}

// Objects can be part dict, part not by mixing an indexer with declared props.
function mix_with_declared_props(o: {[k: number]: X, p: Y}, x: X, y: Y) {
  o[0] as X; // ok
  o.p as Y; // ok
  o[0] = x; // ok
  o.p = y; // ok
}

// Indeed, dict types are still Objects and have Object.prototype stuff
function object_prototype(o: {[k: string]: number}): {
  [k: string]: number,
  +toString: () => string,
} {
  o.toString() as boolean; // error: string ~> boolean
  return o; // ok
}

function unsound_string_conversion_alias_declared_prop(o: {
  [k: number]: any,
  '0': X,
}) {
  o[0] = 'not-x'; // error
}

function unification_dict_values_invariant(x: Array<{[k: string]: B}>) {
  let a: Array<{[k: string]: A}> = x; // error
  a[0].p = new A(); // in[0].p no longer B

  let b: Array<{[k: string]: B}> = x; // ok

  let c: Array<{[k: string]: C}> = x; // error
  x[0].p as C; // not true
}

function subtype_dict_values_invariant(x: {[k: string]: B}) {
  let a: {[k: string]: A} = x; // error
  a.p = new A(); // x[0].p no longer B

  let b: {[k: string]: B} = x; // ok

  let c: {[k: string]: C} = x; // error
  x.p as C; // not true
}

function subtype_dict_values_fresh_exception() {
  let a: {[k: string]: A} = {
    a: new A(), // ok, A == A
    b: new B(), // ok, B <: A
    c: new C(), // ok, C <: A
  };

  let b: {[k: string]: B} = {
    a: new A(), // error, A not <: B
    b: new B(), // ok, B == B
    c: new C(), // ok, C <: A
  };

  let c: {[k: string]: C} = {
    a: new A(), // error, A not <: C
    b: new B(), // error, A not <: C
    c: new C(), // ok, C == C
  };
}

// Actually, unsound_string_conversion_alias_declared_prop behavior makes an
// argument that we shouldn't really care about this, since we ignore the fact
// that coercing values to string keys can cause unintended aliasing in general.
// Barring some compelling use case for that in this context, though, we choose
// to be strict.
function unification_dict_keys_invariant(x: Array<{[k: B]: any}>) {
  let a: Array<{[k: A]: any}> = x; // error
  let b: Array<{[k: B]: any}> = x; // ok
  let c: Array<{[k: C]: any}> = x; // error
}

function subtype_dict_keys_invariant(x: {[k: B]: any}) {
  let a: {[k: A]: any} = x; // error
  let b: {[k: B]: any} = x; // ok
  let c: {[k: C]: any} = x; // error
}

function unification_mix_with_declared_props_invariant_l(
  x: Array<{[k: string]: B}>,
) {
  let a: Array<{[k: string]: B, p: A}> = x; // error: A ~> B
  a[0].p = new A(); // x[0].p no longer B

  let b: Array<{[k: string]: B, p: B}> = x; // ok

  let c: Array<{[k: string]: B, p: C}> = x; // error
  x[0].p as C; // not true
}

function unification_mix_with_declared_props_invariant_r(
  xa: Array<{[k: string]: A, p: B}>,
  xb: Array<{[k: string]: B, p: B}>,
  xc: Array<{[k: string]: C, p: B}>,
) {
  let a: Array<{[k: string]: A}> = xa; // error
  a[0].p = new A(); // xa[0].p no longer B

  let b: Array<{[k: string]: B}> = xb; // ok

  let c: Array<{[k: string]: C}> = xc; // error
  xc[0].p as C; // not true
}

function subtype_mix_with_declared_props_invariant_l(x: {[k: string]: B}) {
  let a: {[k: string]: B, p: A} = x; // error: A ~> B
  a.p = new A(); // x.p no longer B

  let b: {[k: string]: B, p: B} = x; // ok

  let c: {[k: string]: B, p: C} = x; // error
  x.p as C; // not true
}

function subtype_mix_with_declared_props_invariant_r(
  xa: {[k: string]: A, p: B},
  xb: {[k: string]: B, p: B},
  xc: {[k: string]: C, p: B},
) {
  let a: {[k: string]: A} = xa; // error
  a.p = new A(); // xa.p no longer B

  let b: {[k: string]: B} = xb; // ok

  let c: {[k: string]: C} = xc; // error
  xc.p as C; // not true
}

function unification_dict_to_obj(x: Array<{[k: string]: X}>): Array<{p: X}> {
  return x; // error: if allowed, could write {p:X,q:Y} into `x`
}

function unification_obj_to_dict(x: Array<{p: X}>): Array<{[k: string]: X}> {
  return x; // error: if allowed, could write {p:X,q:Y} into returned array
}

function subtype_dict_to_obj(x: {[k: string]: B}) {
  let a: {p: A} = x; // error
  a.p = new A(); // x.p no longer B

  let b: {p: B} = x; // ok

  let c: {p: C} = x; // error
  x.p as C; // not true
}

function subtype_obj_to_dict(x: {|p: B|}) {
  let a: {[k: string]: A} = x; // error
  a.p = new A(); // x.p no longer B

  let b: {[k: string]: B} = x;

  let c: {[k: string]: C} = x; // error
  x.p as C; // not true
}

// Only props in l which are not in u must match indexer, but must do so
// exactly.
function subtype_obj_to_mixed(x: {|p: B, x: X|}) {
  let a: {[k: string]: A, x: X} = x; // error (as above), but exclusive of x
  let b: {[k: string]: B, x: X} = x; // ok,
  let c: {[k: string]: C, x: X} = x; // error (as above), but exclusive of x
}

function unification_dict_to_mixed(x: Array<{[k: string]: B}>) {
  let a: Array<{[k: string]: B, p: A}> = x; // error
  let b: Array<{[k: string]: B, p: B}> = x; // ok
  let c: Array<{[k: string]: B, p: C}> = x; // error
}

function subtype_dict_to_mixed(x: {[k: string]: B}) {
  let a: {[k: string]: B, p: A} = x; // error
  let b: {[k: string]: B, p: B} = x; // ok
  let c: {[k: string]: B, p: C} = x; // error
}

function subtype_dict_to_optional_a(x: {[k: string]: B}) {
  let a: {p?: A} = x; // error
}

function subtype_dict_to_optional_b(x: {[k: string]: B}) {
  let b: {p?: B} = x; // ok
}

function subtype_dict_to_optional_c(x: {[k: string]: B}) {
  let c: {p?: C} = x; // error
}

function subtype_optional_a_to_dict(x: {|p?: A|}): {[k: string]: B} {
  // error: A ~> B
  return x;
}

function subtype_optional_b_to_dict(x: {|p?: B|}): {[k: string]: B} {
  // ok
  return x;
}

function subtype_optional_c_to_dict(x: {|p?: C|}): {[k: string]: B} {
  // error: C ~> B
  return x;
}
