// Copyright 2019 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'dart:collection';

import '../utils.dart';

/// An unmodifiable view of multiple maps merged together as though they were a
/// single map.
///
/// The values in later maps take precedence over those in earlier maps. When a
/// key is set, it's set in the last map that has an existing value for that
/// key.
///
/// Unlike `CombinedMapView` from the `collection` package, this provides `O(1)`
/// index and `length` operations and provides some degree of mutability. It
/// does so by imposing the additional constraint that the underlying maps' sets
/// of keys remain unchanged.
class MergedMapView<K, V> extends MapBase<K, V> {
  // A map from keys to the maps in which those keys first appear.
  final _mapsByKey = <K, Map<K, V>>{};

  Iterable<K> get keys => _mapsByKey.keys;
  int get length => _mapsByKey.length;
  bool get isEmpty => _mapsByKey.isEmpty;
  bool get isNotEmpty => _mapsByKey.isNotEmpty;

  /// Creates a combined view of [maps].
  ///
  /// Each map must have the default notion of equality. The underlying maps'
  /// values may change independently of this view, but their set of keys may
  /// not.
  MergedMapView(Iterable<Map<K, V>> maps) {
    for (var map in maps) {
      if (map is MergedMapView<K, V>) {
        // Flatten nested merged views to avoid O(depth) overhead.
        for (var child in map._mapsByKey.values) {
          setAll(_mapsByKey, child.keys, child);
        }
      } else {
        setAll(_mapsByKey, map.keys, map);
      }
    }
  }

  V? operator [](Object? key) => _mapsByKey[key as K]?[key];

  operator []=(K key, V value) {
    if (_mapsByKey[key] case var child?) {
      child[key] = value;
    } else {
      throw UnsupportedError("New entries may not be added to MergedMapView.");
    }
  }

  V? remove(Object? key) {
    throw UnsupportedError("Entries may not be removed from MergedMapView.");
  }

  void clear() {
    throw UnsupportedError("Entries may not be removed from MergedMapView.");
  }

  bool containsKey(Object? key) => _mapsByKey.containsKey(key);
}
