// Copyright (c) 2022, 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 'type_analyzer.dart';

/// Data structure for tracking declared pattern variables.
///
/// To analyze a single `case pattern when guard`:
/// 1. Invoke [casePatternStart].
/// 2. Invoke zero or more [add].
/// 3. Invoke [casePatternFinish], get the set of variables `VS`.
/// 4. Use `VS` to analyze the guard.
///
/// To analyze a group of `case` members of a `switch` statement, sharing
/// the same body, and so having the shared set of pattern variables:
/// 1. Invoke [switchStatementSharedCaseScopeStart].
/// 2. Analyze individual `case pattern when guard` clauses.
/// 3. Invoke [switchStatementSharedCaseScopeEmpty] if there are labels,
///    or a `default` member.
/// 4. Invoke [switchStatementSharedCaseScopeFinish] to get the set of
///    variables `VS`, and use it to analyze the shared body.
abstract class VariableBinder<Node extends Object, Variable extends Object> {
  /// The interface for reporting error conditions up to the client.
  final VariableBinderErrors<Node, Variable>? errors;

  /// The stack of variable sets, starting with an empty one on
  /// [casePatternStart], or [logicalOrPatternStart].
  List<Map<String, Variable>> _variables = [];

  /// The stack of variable sets for potentially nested (e.g. `switch` in
  /// a closure in a `when` clause) groups of `case` members of a `switch`
  /// statement.
  List<_SharedCaseScope<Variable>> _sharedCaseScopes = [];

  VariableBinder({required this.errors});

  /// Updates the set of bindings to account for the presence of a variable
  /// pattern.  [name] is the name of the variable, [variable] is the object
  /// that represents it in the client.
  bool add(String name, Variable variable) {
    Variable? existing = _variables.last[name];
    if (existing == null) {
      _variables.last[name] = variable;
      return true;
    } else {
      errors?.duplicateVariablePattern(
        name: name,
        original: existing,
        duplicate: variable,
      );
      return false;
    }
  }

  /// Should be invoked after visiting a `case pattern` structure.  Returns
  /// all the accumulated variables (individual and joined).
  ///
  /// If [sharedCaseScopeKey] is provided, it expected to be the same as
  /// the key of the last shared case scope, and the resulting set will be
  /// joined with the current shared case scope.
  Map<String, Variable> casePatternFinish({Object? sharedCaseScopeKey}) {
    Map<String, Variable> variables = _variables.removeLast();

    if (sharedCaseScopeKey != null) {
      _SharedCaseScope<Variable> sharedScope = _sharedCaseScopes.last;
      assert(sharedScope.key == sharedCaseScopeKey);
      sharedScope.addAll(variables);
    }

    return variables;
  }

  /// Notifies that are new `case pattern` structure is about to be visited.
  void casePatternStart() {
    _variables.add({});
  }

  /// Notifies that this instance is about to be discarded.
  void finish() {
    assert(_variables.isEmpty);
    assert(_sharedCaseScopes.isEmpty);
  }

  /// Returns a new variable that is a join of [components].
  Variable joinPatternVariables({
    required Object key,
    required List<Variable> components,
    required JoinedPatternVariableInconsistency inconsistency,
  });

  /// Updates the binder after visiting a logical-or pattern, joins variables
  /// from them.  If some variables are in one side of the pattern, but not
  /// in another, they are still joined, but marked as not consistent.
  void logicalOrPatternFinish(Node node) {
    Map<String, Variable> right = _variables.removeLast();
    Map<String, Variable> left = _variables.removeLast();
    for (MapEntry<String, Variable> leftEntry in left.entries) {
      String name = leftEntry.key;
      Variable leftVariable = leftEntry.value;
      Variable? rightVariable = right.remove(name);
      if (rightVariable != null) {
        add(
          name,
          joinPatternVariables(
            key: node,
            components: [leftVariable, rightVariable],
            inconsistency: JoinedPatternVariableInconsistency.none,
          ),
        );
      } else {
        errors?.logicalOrPatternBranchMissingVariable(
          node: node,
          hasInLeft: true,
          name: name,
          variable: leftVariable,
        );
        add(
          name,
          joinPatternVariables(
            key: node,
            components: [leftVariable],
            inconsistency: JoinedPatternVariableInconsistency.logicalOr,
          ),
        );
      }
    }
    for (MapEntry<String, Variable> rightEntry in right.entries) {
      String name = rightEntry.key;
      Variable rightVariable = rightEntry.value;
      errors?.logicalOrPatternBranchMissingVariable(
        node: node,
        hasInLeft: false,
        name: name,
        variable: rightVariable,
      );
      add(
        name,
        joinPatternVariables(
          key: node,
          components: [rightVariable],
          inconsistency: JoinedPatternVariableInconsistency.logicalOr,
        ),
      );
    }
  }

  /// Notifies that the LHS of a logical-or pattern was visited, and the RHS
  /// is about to be visited.
  void logicalOrPatternFinishLeft() {
    _variables.add({});
  }

  /// Notifies that we are about to start visiting a logical-or pattern.
  void logicalOrPatternStart() {
    _variables.add({});
  }

  /// Notifies that the `default` case head, or a label, was found, so that
  /// all the variables of the current shared case scope are not consistent.
  void switchStatementSharedCaseScopeEmpty(Object key) {
    _SharedCaseScope<Variable> sharedScope = _sharedCaseScopes.last;
    assert(sharedScope.key == key);
    sharedScope.hasLabel = true;
    sharedScope.addAll({});
  }

  /// Notifies that computing of the shared case scope was finished, returns
  /// the joined set of variables.  The variables have not been checked to
  /// have the same types (because we have not done inference, so we don't
  /// know types for many of them), so some of them might become not
  /// consistent later.
  Map<String, Variable> switchStatementSharedCaseScopeFinish(Object key) {
    assert(_variables.isEmpty);
    _SharedCaseScope<Variable> sharedScope = _sharedCaseScopes.removeLast();
    assert(sharedScope.key == key);

    Map<String, Variable> result = {};
    for (MapEntry<String, _SharedCaseScopeVariable<Variable>> entry
        in sharedScope.variables.entries) {
      _SharedCaseScopeVariable<Variable> sharedVariable = entry.value;
      List<Variable> variables = sharedVariable.variables;
      if (sharedVariable.allCases && variables.length == 1) {
        result[entry.key] = variables[0];
      } else {
        result[entry.key] = joinPatternVariables(
          key: key,
          components: variables,
          inconsistency: sharedVariable.allCases
              ? JoinedPatternVariableInconsistency.none
              : sharedScope.hasLabel
              ? JoinedPatternVariableInconsistency.sharedCaseHasLabel
              : JoinedPatternVariableInconsistency.sharedCaseAbsent,
        );
      }
    }
    return result;
  }

  /// Notifies that computing new shared case scope should be started.
  void switchStatementSharedCaseScopeStart(Object key) {
    assert(_variables.isEmpty);
    _sharedCaseScopes.add(new _SharedCaseScope(key));
  }
}

/// Interface used by the [VariableBinder] logic to report error conditions
/// up to the client during the "pre-visit" phase of type analysis.
abstract class VariableBinderErrors<
  Node extends Object,
  Variable extends Object
>
    extends TypeAnalyzerErrorsBase {
  /// Called when a pattern attempts to declare the variable [duplicate] that
  /// has the same [name] as the [original] variable.
  void duplicateVariablePattern({
    required String name,
    required Variable original,
    required Variable duplicate,
  });

  /// Called when one of the branches has the [variable] with the [name], but
  /// the other branch does not.
  void logicalOrPatternBranchMissingVariable({
    required Node node,
    required bool hasInLeft,
    required String name,
    required Variable variable,
  });
}

class _SharedCaseScope<Variable extends Object> {
  final Object key;
  bool isEmpty = true;
  bool hasLabel = false;
  Map<String, _SharedCaseScopeVariable<Variable>> variables = {};

  _SharedCaseScope(this.key);

  /// Adds [newVariables] to [variables], marking absent variables as not
  /// consistent. If [isEmpty], just sets given variables as the starting set.
  void addAll(Map<String, Variable> newVariables) {
    if (isEmpty) {
      isEmpty = false;
      for (MapEntry<String, Variable> entry in newVariables.entries) {
        String name = entry.key;
        Variable variable = entry.value;
        _getVariable(name).variables.add(variable);
      }
    } else {
      for (MapEntry<String, _SharedCaseScopeVariable<Variable>> entry
          in variables.entries) {
        String name = entry.key;
        _SharedCaseScopeVariable<Variable> variable = entry.value;
        Variable? newVariable = newVariables[name];
        if (newVariable != null) {
          variable.variables.add(newVariable);
        } else {
          variable.allCases = false;
        }
      }
      for (MapEntry<String, Variable> newEntry in newVariables.entries) {
        String name = newEntry.key;
        Variable newVariable = newEntry.value;
        if (!variables.containsKey(name)) {
          _getVariable(name)
            ..allCases = false
            ..variables.add(newVariable);
        }
      }
    }
  }

  _SharedCaseScopeVariable _getVariable(String name) {
    return variables[name] ??= new _SharedCaseScopeVariable();
  }
}

class _SharedCaseScopeVariable<Variable extends Object> {
  bool allCases = true;
  final List<Variable> variables = [];
}
