// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:collection';

import 'package:collection/collection.dart';

import 'yaml_node.dart';

/// Returns a [Map] that compares its keys based on [deepEquals].
Map<K, V> deepEqualsMap<K, V>() =>
    new HashMap(equals: deepEquals, hashCode: deepHashCode);

/// Returns whether two objects are structurally equivalent.
///
/// This considers `NaN` values to be equivalent, handles self-referential
/// structures, and considers [YamlScalar]s to be equal to their values.
bool deepEquals(obj1, obj2) => new _DeepEquals().equals(obj1, obj2);

/// A class that provides access to the list of parent objects used for loop
/// detection.
class _DeepEquals {
  final _parents1 = [];
  final _parents2 = [];

  /// Returns whether [obj1] and [obj2] are structurally equivalent.
  bool equals(obj1, obj2) {
    if (obj1 is YamlScalar) obj1 = obj1.value;
    if (obj2 is YamlScalar) obj2 = obj2.value;

    // _parents1 and _parents2 are guaranteed to be the same size.
    for (var i = 0; i < _parents1.length; i++) {
      var loop1 = identical(obj1, _parents1[i]);
      var loop2 = identical(obj2, _parents2[i]);
      // If both structures loop in the same place, they're equal at that point
      // in the structure. If one loops and the other doesn't, they're not
      // equal.
      if (loop1 && loop2) return true;
      if (loop1 || loop2) return false;
    }

    _parents1.add(obj1);
    _parents2.add(obj2);
    try {
      if (obj1 is List && obj2 is List) {
        return _listEquals(obj1, obj2);
      } else if (obj1 is Map && obj2 is Map) {
        return _mapEquals(obj1, obj2);
      } else if (obj1 is num && obj2 is num) {
        return _numEquals(obj1, obj2);
      } else {
        return obj1 == obj2;
      }
    } finally {
      _parents1.removeLast();
      _parents2.removeLast();
    }
  }

  /// Returns whether [list1] and [list2] are structurally equal.
  bool _listEquals(List list1, List list2) {
    if (list1.length != list2.length) return false;

    for (var i = 0; i < list1.length; i++) {
      if (!equals(list1[i], list2[i])) return false;
    }

    return true;
  }

  /// Returns whether [map1] and [map2] are structurally equal.
  bool _mapEquals(Map map1, Map map2) {
    if (map1.length != map2.length) return false;

    for (var key in map1.keys) {
      if (!map2.containsKey(key)) return false;
      if (!equals(map1[key], map2[key])) return false;
    }

    return true;
  }

  /// Returns whether two numbers are equivalent.
  ///
  /// This differs from `n1 == n2` in that it considers `NaN` to be equal to
  /// itself.
  bool _numEquals(num n1, num n2) {
    if (n1.isNaN && n2.isNaN) return true;
    return n1 == n2;
  }
}

/// Returns a hash code for [obj] such that structurally equivalent objects
/// will have the same hash code.
///
/// This supports deep equality for maps and lists, including those with
/// self-referential structures, and returns the same hash code for
/// [YamlScalar]s and their values.
int deepHashCode(obj) {
  var parents = [];

  _deepHashCode(value) {
    if (parents.any((parent) => identical(parent, value))) return -1;

    parents.add(value);
    try {
      if (value is Map) {
        var equality = const UnorderedIterableEquality();
        return equality.hash(value.keys.map(_deepHashCode)) ^
            equality.hash(value.values.map(_deepHashCode));
      } else if (value is Iterable) {
        return const IterableEquality().hash(value.map(deepHashCode));
      } else if (value is YamlScalar) {
        return value.value.hashCode;
      } else {
        return value.hashCode;
      }
    } finally {
      parents.removeLast();
    }
  }

  return _deepHashCode(obj);
}
