// Copyright (c) 2017, 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:async';
import 'package:file/file.dart';
import 'package:file/src/backends/memory/node.dart';

import 'package:meta/meta.dart';

import 'common.dart';
import 'events.dart';
import 'mutable_recording.dart';
import 'proxy.dart';
import 'result_reference.dart';

/// Mixin that enables recording of property accesses, property mutations, and
/// method invocations.
///
/// This class uses `noSuchMethod` to record a well-defined set of invocations
/// (including property gets and sets) on an object before passing the
/// invocation on to a delegate. Subclasses wire this up by doing the following:
///
///   - Populate the list of method invocations to record in the [methods] map.
///   - Populate the list of property invocations to record in the [properties]
///     map. The symbol name for getters should be the property name, and the
///     symbol name for setters should be the property name immediately
///     followed by an equals sign (e.g. `propertyName=`).
///   - Do not implement a concrete getter, setter, or method that you wish to
///     record, as doing so will circumvent the machinery that this mixin uses
///     (`noSuchMethod`) to record invocations.
///
/// **Example use**:
///
///     abstract class Foo {
///       void sampleMethod();
///
///       int sampleProperty;
///     }
///
///     class RecordingFoo extends RecordingProxyMixin implements Foo {
///       final Foo delegate;
///
///       RecordingFoo(this.delegate) {
///         methods.addAll(<Symbol, Function>{
///           #sampleMethod: delegate.sampleMethod,
///         });
///
///         properties.addAll(<Symbol, Function>{
///           #sampleProperty: () => delegate.sampleProperty,
///           const Symbol('sampleProperty='): (int value) {
///             delegate.sampleProperty = value;
///           },
///         });
///       }
///     }
///
/// **Behavioral notes**:
///
/// Methods that return [Future]s will not be recorded until the future
/// completes.
///
/// Methods that return [Stream]s will be recorded immediately, but their
/// return values will be recorded as a [List] that will grow as the stream
/// produces data.
abstract class RecordingProxyMixin implements ProxyObject, ReplayAware {
  /// Maps method names to delegate functions.
  ///
  /// Invocations of methods listed in this map will be recorded after
  /// invoking the underlying delegate function.
  @protected
  final Map<Symbol, Function> methods = <Symbol, Function>{};

  /// Maps property getter and setter names to delegate functions.
  ///
  /// Access and mutation of properties listed in this map will be recorded
  /// after invoking the underlying delegate function.
  ///
  /// The keys for property getters are the simple property names, whereas the
  /// keys for property setters are the property names followed by an equals
  /// sign (e.g. `propertyName=`).
  @protected
  final Map<Symbol, Function> properties = <Symbol, Function>{};

  /// The object to which invocation events will be recorded.
  @protected
  MutableRecording get recording;

  /// The stopwatch used to record invocation timestamps.
  @protected
  Stopwatch get stopwatch;

  // This check is used in noSuchMethod to detect if this code is running in a
  // Dart 1 runtime, or Dart 2.
  // TODO(srawlins): Remove this after the minimum SDK constraint is such that
  // there is no "Dart 1" runtime mode. 2.0.0 or something.
  bool get _runningDart1Runtime => <dynamic>[] is List<String>;

  /// Handles invocations for which there is no concrete implementation
  /// function.
  ///
  /// For invocations that have matching entries in [methods] (for method
  /// invocations) or [properties] (for property access and mutation), this
  /// will record the invocation in [recording] after invoking the underlying
  /// delegate method. All other invocations will throw a [NoSuchMethodError].
  @override
  dynamic noSuchMethod(Invocation invocation) {
    Symbol name = invocation.memberName;
    List<dynamic> args = invocation.positionalArguments;
    Map<Symbol, dynamic> namedArgs = invocation.namedArguments;
    Function method = invocation.isAccessor ? properties[name] : methods[name];
    int time = stopwatch.elapsedMilliseconds;

    if (method == null) {
      // No delegate function generally means that there truly is no such
      // method on this object. The exception is when the invocation represents
      // a getter on a method, in which case we return a method proxy that,
      // when invoked, will perform the desired recording.
      return invocation.isGetter && methods[name] != null
          ? new MethodProxy(this, name)
          : super.noSuchMethod(invocation);
    }

    InvocationEvent<dynamic> createEvent({dynamic result, dynamic error}) {
      if (invocation.isGetter) {
        return new LivePropertyGetEvent<dynamic>(
            this, name, result, error, time);
      } else if (invocation.isSetter) {
        return new LivePropertySetEvent<dynamic>(
            this, name, args[0], error, time);
      } else {
        return new LiveMethodEvent<dynamic>(
            this, name, args, namedArgs, result, error, time);
      }
    }

    // Invoke the configured delegate method, recording an error if one occurs.
    dynamic value;
    try {
      value = Function.apply(method, args, namedArgs);
    } catch (error) {
      recording.add(createEvent(error: error));
      rethrow;
    }

    // Wrap Future and Stream results so that we record their values as they
    // become available.
    // We have to instantiate the correct type of StreamReference or
    // FutureReference, so that types are not lost when we unwrap the references
    // afterward.
    if (_runningDart1Runtime && value is Stream<dynamic>) {
      // This one is here for Dart 1 runtime mode.
      value = new StreamReference<dynamic>(value);
    } else if (value is Stream<FileSystemEntity>) {
      value = new StreamReference<FileSystemEntity>(value);
    } else if (value is Stream<String>) {
      value = new StreamReference<String>(value);
    } else if (value is Stream) {
      throw new UnimplementedError(
          'Cannot record method with return type ${value.runtimeType}');
    } else if (_runningDart1Runtime && value is Future<dynamic>) {
      // This one is here for Dart 1 runtime mode.
      value = new FutureReference<dynamic>(value);
    } else if (value is Future<bool>) {
      value = new FutureReference<bool>(value);
    } else if (value is Future<Directory>) {
      value = new FutureReference<Directory>(value);
    } else if (value is Future<File>) {
      value = new FutureReference<File>(value);
    } else if (value is Future<FileNode>) {
      value = new FutureReference<FileNode>(value);
    } else if (value is Future<FileStat>) {
      value = new FutureReference<FileStat>(value);
    } else if (value is Future<Link>) {
      value = new FutureReference<Link>(value);
    } else if (value is Future<FileSystemEntity>) {
      value = new FutureReference<FileSystemEntity>(value);
    } else if (value is Future<FileSystemEntityType>) {
      value = new FutureReference<FileSystemEntityType>(value);
    } else if (value is Future<String>) {
      value = new FutureReference<String>(value);
    } else if (value is Future) {
      throw new UnimplementedError(
          'Cannot record method with return type ${value.runtimeType}');
    }

    // Record the invocation event associated with this invocation.
    recording.add(createEvent(result: value));

    // Unwrap any result references before returning to the caller.
    dynamic result = value;
    while (result is ResultReference) {
      result = result.value;
    }
    return result;
  }
}
