// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as SDK from '../../../../core/sdk/sdk.js';
import * as Formatter from '../../../../models/formatter/formatter.js';
import * as SourceMapScopes from '../../../../models/source_map_scopes/source_map_scopes.js';
import * as Acorn from '../../../../third_party/acorn/acorn.js';
import * as UI from '../../legacy.js';

import {RemoteObjectPreviewFormatter} from './RemoteObjectPreviewFormatter.js';

export class JavaScriptREPL {
  static wrapObjectLiteral(code: string): string {
    // Only parenthesize what appears to be an object literal.
    const result = /^\s*\{\s*(.*)\s*\}[\s;]*$/.exec(code);
    if (result === null) {
      return code;
    }
    const [, body] = result;
    let level = 0;
    for (const c of body) {
      if (c === '{') {
        level++;
      } else if (c === '}' && --level < 0) {
        return code;
      }
    }

    const parse = (expression: string): void => void Acorn.parse(
        expression,
        {ecmaVersion: 2022, allowAwaitOutsideFunction: true, ranges: false, allowReturnOutsideFunction: true});
    try {
      // Check if the body can be interpreted as an expression.
      parse('return {' + body + '};');

      // No syntax error! Does it work parenthesized?
      const wrappedCode = '({' + body + '})';
      parse(wrappedCode);

      return wrappedCode;
    } catch {
      return code;
    }
  }

  static async evaluate(
      text: string, executionContext: SDK.RuntimeModel.ExecutionContext, throwOnSideEffect: boolean, replMode: boolean,
      timeout?: number, objectGroup?: string, awaitPromise = false,
      silent = false): Promise<SDK.RuntimeModel.EvaluationResult|null> {
    const isTextLong = text.length > maxLengthForEvaluation;
    if (!text || (throwOnSideEffect && isTextLong)) {
      return null;
    }

    let expression = text;
    const callFrame = executionContext.debuggerModel.selectedCallFrame();
    if (callFrame?.script.isJavaScript()) {
      const nameMap = await SourceMapScopes.NamesResolver.allVariablesInCallFrame(callFrame);
      try {
        expression =
            await Formatter.FormatterWorkerPool.formatterWorkerPool().javaScriptSubstitute(expression, nameMap);
      } catch {
      }
    }

    expression = JavaScriptREPL.wrapObjectLiteral(expression);
    const options = {
      expression,
      generatePreview: true,
      includeCommandLineAPI: true,
      throwOnSideEffect,
      timeout,
      objectGroup,
      disableBreaks: true,
      replMode,
      silent,
    };
    return await executionContext.evaluate(options, false /* userGesture */, awaitPromise);
  }

  static async evaluateAndBuildPreview(
      text: string, throwOnSideEffect: boolean, replMode: boolean, timeout?: number, allowErrors?: boolean,
      objectGroup?: string, awaitPromise = false, silent = false): Promise<{
    preview: DocumentFragment,
    result: SDK.RuntimeModel.EvaluationResult|null,
  }> {
    const executionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext);
    if (!executionContext) {
      return {preview: document.createDocumentFragment(), result: null};
    }

    const result = await JavaScriptREPL.evaluate(
        text, executionContext, throwOnSideEffect, replMode, timeout, objectGroup, awaitPromise, silent);
    if (!result) {
      return {preview: document.createDocumentFragment(), result: null};
    }

    const formatter = new RemoteObjectPreviewFormatter();
    const preview = formatter.renderEvaluationResultPreviewFragment(result, allowErrors);
    return {preview, result};
  }
}

const maxLengthForEvaluation = 2000;
