// Copyright (c) 2023, 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:dds_service_extensions/dap.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;

import '../dap.dart';
import 'dap/adapters/dds_hosted_adapter.dart';
import 'dap/constants.dart';
import 'dds_impl.dart';

/// Responds to incoming DAP messages using a debug adapter connected to DDS.
class DapHandler {
  DapHandler(this.dds);

  final _initializedCompleter = Completer<void>();

  Future<Map<String, dynamic>> sendRequest(
    DdsHostedAdapter adapter,
    json_rpc.Parameters parameters,
  ) async {
    if (adapter.ddsUri == null) {
      await _startAdapter(adapter);
    }

    // TODO(helin24): Consider a sequence offset for incoming messages to avoid
    // overlapping sequence numbers with startup requests.
    final message = parameters['message'].asString;

    // TODO(dantup): If/when DAP needs to care about ordering (eg. it handles
    // both requests and events), this will need to be changed to have the
    // caller provide a "responseWriter" function so the result can be written
    // directly to the stream synchronously, to avoid future events being able
    // to be inserted before the response (eg. initializedEvent).
    final responseCompleter = Completer<Response>();
    adapter.handleMessage(message, responseCompleter.complete);
    final result = await responseCompleter.future;

    return <String, dynamic>{
      'type': 'DapResponse',
      'dapResponse': result,
    };
  }

  void _handleEvent(Event event) {
    if (event.event == 'initialized') {
      _initializedCompleter.complete();
    }
    dds.streamManager.streamNotify(DapEventStreams.kDAP, {
      'streamId': DapEventStreams.kDAP,
      'event': {
        'kind': DapEventKind.kDAPEvent,
        'timestamp': DateTime.now().millisecondsSinceEpoch,
        'dapData': event,
      },
    });
  }

  Future<void> _startAdapter(DdsHostedAdapter adapter) async {
    adapter.ddsUri = dds.uri;
    adapter.setEventHandler(_handleEvent);

    // TODO(helin24): Most likely we'll want the client to do these
    // initialization steps so that clients can differentiate capabilities. This
    // may require a custom stream for the debug adapter.

    // Each DAP request has a `seq` number (essentially a message ID) which
    // should be unique.
    //
    // We send a few requests to initialize the adapter, but these are not
    // visible to the DDS client so if we start at 1, the IDs will be
    // reused.
    //
    // To avoid that, for our own initialization requests, use negative numbers
    // (though they must still ascend) so there's no overlay with the messages
    // we'll forward from the DDS client.
    int seq = -1000;
    await adapter.initializeRequest(
      Request(
        command: Command.initialize,
        seq: seq,
      ),
      DartInitializeRequestArguments(
        adapterID: 'dds-dap-handler',
      ),
      (capabilities) {},
    );
    await _initializedCompleter.future;
    await adapter.configurationDoneRequest(
      Request(
        arguments: const {},
        command: Command.configurationDone,
        seq: seq++,
      ),
      ConfigurationDoneArguments(),
      noopCallback,
    );
    await adapter.attachRequest(
      Request(
        arguments: const {},
        command: Command.attach,
        seq: seq++,
      ),
      DartAttachRequestArguments(
        vmServiceUri: dds.remoteVmServiceUri.toString(),
      ),
      noopCallback,
    );
    // Wait for the debugger to fully initialize, because the request that
    // triggered this initialization may require things like isolates that will
    // only be known after the debugger has initialized.
    await adapter.debuggerInitialized;
  }

  final DartDevelopmentServiceImpl dds;
}

void noopCallback() {}
