// Copyright (c) 2015, 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 'package:source_span/source_span.dart';
import 'package:yaml/src/event.dart';
import 'package:yaml/yaml.dart';

bool _contains(YamlList l1, YamlNode n2) {
  for (YamlNode n1 in l1.nodes) {
    if (n1.value == n2.value) {
      return true;
    }
  }
  return false;
}

/// Merges two maps (of yaml) with simple override semantics, suitable for
/// merging two maps where one map defines default values that are added to
/// (and possibly overridden) by an overriding map.
class Merger {
  /// Merges a default [o1] with an overriding object [o2].
  ///
  ///   * lists are merged (without duplicates).
  ///   * lists of scalar values can be promoted to simple maps when merged with
  ///     maps of strings to booleans (e.g., ['opt1', 'opt2'] becomes
  ///     {'opt1': true, 'opt2': true}.
  ///   * maps are merged recursively.
  ///   * if map values cannot be merged, the overriding value is taken.
  ///
  YamlNode merge(YamlNode o1, YamlNode o2) {
    // Handle promotion first.
    YamlMap listToMap(YamlList list) {
      // We use the default linked hash map so the ordering is the same every time.
      Map<YamlNode, YamlNode> map = {};
      ScalarEvent event =
          ScalarEvent(o1.span as FileSpan, 'true', ScalarStyle.PLAIN);
      for (var element in list.nodes) {
        map[element] = YamlScalar.internal(true, event);
      }
      return YamlMap.internal(map, o1.span, CollectionStyle.BLOCK);
    }

    if (_isListOfString(o1) && _isMapToBools(o2)) {
      o1 = listToMap(o1 as YamlList);
    } else if (_isMapToBools(o1) && _isListOfString(o2)) {
      o2 = listToMap(o2 as YamlList);
    }

    if (o1 is YamlMap && o2 is YamlMap) {
      return mergeMap(o1, o2);
    }
    if (o1 is YamlList && o2 is YamlList) {
      return mergeList(o1, o2);
    }
    // Default to override, unless the overriding value is `null`.
    if (o2 is YamlScalar && o2.value == null) {
      return o1;
    }
    return o2;
  }

  /// Merge lists, avoiding duplicates.
  YamlList mergeList(YamlList l1, YamlList l2) {
    List<YamlNode> list = <YamlNode>[];
    list.addAll(l1.nodes);
    for (YamlNode n2 in l2.nodes) {
      if (!_contains(l1, n2)) {
        list.add(n2);
      }
    }
    return YamlList.internal(list, l1.span, CollectionStyle.BLOCK);
  }

  /// Merge maps (recursively).
  YamlMap mergeMap(YamlMap m1, YamlMap m2) {
    // We use the default linked hash map so the ordering is the same every time.
    Map<YamlNode, YamlNode> merged = {};
    m1.nodeMap.forEach((k, v) {
      merged[k] = v;
    });
    m2.nodeMap.forEach((k, v) {
      var value = k.value;
      var mergedKey =
          merged.keys.firstWhere((key) => key.value == value, orElse: () => k)
              as YamlScalar;
      var o1 = merged[mergedKey];
      if (o1 != null) {
        merged[mergedKey] = merge(o1, v);
      } else {
        merged[mergedKey] = v;
      }
    });
    return YamlMap.internal(merged, m1.span, CollectionStyle.BLOCK);
  }

  static bool _isListOfString(Object? o) =>
      o is YamlList &&
      o.nodes.every((e) => e is YamlScalar && e.value is String);

  static bool _isMapToBools(Object? o) =>
      o is YamlMap &&
      o.nodes.values.every((v) => v is YamlScalar && v.value is bool);
}

extension YamlMapExtensions on YamlMap {
  /// Return [nodes] as a Map with [YamlNode] keys.
  Map<YamlNode, YamlNode> get nodeMap => nodes.cast<YamlNode, YamlNode>();

  /// Return the [YamlNode] associated with the given [key], or `null` if there
  /// is no matching key.
  YamlNode? getKey(String key) {
    for (var k in nodes.keys.cast<YamlNode>()) {
      if (k is YamlScalar && k.value == key) {
        return k;
      }
    }
    return null;
  }

  /// Return the [YamlNode] representing the key that corresponds to the value
  /// represented by the [value] node.
  YamlNode? keyAtValue(YamlNode value) {
    for (var entry in nodes.entries) {
      if (entry.value == value) {
        return entry.key as YamlNode?;
      }
    }
    return null;
  }

  /// Return the value associated with the key whose value matches the given
  /// [key], or `null` if there is no matching key.
  YamlNode? valueAt(String key) {
    for (var keyNode in nodes.keys) {
      if (keyNode is YamlScalar && keyNode.value == key) {
        return nodes[keyNode];
      }
    }
    return null;
  }
}

extension YamlNodeExtension on YamlNode {
  Object get valueOrThrow => value as Object;
}
