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

/// An enum of all Dart runtimes supported by the test runner.
final class Runtime {
  // When adding new runtimes, be sure to update the baseline and derived
  // variable tests in test/backend/platform_selector/evaluate_test.

  /// The command-line Dart VM.
  static const Runtime vm = Runtime('VM', 'vm', Compiler.kernel, [
    Compiler.kernel,
    Compiler.source,
    Compiler.exe,
  ], isDartVM: true);

  /// Google Chrome.
  static const Runtime chrome = Runtime(
    'Chrome',
    'chrome',
    Compiler.dart2js,
    [Compiler.dart2js, Compiler.dart2wasm],
    isBrowser: true,
    isBlink: true,
  );

  /// Mozilla Firefox.
  static const Runtime firefox = Runtime(
    'Firefox',
    'firefox',
    Compiler.dart2js,
    [Compiler.dart2js, Compiler.dart2wasm],
    isBrowser: true,
  );

  /// Apple Safari.
  static const Runtime safari = Runtime('Safari', 'safari', Compiler.dart2js, [
    Compiler.dart2js,
  ], isBrowser: true);

  /// Microsoft Internet Explorer.
  @Deprecated('Internet Explorer is no longer supported')
  static const Runtime internetExplorer = Runtime(
    'Internet Explorer',
    'ie',
    Compiler.dart2js,
    [Compiler.dart2js],
    isBrowser: true,
  );

  /// Microsoft Edge (based on Chromium).
  static const Runtime edge = Runtime(
    'Microsoft Edge',
    'edge',
    Compiler.dart2js,
    [Compiler.dart2js],
    isBrowser: true,
    isBlink: true,
  );

  /// The command-line Node.js VM.
  static const Runtime nodeJS = Runtime('Node.js', 'node', Compiler.dart2js, [
    Compiler.dart2js,
    Compiler.dart2wasm,
  ]);

  /// The platforms that are supported by the test runner by default.
  static const List<Runtime> builtIn = [
    Runtime.vm,
    Runtime.chrome,
    Runtime.firefox,
    Runtime.safari,
    Runtime.edge,
    Runtime.nodeJS,
  ];

  /// The human-friendly name of the platform.
  final String name;

  /// The identifier used to look up the platform.
  final String identifier;

  /// The parent platform that this is based on, or `null` if there is no
  /// parent.
  final Runtime? parent;

  /// Returns whether this is a child of another platform.
  bool get isChild => parent != null;

  /// Whether this platform runs the Dart VM in any capacity.
  final bool isDartVM;

  /// Whether this platform is a browser.
  final bool isBrowser;

  /// Whether this platform uses the Blink rendering engine.
  final bool isBlink;

  /// Whether this platform has no visible window.
  final bool isHeadless;

  /// Returns the platform this is based on, or [this] if it's not based on
  /// anything.
  ///
  /// That is, returns [parent] if it's non-`null` or [this] if it's `null`.
  Runtime get root => parent ?? this;

  /// The default compiler to use with this runtime.
  final Compiler defaultCompiler;

  /// All the supported compilers for this runtime.
  final List<Compiler> supportedCompilers;

  const Runtime(
    this.name,
    this.identifier,
    this.defaultCompiler,
    this.supportedCompilers, {
    this.isDartVM = false,
    this.isBrowser = false,
    this.isBlink = false,
    this.isHeadless = false,
  }) : parent = null;

  Runtime._child(
    this.name,
    this.identifier,
    this.defaultCompiler,
    this.supportedCompilers,
    Runtime this.parent,
  ) : isDartVM = parent.isDartVM,
      isBrowser = parent.isBrowser,
      isBlink = parent.isBlink,
      isHeadless = parent.isHeadless;

  /// Converts a JSON-safe representation generated by [serialize] back into a
  /// [Runtime].
  factory Runtime.deserialize(Object serialized) {
    if (serialized is String) {
      return builtIn.firstWhere(
        (platform) => platform.identifier == serialized,
      );
    }

    var map = serialized as Map;
    var name = map['name'] as String;
    var identifier = map['identifier'] as String;
    var defaultCompiler = Compiler.deserialize(
      map['defaultCompiler'] as Object,
    );
    var supportedCompilers = [
      for (var compiler in map['supportedCompilers'] as List)
        Compiler.deserialize(compiler as Object),
    ];

    var parent = map['parent'];
    if (parent != null) {
      // Note that the returned platform's [parent] won't necessarily be `==` to
      // a separately-deserialized parent platform. This should be fine, though,
      // since we only deserialize platforms in the remote execution context
      // where they're only used to evaluate platform selectors.
      return Runtime._child(
        name,
        identifier,
        defaultCompiler,
        supportedCompilers,
        Runtime.deserialize(parent as Object),
      );
    }

    return Runtime(
      name,
      identifier,
      defaultCompiler,
      supportedCompilers,
      isDartVM: map['isDartVM'] as bool,
      isBrowser: map['isBrowser'] as bool,
      isBlink: map['isBlink'] as bool,
      isHeadless: map['isHeadless'] as bool,
    );
  }

  /// Converts [this] into a JSON-safe object that can be converted back to a
  /// [Runtime] using [Runtime.deserialize].
  Object serialize() {
    if (builtIn.contains(this)) return identifier;

    if (parent != null) {
      return {
        'name': name,
        'defaultCompiler': defaultCompiler.serialize(),
        'supportedCompilers': [
          for (var compiler in supportedCompilers) compiler.serialize(),
        ],
        'identifier': identifier,
        'parent': parent!.serialize(),
      };
    }

    return {
      'name': name,
      'defaultCompiler': defaultCompiler.serialize(),
      'supportedCompilers': [
        for (var compiler in supportedCompilers) compiler.serialize(),
      ],
      'identifier': identifier,
      'isDartVM': isDartVM,
      'isBrowser': isBrowser,
      'isBlink': isBlink,
      'isHeadless': isHeadless,
      // TODO(https://github.com/dart-lang/test/issues/2146): Remove this.
      'isJS': isBrowser || this == Runtime.nodeJS,
      // TODO(https://github.com/dart-lang/test/issues/2146): Remove this.
      'isWasm': false,
    };
  }

  /// Returns a child of [this] that counts as both this platform's identifier
  /// and the new [identifier].
  ///
  /// This may not be called on a platform that's already a child.
  Runtime extend(String name, String identifier) {
    if (parent == null) {
      return Runtime._child(
        name,
        identifier,
        defaultCompiler,
        supportedCompilers,
        this,
      );
    }
    throw StateError('A child platform may not be extended.');
  }

  @override
  String toString() => name;
}
