// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

part of 'reporting.dart';

/// A generic usage even that does not involve custom dimensions.
///
/// If sending values for custom dimensions is required, extend this class as
/// below.
class UsageEvent {
  UsageEvent(this.category, this.parameter, {
    this.label,
    this.value,
    required this.flutterUsage,
  });

  final String category;
  final String parameter;
  final String? label;
  final int? value;
  final Usage flutterUsage;

  void send() {
    flutterUsage.sendEvent(category, parameter, label: label, value: value);
  }
}

/// A usage event related to hot reload/restart.
///
/// On a successful hot reload, we collect stats that help understand scale of
/// the update. For example, [syncedLibraryCount]/[finalLibraryCount] indicates
/// how many libraries were affected by the hot reload request. Relation of
/// [invalidatedSourcesCount] to [syncedLibraryCount] should help understand
/// sync/transfer "overhead" of updating this number of source files.
class HotEvent extends UsageEvent {
  HotEvent(String parameter, {
    required this.targetPlatform,
    required this.sdkName,
    required this.emulator,
    required this.fullRestart,
    this.reason,
    this.finalLibraryCount,
    this.syncedLibraryCount,
    this.syncedClassesCount,
    this.syncedProceduresCount,
    this.syncedBytes,
    this.invalidatedSourcesCount,
    this.transferTimeInMs,
    this.overallTimeInMs,
    this.compileTimeInMs,
    this.findInvalidatedTimeInMs,
    this.scannedSourcesCount,
    this.reassembleTimeInMs,
    this.reloadVMTimeInMs,
    // TODO(fujino): make this required
    Usage? usage,
  }) : super('hot', parameter, flutterUsage: usage ?? globals.flutterUsage);

  final String? reason;
  final String targetPlatform;
  final String sdkName;
  final bool emulator;
  final bool fullRestart;
  final int? finalLibraryCount;
  final int? syncedLibraryCount;
  final int? syncedClassesCount;
  final int? syncedProceduresCount;
  final int? syncedBytes;
  final int? invalidatedSourcesCount;
  final int? transferTimeInMs;
  final int? overallTimeInMs;
  final int? compileTimeInMs;
  final int? findInvalidatedTimeInMs;
  final int? scannedSourcesCount;
  final int? reassembleTimeInMs;
  final int? reloadVMTimeInMs;

  @override
  void send() {
    final CustomDimensions parameters = CustomDimensions(
      hotEventTargetPlatform: targetPlatform,
      hotEventSdkName: sdkName,
      hotEventEmulator: emulator,
      hotEventFullRestart: fullRestart,
      hotEventReason: reason,
      hotEventFinalLibraryCount: finalLibraryCount,
      hotEventSyncedLibraryCount: syncedLibraryCount,
      hotEventSyncedClassesCount: syncedClassesCount,
      hotEventSyncedProceduresCount: syncedProceduresCount,
      hotEventSyncedBytes: syncedBytes,
      hotEventInvalidatedSourcesCount: invalidatedSourcesCount,
      hotEventTransferTimeInMs: transferTimeInMs,
      hotEventOverallTimeInMs: overallTimeInMs,
      hotEventCompileTimeInMs: compileTimeInMs,
      hotEventFindInvalidatedTimeInMs: findInvalidatedTimeInMs,
      hotEventScannedSourcesCount: scannedSourcesCount,
      hotEventReassembleTimeInMs: reassembleTimeInMs,
      hotEventReloadVMTimeInMs: reloadVMTimeInMs,
    );
    flutterUsage.sendEvent(category, parameter, parameters: parameters);
  }
}

/// An event that reports the result of a [DoctorValidator]
class DoctorResultEvent extends UsageEvent {
  DoctorResultEvent({
    required this.validator,
    required this.result,
    Usage? flutterUsage,
  }) : super(
    'doctor-result',
    '${validator.runtimeType}',
    label: result.typeStr,
    flutterUsage: flutterUsage ?? globals.flutterUsage,
  );

  final DoctorValidator validator;
  final ValidationResult result;

  @override
  void send() {
    if (validator is! GroupedValidator) {
      flutterUsage.sendEvent(category, parameter, label: label);
      return;
    }
    final GroupedValidator group = validator as GroupedValidator;
    // The validator crashed.
    if (group.subResults.isEmpty) {
      flutterUsage.sendEvent(category, parameter, label: label);
      return;
    }
    for (int i = 0; i < group.subValidators.length; i++) {
      final DoctorValidator v = group.subValidators[i];
      final ValidationResult r = group.subResults[i];
      DoctorResultEvent(validator: v, result: r, flutterUsage: flutterUsage).send();
    }
  }
}

/// An event that reports on the result of a pub invocation.
class PubResultEvent extends UsageEvent {
  PubResultEvent({
    required String context,
    required String result,
    required Usage usage,
  }) : super('pub-result', context, label: result, flutterUsage: usage);
}

/// An event that reports something about a build.
class BuildEvent extends UsageEvent {
  BuildEvent(String label, {
    String? command,
    String? settings,
    String? eventError,
    required Usage flutterUsage,
    required String type,
  }) : _command = command,
  _settings = settings,
  _eventError = eventError,
      super(
    // category
    'build',
    // parameter
    type,
    label: label,
    flutterUsage: flutterUsage,
  );

  final String? _command;
  final String? _settings;
  final String? _eventError;

  @override
  void send() {
    final CustomDimensions parameters = CustomDimensions(
      buildEventCommand: _command,
      buildEventSettings: _settings,
      buildEventError: _eventError,
    );
    flutterUsage.sendEvent(
      category,
      parameter,
      label: label,
      parameters: parameters,
    );
  }
}

/// An event that reports the result of a top-level command.
class CommandResultEvent extends UsageEvent {
  CommandResultEvent(super.commandPath, super.result)
      : super(flutterUsage: globals.flutterUsage);

  @override
  void send() {
    // An event for the command result.
    flutterUsage.sendEvent(
      'tool-command-result',
      category,
      label: parameter,
    );

    // A separate event for the memory highwater mark. This is a separate event
    // so that we can get the command result even if trying to grab maxRss
    // throws an exception.
    try {
      final int maxRss = globals.processInfo.maxRss;
      flutterUsage.sendEvent(
        'tool-command-max-rss',
        category,
        label: parameter,
        value: maxRss,
      );
    } on Exception catch (error) {
      // If grabbing the maxRss fails for some reason, just don't send an event.
      globals.printTrace('Querying maxRss failed with error: $error');
    }
  }
}

/// An event that reports on changes in the configuration of analytics.
class AnalyticsConfigEvent extends UsageEvent {
  AnalyticsConfigEvent({
    /// Whether analytics reporting is being enabled (true) or disabled (false).
    required bool enabled,
  }) : super(
    'analytics',
    'enabled',
    label: enabled ? 'true' : 'false',
    flutterUsage: globals.flutterUsage,
  );
}

/// An event that reports when the code size measurement is run via `--analyze-size`.
class CodeSizeEvent extends UsageEvent {
  CodeSizeEvent(String platform, {
    required Usage flutterUsage,
  }) : super(
    'code-size-analysis',
    platform,
    flutterUsage: flutterUsage
  );
}

/// An event for tracking the usage of specific error handling fallbacks.
class ErrorHandlingEvent extends UsageEvent {
  ErrorHandlingEvent(String parameter) : super('error-handling', parameter, flutterUsage: globals.flutterUsage);
}

/// Emit various null safety analytic events.
///
/// 1. The current null safety runtime mode.
/// 2. The number of packages that are migrated, along with the total number of packages
/// 3. The main packages language version.
class NullSafetyAnalysisEvent implements UsageEvent {
  NullSafetyAnalysisEvent(
    this.packageConfig,
    this.nullSafetyMode,
    this.currentPackage,
    this.flutterUsage,
  );

  /// The category for analytics events related to null safety.
  static const String kNullSafetyCategory = 'null-safety';

  final PackageConfig packageConfig;
  final NullSafetyMode nullSafetyMode;
  final String currentPackage;
  @override
  final Usage flutterUsage;

  @override
  void send() {
    if (packageConfig.packages.isEmpty) {
      return;
    }
    int migrated = 0;
    LanguageVersion? languageVersion;
    for (final Package package in packageConfig.packages) {
      final LanguageVersion? packageLanguageVersion = package.languageVersion;
      if (package.name == currentPackage) {
        languageVersion = packageLanguageVersion;
      }
      if (packageLanguageVersion != null &&
          packageLanguageVersion.major >= nullSafeVersion.major &&
          packageLanguageVersion.minor >= nullSafeVersion.minor) {
        migrated += 1;
      }
    }
    flutterUsage.sendEvent(kNullSafetyCategory, 'runtime-mode', label: nullSafetyMode.toString());
    flutterUsage.sendEvent(kNullSafetyCategory, 'stats', parameters: CustomDimensions(
      nullSafeMigratedLibraries: migrated,
      nullSafeTotalLibraries: packageConfig.packages.length,
    ));
    if (languageVersion != null) {
      final String formattedVersion = '${languageVersion.major}.${languageVersion.minor}';
      flutterUsage.sendEvent(kNullSafetyCategory, 'language-version', label: formattedVersion);
    }
  }

  @override
  String get category => kNullSafetyCategory;

  @override
  String get label => throw UnsupportedError('');

  @override
  String get parameter => throw UnsupportedError('');

  @override
  int get value => throw UnsupportedError('');
}
