// Copyright (c) 2019, 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 'package:analyzer/instrumentation/logger.dart';
import 'package:analyzer/instrumentation/plugin_data.dart';
import 'package:analyzer/instrumentation/service.dart';

/// A class to adapt an [InstrumentationService] into a log using an [InstrumentationLogger].
class InstrumentationLogAdapter implements InstrumentationService {
  static const String TAG_ERROR = 'Err';
  static const String TAG_EXCEPTION = 'Ex';
  static const String TAG_INFO = 'Info';
  static const String TAG_LOG_ENTRY = 'Log';
  static const String TAG_NOTIFICATION = 'Noti';
  static const String TAG_PLUGIN_ERROR = 'PluginErr';
  static const String TAG_PLUGIN_EXCEPTION = 'PluginEx';
  static const String TAG_PLUGIN_NOTIFICATION = 'PluginNoti';
  static const String TAG_PLUGIN_REQUEST = 'PluginReq';
  static const String TAG_PLUGIN_RESPONSE = 'PluginRes';
  static const String TAG_PLUGIN_TIMEOUT = 'PluginTo';
  static const String TAG_REQUEST = 'Req';
  static const String TAG_RESPONSE = 'Res';
  static const String TAG_VERSION = 'Ver';
  static const String TAG_WATCH_EVENT = 'Watch';

  /// A logger used to log instrumentation in string format.
  final InstrumentationLogger _instrumentationLogger;

  /// Files that should not have their watch events logged (to prevent feedback
  /// loops).
  final Set<String>? _watchEventExclusionFiles;

  /// Initialize a newly created instrumentation service to communicate with the
  /// given [_instrumentationLogger].
  ///
  /// Any file paths in [watchEventExclusionFiles] will be excluded from the
  /// logging of watch events (to prevent feedback loops).
  InstrumentationLogAdapter(
    this._instrumentationLogger, {
    Set<String>? watchEventExclusionFiles,
  }) : _watchEventExclusionFiles = watchEventExclusionFiles;

  /// The current time, expressed as a decimal encoded number of milliseconds.
  String get _timestamp => DateTime.now().millisecondsSinceEpoch.toString();

  @override
  void logError(String message) => _log(TAG_ERROR, message);

  @override
  void logException(
    dynamic exception, [
    StackTrace? stackTrace,
    List<InstrumentationServiceAttachment>? attachments,
  ]) {
    String message = _toString(exception);
    String trace = _toString(stackTrace);
    _instrumentationLogger.log(_join([TAG_EXCEPTION, message, trace]));
  }

  @override
  void logInfo(String message, [dynamic exception]) =>
      _log(TAG_INFO, message + (exception == null ? "" : exception.toString()));

  @override
  void logLogEntry(
    String level,
    DateTime? time,
    String message,
    Object exception,
    StackTrace stackTrace,
  ) {
    String timeStamp = time == null
        ? 'null'
        : time.millisecondsSinceEpoch.toString();
    String exceptionText = exception.toString();
    String stackTraceText = stackTrace.toString();
    _instrumentationLogger.log(
      _join([
        TAG_LOG_ENTRY,
        level,
        timeStamp,
        message,
        exceptionText,
        stackTraceText,
      ]),
    );
  }

  @override
  void logNotification(String notification) =>
      _log(TAG_NOTIFICATION, notification);

  @override
  void logPluginError(
    PluginData plugin,
    String code,
    String message,
    String stackTrace,
  ) {
    List<String> fields = <String>[TAG_PLUGIN_ERROR, code, message, stackTrace];
    plugin.addToFields(fields);
    _instrumentationLogger.log(_join(fields));
  }

  @override
  void logPluginException(
    PluginData plugin,
    dynamic exception,
    StackTrace? stackTrace,
  ) {
    List<String> fields = <String>[
      TAG_PLUGIN_EXCEPTION,
      _toString(exception),
      _toString(stackTrace),
    ];
    plugin.addToFields(fields);
    _instrumentationLogger.log(_join(fields));
  }

  @override
  void logPluginNotification(String pluginId, String notification) {
    _instrumentationLogger.log(
      _join([TAG_PLUGIN_NOTIFICATION, notification, pluginId, '', '']),
    );
  }

  @override
  void logPluginRequest(String pluginId, String request) {
    _instrumentationLogger.log(
      _join([TAG_PLUGIN_REQUEST, request, pluginId, '', '']),
    );
  }

  @override
  void logPluginResponse(String pluginId, String response) {
    _instrumentationLogger.log(
      _join([TAG_PLUGIN_RESPONSE, response, pluginId, '', '']),
    );
  }

  @override
  void logPluginTimeout(PluginData plugin, String request) {
    List<String> fields = <String>[TAG_PLUGIN_TIMEOUT, request];
    plugin.addToFields(fields);
    _instrumentationLogger.log(_join(fields));
  }

  @override
  void logRequest(String request) => _log(TAG_REQUEST, request);

  @override
  void logResponse(String response) => _log(TAG_RESPONSE, response);

  @override
  void logVersion(
    String uuid,
    String clientId,
    String clientVersion,
    String serverVersion,
    String sdkVersion,
  ) {
    String normalize(String? value) =>
        value != null && value.isNotEmpty ? value : 'unknown';

    _instrumentationLogger.log(
      _join([
        TAG_VERSION,
        uuid,
        normalize(clientId),
        normalize(clientVersion),
        serverVersion,
        sdkVersion,
      ]),
    );
  }

  @override
  void logWatchEvent(String folderPath, String filePath, String changeType) {
    if (_watchEventExclusionFiles?.contains(filePath) ?? false) {
      return;
    }

    _instrumentationLogger.log(
      _join([TAG_WATCH_EVENT, folderPath, filePath, changeType]),
    );
  }

  @override
  Future<void> shutdown() async {
    await _instrumentationLogger.shutdown();
  }

  /// Write an escaped version of the given [field] to the given [buffer].
  void _escape(StringBuffer buffer, String field) {
    int index = field.indexOf(':');
    if (index < 0) {
      buffer.write(field);
      return;
    }
    int start = 0;
    while (index >= 0) {
      buffer.write(field.substring(start, index));
      buffer.write('::');
      start = index + 1;
      index = field.indexOf(':', start);
    }
    buffer.write(field.substring(start));
  }

  /// Return the result of joining the values of the given fields, escaping the
  /// separator character by doubling it.
  String _join(List<String> fields) {
    StringBuffer buffer = StringBuffer();
    buffer.write(_timestamp);
    int length = fields.length;
    for (int i = 0; i < length; i++) {
      buffer.write(':');
      _escape(buffer, fields[i]);
    }
    return buffer.toString();
  }

  /// Log the given message with the given tag.
  void _log(String tag, String message) {
    _instrumentationLogger.log(_join([tag, message]));
  }

  /// Convert the given [object] to a string.
  String _toString(Object? object) {
    if (object == null) {
      return 'null';
    }
    return object.toString();
  }
}
