import React from "react";
import { CodeEditor, CodeEditorRef } from "../ui/code-editor";
import { InputEditor, InputEditorRef } from "../ui/input-editor";
import { MainLayout } from "../ui/MainLayout";
import { useExecController } from "../ui/use-exec-controller";
import { LanguageProvider, StepExecutionResult } from "../engines/types";
import { OutputViewer, OutputViewerRef } from "../ui/output-viewer";
import { ExecutionControls } from "./execution-controls";
import { RendererRef, RendererWrapper } from "./renderer-wrapper";
import { WorkerRuntimeError } from "../engines/worker-errors";

type Props<RS> = {
  langName: string;
  provider: LanguageProvider<RS>;
};

/**
 * React component that contains and controls the entire IDE.
 *
 * For performance reasons, Mainframe makes spare use of state hooks. This
 * component is rather expensive to render, and will block the main thread on
 * small execution intervals if rendered on every execution. All state management
 * is delegated to imperatively controlled child components.
 */
export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
  // Language provider and engine
  const providerRef = React.useRef(provider);
  const execController = useExecController(langName);

  // Refs for controlling UI components
  const codeEditorRef = React.useRef<CodeEditorRef>(null);
  const inputEditorRef = React.useRef<InputEditorRef>(null);
  const outputEditorRef = React.useRef<OutputViewerRef>(null);
  const rendererRef = React.useRef<RendererRef<any>>(null);

  // Interval of execution
  const [execInterval, setExecInterval] = React.useState(20);

  /** Utility that updates UI with the provided execution result */
  const updateWithResult = (
    result: StepExecutionResult<any>,
    error?: WorkerRuntimeError
  ) => {
    rendererRef.current!.updateState(result.rendererState);
    codeEditorRef.current!.updateHighlights(result.nextStepLocation);
    outputEditorRef.current!.append(result.output);

    // Self-modifying programs: update code
    if (result.codeEdits != null)
      codeEditorRef.current!.editCode(result.codeEdits);

    // End of program: reset code to original version
    if (!result.nextStepLocation) codeEditorRef.current!.endExecutionMode();

    // RuntimeError: print error to output
    if (error) outputEditorRef.current!.setError(error);
  };

  /** Reset and begin a new execution */
  const runProgram = async () => {
    // Check if controller is free for execution
    const readyStates = ["empty", "done"];
    if (!readyStates.includes(execController.state)) {
      console.error(`Controller not ready: state is ${execController.state}`);
      return;
    }

    // Reset any existing execution state
    outputEditorRef.current!.reset();
    await execController.resetState();
    const error = await execController.prepare(
      codeEditorRef.current!.getCode(),
      inputEditorRef.current!.getValue()
    );

    // Check for ParseError, else begin execution
    if (error) outputEditorRef.current!.setError(error);
    else {
      codeEditorRef.current!.startExecutionMode();
      await execController.execute(updateWithResult, execInterval);
    }
  };

  /** Pause the ongoing execution */
  const pauseExecution = async () => {
    // Check if controller is indeed executing code
    if (execController.state !== "processing") {
      console.error("Controller not processing any code");
      return;
    }
    await execController.pauseExecution();
  };

  /** Run a single step of execution */
  const executeStep = async () => {
    // Check if controller is paused
    if (execController.state !== "paused") {
      console.error("Controller not paused");
      return;
    }

    // Run and update execution states
    const response = await execController.executeStep();
    updateWithResult(response.result, response.error);
  };

  /** Resume the currently paused execution */
  const resumeExecution = async () => {
    // Check if controller is indeed paused
    if (execController.state !== "paused") {
      console.error("Controller is not paused");
      return;
    }

    // Begin execution
    await execController.execute(updateWithResult, execInterval);
  };

  /** Stop the currently active execution */
  const stopExecution = async () => {
    // Check if controller has execution
    if (!["paused", "processing", "error"].includes(execController.state)) {
      console.error("No active execution in controller");
      return;
    }

    // If currently processing, pause execution loop first
    if (execController.state === "processing")
      await execController.pauseExecution();

    // Reset all execution states
    await execController.resetState();
    rendererRef.current!.updateState(null);
    codeEditorRef.current!.updateHighlights(null);
    codeEditorRef.current!.endExecutionMode();
  };

  /** Translate execution controller state to debug controls state */
  const getDebugState = () => {
    const currState = execController.state;
    if (currState === "processing") return "running";
    else if (currState === "paused") return "paused";
    else if (currState === "error") return "error";
    else return "off";
  };

  return (
    <MainLayout
      renderEditor={() => (
        <CodeEditor
          ref={codeEditorRef}
          languageId={langName}
          defaultValue={providerRef.current.sampleProgram}
          tokensProvider={providerRef.current.editorTokensProvider}
          onValidateCode={execController.validateCode}
          onUpdateBreakpoints={(newPoints) =>
            execController.updateBreakpoints(newPoints)
          }
        />
      )}
      renderRenderer={() => (
        <RendererWrapper
          ref={rendererRef}
          renderer={providerRef.current.Renderer as any}
        />
      )}
      renderInput={() => (
        <InputEditor
          ref={inputEditorRef}
          readOnly={execController.state === "processing"}
        />
      )}
      renderOutput={() => <OutputViewer ref={outputEditorRef} />}
      renderExecControls={() => (
        <ExecutionControls
          state={getDebugState()}
          onRun={runProgram}
          onPause={pauseExecution}
          onResume={resumeExecution}
          onStep={executeStep}
          onStop={stopExecution}
          onChangeInterval={setExecInterval}
        />
      )}
    />
  );
};