/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {
  CodeIcon,
  DocumentAddIcon,
  InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
import {type CompilerError} from 'babel-plugin-react-compiler';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {memo, ReactNode, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {BabelFileResult} from '@babel/core';
const MemoizedOutput = memo(Output);

export default MemoizedOutput;

export type PrintedCompilerPipelineValue =
  | {
      kind: 'hir';
      name: string;
      fnName: string | null;
      value: string;
    }
  | {kind: 'reactive'; name: string; fnName: string | null; value: string}
  | {kind: 'debug'; name: string; fnName: string | null; value: string};

export type CompilerTransformOutput = {
  code: string;
  sourceMaps: BabelFileResult['map'];
  language: 'flow' | 'typescript';
};
export type CompilerOutput =
  | {
      kind: 'ok';
      transformOutput: CompilerTransformOutput;
      results: Map<string, Array<PrintedCompilerPipelineValue>>;
    }
  | {
      kind: 'err';
      results: Map<string, Array<PrintedCompilerPipelineValue>>;
      error: CompilerError;
    };

type Props = {
  store: Store;
  compilerOutput: CompilerOutput;
};

async function tabify(
  source: string,
  compilerOutput: CompilerOutput,
): Promise<Map<string, ReactNode>> {
  const tabs = new Map<string, React.ReactNode>();
  const reorderedTabs = new Map<string, React.ReactNode>();
  const concattedResults = new Map<string, string>();
  // Concat all top level function declaration results into a single tab for each pass
  for (const [passName, results] of compilerOutput.results) {
    for (const result of results) {
      switch (result.kind) {
        case 'hir': {
          const prev = concattedResults.get(result.name);
          const next = result.value;
          const identName = `function ${result.fnName}`;
          if (prev != null) {
            concattedResults.set(passName, `${prev}\n\n${identName}\n${next}`);
          } else {
            concattedResults.set(passName, `${identName}\n${next}`);
          }
          break;
        }
        case 'reactive': {
          const prev = concattedResults.get(passName);
          const next = result.value;
          if (prev != null) {
            concattedResults.set(passName, `${prev}\n\n${next}`);
          } else {
            concattedResults.set(passName, next);
          }
          break;
        }
        case 'debug': {
          concattedResults.set(passName, result.value);
          break;
        }
        default: {
          const _: never = result;
          throw new Error('Unexpected result kind');
        }
      }
    }
  }
  let lastPassOutput: string | null = null;
  let nonDiffPasses = ['HIR', 'BuildReactiveFunction', 'EnvironmentConfig'];
  for (const [passName, text] of concattedResults) {
    tabs.set(
      passName,
      <TextTabContent
        output={text}
        diff={lastPassOutput}
        showInfoPanel={!nonDiffPasses.includes(passName)}></TextTabContent>,
    );
    lastPassOutput = text;
  }
  // Ensure that JS and the JS source map come first
  if (compilerOutput.kind === 'ok') {
    const {transformOutput} = compilerOutput;
    const sourceMapUrl = getSourceMapUrl(
      transformOutput.code,
      JSON.stringify(transformOutput.sourceMaps),
    );
    const code = await prettier.format(transformOutput.code, {
      semi: true,
      parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
      plugins: [parserBabel, prettierPluginEstree],
    });
    reorderedTabs.set(
      'JS',
      <TextTabContent
        output={code}
        diff={null}
        showInfoPanel={false}></TextTabContent>,
    );
    if (sourceMapUrl) {
      reorderedTabs.set(
        'SourceMap',
        <>
          <iframe
            src={sourceMapUrl}
            className="w-full h-monaco_small sm:h-monaco"
            title="Generated Code"
          />
        </>,
      );
    }
  }
  tabs.forEach((tab, name) => {
    reorderedTabs.set(name, tab);
  });
  return reorderedTabs;
}

function utf16ToUTF8(s: string): string {
  return unescape(encodeURIComponent(s));
}

function getSourceMapUrl(code: string, map: string): string | null {
  code = utf16ToUTF8(code);
  map = utf16ToUTF8(map);
  return `https://evanw.github.io/source-map-visualization/#${btoa(
    `${code.length}\0${code}${map.length}\0${map}`,
  )}`;
}

function Output({store, compilerOutput}: Props): JSX.Element {
  const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
  const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
    () => new Map(),
  );
  useEffect(() => {
    tabify(store.source, compilerOutput).then(tabs => {
      setTabs(tabs);
    });
  }, [store.source, compilerOutput]);

  const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
  let lastResult: string = '';
  for (const [passName, results] of compilerOutput.results) {
    for (const result of results) {
      let currResult = '';
      if (result.kind === 'hir' || result.kind === 'reactive') {
        currResult += `function ${result.fnName}\n\n${result.value}`;
      }
      if (currResult !== lastResult) {
        changedPasses.add(passName);
      }
      lastResult = currResult;
    }
  }

  return (
    <>
      <TabbedWindow
        defaultTab="HIR"
        setTabsOpen={setTabsOpen}
        tabsOpen={tabsOpen}
        tabs={tabs}
        changedPasses={changedPasses}
      />
      {compilerOutput.kind === 'err' ? (
        <div
          className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
          style={{width: 'calc(100vw - 650px)'}}>
          <div className="w-full p-4 basis-full border-b">
            <h2>COMPILER ERRORS</h2>
          </div>
          <pre
            className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
            style={{width: 'calc(100vw - 650px)', height: '150px'}}>
            <code>{compilerOutput.error.toString()}</code>
          </pre>
        </div>
      ) : null}
    </>
  );
}

function TextTabContent({
  output,
  diff,
  showInfoPanel,
}: {
  output: string;
  diff: string | null;
  showInfoPanel: boolean;
}): JSX.Element {
  const [diffMode, setDiffMode] = useState(false);
  return (
    /**
     * Restrict MonacoEditor's height, since the config autoLayout:true
     * will grow the editor to fit within parent element
     */
    <div className="w-full h-monaco_small sm:h-monaco">
      {showInfoPanel ? (
        <div className="flex items-center gap-1 bg-amber-50 p-2">
          {diff != null && output !== diff ? (
            <button
              className="flex items-center gap-1 transition-colors duration-150 ease-in text-secondary hover:text-link"
              onClick={() => setDiffMode(diffMode => !diffMode)}>
              {!diffMode ? (
                <>
                  <DocumentAddIcon className="w-5 h-5" /> Show Diff
                </>
              ) : (
                <>
                  <CodeIcon className="w-5 h-5" /> Show Output
                </>
              )}
            </button>
          ) : (
            <>
              <span className="flex items-center gap-1">
                <InformationCircleIcon className="w-5 h-5" /> No changes from
                previous pass
              </span>
            </>
          )}
        </div>
      ) : null}
      {diff != null && diffMode ? (
        <DiffEditor
          original={diff}
          modified={output}
          options={{
            ...monacoOptions,
            readOnly: true,
            lineNumbers: 'off',
            glyphMargin: false,
            // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
            lineDecorationsWidth: 0,
            lineNumbersMinChars: 0,
          }}
        />
      ) : (
        <MonacoEditor
          defaultLanguage="javascript"
          value={output}
          options={{
            ...monacoOptions,
            readOnly: true,
            lineNumbers: 'off',
            glyphMargin: false,
            // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
            lineDecorationsWidth: 0,
            lineNumbersMinChars: 0,
          }}
        />
      )}
    </div>
  );
}
