// Copyright (c) 2019, 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:analyzer/src/dart/element/element.dart';
import 'package:meta/meta.dart';

/// Indirection between a name and the corresponding [ElementImpl].
///
/// References are organized in a prefix tree.
/// Each reference knows its parent, children, and the [ElementImpl].
///
///      Library:
///         URI of library
///
///      Class:
///         Reference of the enclosing library
///         "@class"
///         Name of the class
///
///      Method:
///         Reference of the enclosing class
///         "@method"
///         Name of the method
///
/// There is only one reference object per [ElementImpl].
class Reference {
  /// The name of the container used for duplicate declarations.
  static const _defName = '@def';

  /// The parent of this reference, or `null` if the root.
  Reference? parent;

  /// The simple name of the reference in its [parent].
  String name;

  /// The corresponding [ElementImpl], or `null` if a named container.
  ElementImpl? element;

  /// Temporary index used during serialization and linking.
  int? index;

  // null, Reference or Map<String, Reference>.
  Object? _childrenUnion;

  Reference.root() : this._(null, '');

  Reference._(this.parent, this.name);

  Iterable<Reference> get children {
    var childrenUnion = _childrenUnion;
    if (childrenUnion == null) return const [];
    if (childrenUnion is Reference) return [childrenUnion];
    return (childrenUnion as Map<String, Reference>).values;
  }

  @visibleForTesting
  Object? get childrenUnionForTesting => _childrenUnion;

  /// The name of the element that this reference represents.
  ///
  /// Normally, this is [name]. But in case of duplicate declarations, such
  /// as augmentations (which is allowed by the specification), or invalid
  /// code, the actual name is the name of the parent of the duplicates
  /// container `@def`.
  String get elementName {
    if (parent?.name == _defName) {
      return parent!.parent!.name;
    }
    return name;
  }

  bool get isLibrary => parent?.isRoot == true;

  bool get isPrefix => parent?.name == '@prefix';

  bool get isRoot => parent == null;

  bool get isSetter => parent?.name == '@setter';

  /// The parent that is not a container like `@method`.
  ///
  /// Usually this is the parent of the parent.
  /// @class::A::@method::foo -> @class::A
  ///
  /// But if this is a duplicates, we go two more levels up.
  /// @class::A::@method::foo::@def::0 -> @class::A
  Reference get parentNotContainer {
    // Should be `@method`, `@constructor`, etc.
    var containerInParent = parent!;

    // Skip the duplicates container.
    if (containerInParent.name == _defName) {
      containerInParent = containerInParent.parent!.parent!;
    }

    return containerInParent.parent!;
  }

  /// Return the child with the given name, or `null` if does not exist.
  Reference? operator [](String name) {
    var childrenUnion = _childrenUnion;
    if (childrenUnion == null) return null;
    if (childrenUnion is Reference) {
      if (childrenUnion.name == name) return childrenUnion;
      return null;
    }
    return (childrenUnion as Map<String, Reference>)[name];
  }

  /// Adds a new child with the given [name].
  ///
  /// This method should be used when a new declaration of an element with
  /// this name is processed. If there is no existing child with this name,
  /// this method works exactly as [getChild]. If there is a duplicate, which
  /// should happen rarely, an intermediate `@def` container is added, the
  /// existing child is transferred to it and renamed to `0`, then a new child
  /// is added with name `1`. Additional duplicate children get names `2`, etc.
  Reference addChild(String name) {
    var child = Reference._(null, name);
    addChildReference(name, child);
    return child;
  }

  /// Transfers [child] to this parent.
  void addChildReference(String name, Reference child) {
    child.parent = this;

    var existing = this[name];

    // If not a duplicate.
    if (existing == null) {
      _addChild(name, child);
      return;
    }

    var def = existing[_defName];

    // If no duplicates container yet.
    if (def == null) {
      removeChild(name); // existing
      def = getChild(name).getChild(_defName);
      existing.parent = def;
      existing.name = '0';
      def._addChild(existing.name, existing);
    }

    // Add a new child to the duplicates container.
    child.parent = def;
    child.name = '${def.children.length}';
    def._addChild(child.name, child);
  }

  /// Return the child with the given name, create if does not exist yet.
  Reference getChild(String name) {
    var childrenUnion = _childrenUnion;
    if (childrenUnion == null) {
      // 0 -> 1 children.
      return _childrenUnion = Reference._(this, name);
    }
    if (childrenUnion is Reference) {
      if (childrenUnion.name == name) return childrenUnion;

      // 1 -> 2 children.
      var childrenUnionAsMap = _childrenUnion = <String, Reference>{};
      childrenUnionAsMap[childrenUnion.name] = childrenUnion;
      return childrenUnionAsMap[name] = Reference._(this, name);
    }
    return (childrenUnion as Map<String, Reference>)[name] ??= Reference._(
      this,
      name,
    );
  }

  /// Returns children with the given name.
  /// Usually returns zero or one child.
  /// But in case of duplicates will return more than one.
  List<Reference> getChildrenByName(String name) {
    var result = this[name];

    // No such child yet.
    if (result == null) {
      return const [];
    }

    // Maybe has the container with duplicates.
    if (result[_defName] case var defContainer?) {
      return defContainer.children.toList();
    }

    // Should be the only child with such name.
    return [result];
  }

  Reference? removeChild(String name) {
    var childrenUnion = _childrenUnion;
    if (childrenUnion == null) return null;
    if (childrenUnion is Reference) {
      if (childrenUnion.name == name) {
        // 1 -> 0 children.
        _childrenUnion = null;
        return childrenUnion;
      }
      return null;
    }
    var childrenUnionAsMap = childrenUnion as Map<String, Reference>;
    var result = childrenUnionAsMap.remove(name);
    if (childrenUnionAsMap.length == 1) {
      // 2 -> 1 children.
      _childrenUnion = childrenUnionAsMap.values.single;
    }
    return result;
  }

  @override
  String toString() => parent == null ? 'root' : '$parent::$name';

  void _addChild(String name, Reference child) {
    var childrenUnion = _childrenUnion;
    if (childrenUnion == null) {
      // 0 -> 1 children.
      _childrenUnion = child;
      return;
    }
    if (childrenUnion is Reference) {
      // 1 -> 2 children.
      var childrenUnionAsMap = _childrenUnion = <String, Reference>{};
      childrenUnionAsMap[childrenUnion.name] = childrenUnion;
      childrenUnionAsMap[name] = child;
      return;
    }
    (childrenUnion as Map<String, Reference>)[name] ??= child;
  }
}
