// 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 'codecs.dart';
import 'common.dart';
import 'recording.dart';
import 'result_reference.dart';

/// Base class for recordable file system invocation events.
///
/// Instances of this class will be aggregated in a [Recording]
abstract class InvocationEvent<T> {
  /// The object on which the invocation occurred. Will always be non-null.
  Object get object;

  /// The return value of the invocation if the invocation completed
  /// successfully.
  ///
  /// This may be null (and will always be `null` for setters).
  ///
  /// If the invocation completed with an error, this value will be `null`,
  /// and [error] will be set.
  T get result;

  /// The error that was thrown by the invocation if the invocation completed
  /// with an error.
  ///
  /// If the invocation completed successfully, this value will be `null`, and
  /// [result] will hold the result of the invocation (which may also be
  /// `null`).
  ///
  /// This field being non-null can be used as an indication that the invocation
  /// completed with an error.
  dynamic get error;

  /// The stopwatch value (in milliseconds) when the invocation occurred.
  ///
  /// This value is recorded when the invocation first occurs, not when the
  /// delegate returns.
  int get timestamp;
}

/// A recordable invocation of a property getter on a file system object.
abstract class PropertyGetEvent<T> extends InvocationEvent<T> {
  /// The property that was retrieved.
  Symbol get property;
}

/// A recordable invocation of a property setter on a file system object.
abstract class PropertySetEvent<T> extends InvocationEvent<Null> {
  /// The property that was set.
  ///
  /// All setter property symbols will have a trailing equals sign. For example,
  /// if the `foo` property was set, this value will be a symbol of `foo=`.
  Symbol get property;

  /// The value to which [property] was set. This is distinct from [result],
  /// which is always `null` for setters.
  T get value;
}

/// A recordable invocation of a method on a file system object.
abstract class MethodEvent<T> extends InvocationEvent<T> {
  /// The method that was invoked.
  Symbol get method;

  /// The positional arguments that were passed to the method.
  List<dynamic> get positionalArguments;

  /// The named arguments that were passed to the method.
  Map<Symbol, dynamic> get namedArguments;
}

/// An [InvocationEvent] that's in the process of being recorded.
abstract class LiveInvocationEvent<T> implements InvocationEvent<T> {
  /// Creates a new [LiveInvocationEvent].
  LiveInvocationEvent(this.object, this._result, this.error, this.timestamp);

  final dynamic _result;

  @override
  final Object object;

  @override
  T get result {
    dynamic result = _result;
    while (result is ResultReference) {
      ResultReference<dynamic> reference = result;
      result = reference.recordedValue;
    }
    return result;
  }

  @override
  final dynamic error;

  @override
  final int timestamp;

  /// A [Future] that completes once [result] is ready for serialization.
  ///
  /// If [result] is a [Future], this future completes when [result] completes.
  /// If [result] is a [Stream], this future completes when the stream sends a
  /// "done" event. If [result] is neither a future nor a stream, this future
  /// completes immediately.
  ///
  /// It is legal for [serialize] to be called before this future completes,
  /// but doing so will cause incomplete results to be serialized. Results that
  /// are unfinished futures will be serialized as `null`, and results that are
  /// unfinished streams will be serialized as the data that has been received
  /// thus far.
  Future<Null> get done async {
    dynamic result = _result;
    while (result is ResultReference) {
      ResultReference<dynamic> reference = result;
      await reference.complete;
      result = reference.recordedValue;
    }
  }

  /// Returns this event as a JSON-serializable object.
  Map<String, dynamic> serialize() {
    return <String, dynamic>{
      kManifestObjectKey: encode(object),
      kManifestResultKey: encode(_result),
      kManifestErrorKey: encode(error),
      kManifestTimestampKey: timestamp,
    };
  }

  @override
  String toString() => serialize().toString();
}

/// A [PropertyGetEvent] that's in the process of being recorded.
class LivePropertyGetEvent<T> extends LiveInvocationEvent<T>
    implements PropertyGetEvent<T> {
  /// Creates a new [LivePropertyGetEvent].
  LivePropertyGetEvent(
      Object object, this.property, T result, dynamic error, int timestamp)
      : super(object, result, error, timestamp);

  @override
  final Symbol property;

  @override
  Map<String, dynamic> serialize() {
    return <String, dynamic>{
      kManifestTypeKey: kGetType,
      kManifestPropertyKey: getSymbolName(property),
    }..addAll(super.serialize());
  }
}

/// A [PropertySetEvent] that's in the process of being recorded.
class LivePropertySetEvent<T> extends LiveInvocationEvent<Null>
    implements PropertySetEvent<T> {
  /// Creates a new [LivePropertySetEvent].
  LivePropertySetEvent(
      Object object, this.property, this.value, dynamic error, int timestamp)
      : super(object, null, error, timestamp);

  @override
  final Symbol property;

  @override
  final T value;

  @override
  Map<String, dynamic> serialize() {
    return <String, dynamic>{
      kManifestTypeKey: kSetType,
      kManifestPropertyKey: getSymbolName(property),
      kManifestValueKey: encode(value),
    }..addAll(super.serialize());
  }
}

/// A [MethodEvent] that's in the process of being recorded.
class LiveMethodEvent<T> extends LiveInvocationEvent<T>
    implements MethodEvent<T> {
  /// Creates a new [LiveMethodEvent].
  LiveMethodEvent(
    Object object,
    this.method,
    List<dynamic> positionalArguments,
    Map<Symbol, dynamic> namedArguments,
    T result,
    dynamic error,
    int timestamp,
  )   : this.positionalArguments =
            new List<dynamic>.unmodifiable(positionalArguments),
        this.namedArguments =
            new Map<Symbol, dynamic>.unmodifiable(namedArguments),
        super(object, result, error, timestamp);

  @override
  final Symbol method;

  @override
  final List<dynamic> positionalArguments;

  @override
  final Map<Symbol, dynamic> namedArguments;

  @override
  Map<String, dynamic> serialize() {
    return <String, dynamic>{
      kManifestTypeKey: kInvokeType,
      kManifestMethodKey: getSymbolName(method),
      kManifestPositionalArgumentsKey: encode(positionalArguments),
      kManifestNamedArgumentsKey: encode(namedArguments),
    }..addAll(super.serialize());
  }
}
