// Copyright (c) 2020, 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:dwds/src/debugging/debugger.dart';
import 'package:dwds/src/utilities/synchronized.dart';
import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';

class FrameComputer {
  static final logger = Logger('FrameComputer');

  final Debugger debugger;

  // To ensure that the frames are computed only once, we use an atomic queue
  // to guard the work. Frames are computed sequentially.
  final _queue = AtomicQueue();

  final List<WipCallFrame> _callFrames;
  final List<Frame> _computedFrames = [];

  var _frameIndex = 0;

  StackTrace? _asyncStackTrace;
  List<CallFrame>? _asyncFramesToProcess;

  FrameComputer(this.debugger, this._callFrames, {StackTrace? asyncStackTrace})
      : _asyncStackTrace = asyncStackTrace;

  /// Given a frame index, return the corresponding JS frame.
  WipCallFrame? jsFrameForIndex(int frameIndex) {
    // Clients can send us indices greater than the number of JS frames as async
    // frames don't have corresponding WipCallFrames.
    return frameIndex < _callFrames.length ? _callFrames[frameIndex] : null;
  }

  /// Translates Chrome callFrames contained in [DebuggerPausedEvent] into Dart
  /// [Frame]s.
  Future<List<Frame>> calculateFrames({int? limit}) {
    return _queue.run(() async {
      if (limit != null && _computedFrames.length >= limit) {
        return _computedFrames.take(limit).toList();
      }
      // TODO(grouma) - We compute the frames sequentially. Consider computing
      // frames in parallel batches.
      await _collectSyncFrames(limit: limit);
      await _collectAsyncFrames(limit: limit);

      // Remove any trailing kAsyncSuspensionMarker frame.
      if (limit == null &&
          _computedFrames.isNotEmpty &&
          _computedFrames.last.kind == FrameKind.kAsyncSuspensionMarker) {
        _computedFrames.removeLast();
      }

      return _computedFrames;
    });
  }

  Future<void> _collectSyncFrames({int? limit}) async {
    while (_frameIndex < _callFrames.length) {
      if (limit != null && _computedFrames.length == limit) return;
      try {
        final callFrame = _callFrames[_frameIndex];
        final dartFrame =
            await debugger.calculateDartFrameFor(callFrame, _frameIndex++);
        if (dartFrame != null) {
          _computedFrames.add(dartFrame);
        }
      } catch (e) {
        // If there is an error calculating the frame, then skip it.
        logger.warning('Error calculating sync frame: $e');
      }
    }
    debugger.logAnyFrameErrors(frameType: 'sync');
  }

  Future<void> _collectAsyncFrames({int? limit}) async {
    if (_asyncStackTrace == null) return;

    while (_asyncStackTrace != null) {
      final asyncStackTrace = _asyncStackTrace!;
      if (limit != null && _computedFrames.length == limit) {
        return;
      }

      // We are processing a new set of async frames, add a suspension marker.
      if (_asyncFramesToProcess == null) {
        if (_computedFrames.isNotEmpty &&
            _computedFrames.last.kind != FrameKind.kAsyncSuspensionMarker) {
          _computedFrames.add(
            Frame(
              index: _frameIndex++,
              kind: FrameKind.kAsyncSuspensionMarker,
            ),
          );
        }
        _asyncFramesToProcess = asyncStackTrace.callFrames;
      } else {
        final asyncFramesToProcess = _asyncFramesToProcess!;
        // Process a single async frame.
        if (asyncFramesToProcess.isNotEmpty) {
          try {
            final callFrame = asyncFramesToProcess.removeAt(0);
            final location = WipLocation.fromValues(
              callFrame.scriptId,
              callFrame.lineNumber,
              columnNumber: callFrame.columnNumber,
            );

            final tempWipFrame = WipCallFrame({
              'url': callFrame.url,
              'functionName': callFrame.functionName,
              'location': location.json,
              'scopeChain': [],
            });

            final frame = await debugger.calculateDartFrameFor(
              tempWipFrame,
              _frameIndex++,
              populateVariables: false,
            );
            if (frame != null) {
              frame.kind = FrameKind.kAsyncCausal;
              _computedFrames.add(frame);
            }
          } catch (e) {
            // If there is an error calculating the frame, then skip it.
            logger.warning('Error calculating async frame: $e');
          }
        }
        debugger.logAnyFrameErrors(frameType: 'async');
      }

      // Async frames are no longer on the stack - we don't have local variable
      // information for them.
      if (_asyncFramesToProcess!.isEmpty) {
        _asyncStackTrace = asyncStackTrace.parent;
        _asyncFramesToProcess = null;
      }
    }
  }
}
