import { graphql } from 'gatsby';
import React, { useState, useEffect, useRef } from 'react';
import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { HandlerProps } from 'react-reflex';
import { useMediaQuery } from 'react-responsive';
import { bindActionCreators, Dispatch } from 'redux';
import store from 'store';
import { editor } from 'monaco-editor';
import type { FitAddon } from '@xterm/addon-fit';

import { useFeature } from '@growthbook/growthbook-react';
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
import LearnLayout from '../../../components/layouts/learn';
import { MAX_MOBILE_WIDTH } from '../../../../config/misc';

import type {
  ChallengeFiles,
  ChallengeMeta,
  ChallengeNode,
  Hooks,
  DailyCodingChallengeLanguages,
  DailyCodingChallengeNode,
  DailyCodingChallengePageContext,
  PageContext,
  ResizeProps,
  SavedChallenge,
  SavedChallengeFiles,
  Test
} from '../../../redux/prop-types';
import { isContained } from '../../../utils/is-contained';
import ChallengeDescription from '../components/challenge-description';
import Hotkeys from '../components/hotkeys';
import ResetModal from '../components/reset-modal';
import ChallengeTitle from '../components/challenge-title';
import CompletionModal from '../components/completion-modal';
import HelpModal from '../components/help-modal';
import ShortcutsModal from '../components/shortcuts-modal';
import MobileAppModal from '../components/mobile-app-modal';
import Output from '../components/output';
import Preview, { type PreviewProps } from '../components/preview';
import ProjectPreviewModal from '../components/project-preview-modal';
import SidePanel from '../components/side-panel';
import VideoModal from '../components/video-modal';
import {
  cancelTests,
  challengeMounted,
  createFiles,
  executeChallenge,
  initConsole,
  initTests,
  initHooks,
  initVisibleEditors,
  previewMounted,
  updateChallengeMeta,
  openModal,
  setEditorFocusability,
  setIsAdvancing
} from '../redux/actions';
import {
  challengeFilesSelector,
  consoleOutputSelector,
  isChallengeCompletedSelector
} from '../redux/selectors';
import { savedChallengesSelector } from '../../../redux/selectors';
import { getGuideUrl } from '../utils';
import { preloadPage } from '../../../../utils/gatsby/page-loading';
import envData from '../../../../config/env.json';
import ToolPanel from '../components/tool-panel';
import { getChallengePaths } from '../utils/challenge-paths';
import { challengeHasPreview, isJavaScriptChallenge } from '../utils/build';
import { XtermTerminal } from './xterm';
import MultifileEditor from './multifile-editor';
import DesktopLayout from './desktop-layout';
import MobileLayout from './mobile-layout';
import { mergeChallengeFiles } from './saved-challenges';

import './classic.css';
import '../components/test-frame.css';

const mapStateToProps = (state: unknown) => ({
  challengeFiles: challengeFilesSelector(state) as ChallengeFiles,
  output: consoleOutputSelector(state) as string,
  isChallengeCompleted: isChallengeCompletedSelector(state),
  savedChallenges: savedChallengesSelector(state) as SavedChallenge[]
});

const mapDispatchToProps = (dispatch: Dispatch) =>
  bindActionCreators(
    {
      createFiles,
      initConsole,
      initTests,
      initHooks,
      initVisibleEditors,
      updateChallengeMeta,
      challengeMounted,
      executeChallenge,
      cancelTests,
      previewMounted,
      openModal,
      setEditorFocusability,
      setIsAdvancing
    },
    dispatch
  );

interface ShowClassicProps extends Pick<PreviewProps, 'previewMounted'> {
  cancelTests: () => void;
  challengeMounted: (arg0: string) => void;
  createFiles: (arg0: ChallengeFiles | SavedChallengeFiles) => void;
  dailyCodingChallengeLanguage: DailyCodingChallengeLanguages;
  data: { challengeNode: ChallengeNode | DailyCodingChallengeNode };
  executeChallenge: (options?: { showCompletionModal: boolean }) => void;
  challengeFiles: ChallengeFiles;
  initConsole: (arg0: string) => void;
  initTests: (tests: Test[]) => void;
  initHooks: (hooks?: Hooks) => void;
  initVisibleEditors: () => void;
  isChallengeCompleted: boolean;
  isDailyCodingChallenge?: boolean;
  output: string;
  pageContext: PageContext | DailyCodingChallengePageContext;
  updateChallengeMeta: (arg0: ChallengeMeta) => void;
  openModal: (modal: string) => void;
  setDailyCodingChallengeLanguage: (
    language: DailyCodingChallengeLanguages
  ) => void;
  setEditorFocusability: (canFocus: boolean) => void;
  setIsAdvancing: (arg: boolean) => void;
  savedChallenges: SavedChallenge[];
}

interface ReflexLayout {
  codePane: { flex: number };
  editorPane: { flex: number };
  instructionPane: { flex: number };
  notesPane: { flex: number };
  previewPane: { flex: number };
  testsPane: { flex: number };
}

interface RenderEditorArgs {
  isMobileLayout: boolean;
  isUsingKeyboardInTablist: boolean;
}

const REFLEX_LAYOUT = 'challenge-layout';
const BASE_LAYOUT = {
  codePane: { flex: 1 },
  editorPane: { flex: 1 },
  instructionPane: { flex: 1 },
  previewPane: { flex: 0.7 },
  notesPane: { flex: 0.7 },
  testsPane: { flex: 0.3 }
};

const StepPreview = ({
  dimensions,
  disableIframe,
  previewMounted,
  challengeType,
  xtermFitRef
}: Pick<PreviewProps, 'disableIframe' | 'previewMounted'> & {
  challengeType: number;
  xtermFitRef: React.MutableRefObject<FitAddon | null>;
  dimensions?: { width: number; height: number };
}) => {
  return challengeType === challengeTypes.python ||
    challengeType === challengeTypes.multifilePythonCertProject ||
    challengeType === challengeTypes.pyLab ||
    challengeType === challengeTypes.dailyChallengePy ? (
    <XtermTerminal dimensions={dimensions} xtermFitRef={xtermFitRef} />
  ) : (
    <Preview disableIframe={disableIframe} previewMounted={previewMounted} />
  );
};

// The newline is important, because this text ends up in a `pre` element.
const defaultOutput = `
/**
* Your test output will go here
*/`;

function ShowClassic({
  challengeFiles,
  data: {
    challengeNode: {
      challenge: {
        challengeFiles: seedChallengeFiles,
        block,
        demoType,
        title,
        description,
        instructions,
        id,
        hooks,
        tests,
        challengeType,
        hasEditableBoundaries = false,
        superBlock,
        helpCategory,
        forumTopicId,
        usesMultifileEditor,
        notes,
        videoUrl,
        translationPending,
        saveSubmissionToDB
      }
    }
  },
  pageContext: {
    challengeMeta,
    challengeMeta: { isFirstStep, nextChallengePath },
    projectPreview: { challengeData }
  },
  createFiles,
  cancelTests,
  challengeMounted,
  initConsole,
  initTests,
  initHooks,
  initVisibleEditors,
  dailyCodingChallengeLanguage,
  isDailyCodingChallenge = false,
  setDailyCodingChallengeLanguage,
  updateChallengeMeta,
  openModal,
  setIsAdvancing,
  savedChallenges,
  isChallengeCompleted,
  output,
  executeChallenge,
  previewMounted
}: ShowClassicProps) {
  const { t } = useTranslation();
  const [resizing, setResizing] = useState(false);
  const [usingKeyboardInTablist, setUsingKeyboardInTablist] = useState(false);
  const containerRef = useRef<HTMLElement>(null);
  const editorRef = useRef<editor.IStandaloneCodeEditor>();
  const instructionsPanelRef = useRef<HTMLDivElement>(null);
  const xtermFitRef = useRef<FitAddon | null>(null);
  const isMobile = useMediaQuery({
    query: `(max-width: ${MAX_MOBILE_WIDTH}px)`
  });

  const guideUrl = getGuideUrl({ forumTopicId, title });

  const blockNameTitle = `${t(
    `intro:${superBlock}.blocks.${block}.title`
  )}: ${title}`;
  const windowTitle = `${blockNameTitle} | freeCodeCamp.org`;
  const openConsole = isJavaScriptChallenge({ challengeType });
  const hasPreview = challengeHasPreview({ challengeType });
  const getLayoutState = () => {
    const reflexLayout = store.get(REFLEX_LAYOUT) as ReflexLayout | null;

    // Check that the layout values stored are valid (exist in base layout). If
    // not valid, it will fallback to the base layout values and be set on next
    // user resize.
    const isValidLayout =
      reflexLayout &&
      isContained(Object.keys(BASE_LAYOUT), Object.keys(reflexLayout));

    if (!isValidLayout) store.remove(REFLEX_LAYOUT);

    return isValidLayout ? reflexLayout : BASE_LAYOUT;
  };

  const onPreviewResize = () => xtermFitRef.current?.fit();

  // layout: Holds the information of the panes sizes for desktop view
  const [layout, setLayout] = useState(getLayoutState());

  const onStopResize = (event: HandlerProps) => {
    setResizing(false);
    // 'name' is used to identify the Elements whose layout is stored.
    const { name, flex } = event.component.props;

    // onStopResize can be called multiple times before the state changes, so
    // we need an updater function to ensure all updates are applied.
    setLayout(l => {
      const newLayout = name ? { ...l, [name]: { flex } } : l;
      store.set(REFLEX_LAYOUT, newLayout);
      return newLayout;
    });
  };

  const setHtmlHeight = () => {
    const vh = String(window.innerHeight - 1);
    document.documentElement.style.height = vh + 'px';
  };
  const onResize = () => {
    setResizing(true);
  };
  const resizeProps: ResizeProps = {
    onResize,
    onStopResize
  };

  const updateUsingKeyboardInTablist = (
    usingKeyboardInTablist: boolean
  ): void => {
    setUsingKeyboardInTablist(usingKeyboardInTablist);
  };

  // AB testing Pre-fetch in the Spanish locale
  const isPreFetchEnabled = useFeature('prefetch_ab_test').on;

  // Independent lower jaw is only enabled for desktop.
  const showIndependentLowerJaw = !isMobile;

  const showSidePanelTests = isMobile || !hasEditableBoundaries;

  // Show test

  useEffect(() => {
    if (isPreFetchEnabled && envData.clientLocale === 'espanol') {
      preloadPage(nextChallengePath);
    }
  }, [nextChallengePath, isPreFetchEnabled]);

  useEffect(() => {
    initializeComponent(title);

    window.addEventListener('resize', setHtmlHeight);
    setHtmlHeight();

    return () => {
      createFiles([]);
      cancelTests();
      window.removeEventListener('resize', setHtmlHeight);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dailyCodingChallengeLanguage]);

  const initializeComponent = (title: string): void => {
    initConsole('');

    const savedChallenge = savedChallenges?.find(challenge => {
      return challenge.id === challengeMeta.id;
    });

    createFiles(
      mergeChallengeFiles(seedChallengeFiles, savedChallenge?.challengeFiles)
    );

    initTests(tests);
    initHooks(hooks);

    initVisibleEditors();

    // Typically, this kind of preview only appears on the first step of a
    // project and is shown (once) automatically. In contrast, labs are more
    // freeform, so the preview is shown on demand.
    if (demoType === 'onLoad') openModal('projectPreview');
    const challengePaths = getChallengePaths({
      currentCurriculumPaths: challengeMeta
    });

    updateChallengeMeta({
      ...challengeMeta,
      title,
      challengeType,
      helpCategory,
      description,
      ...challengePaths
    });
    challengeMounted(challengeMeta.id);
    setIsAdvancing(false);
  };

  const renderInstructionsPanel = ({
    toolPanel,
    hasDemo
  }: {
    toolPanel: React.ReactNode;
    hasDemo: boolean;
  }) => {
    return (
      <SidePanel
        challengeDescription={
          <ChallengeDescription
            description={description}
            instructions={instructions}
            superBlock={superBlock}
            challengeId={id}
            block={block}
          />
        }
        challengeTitle={
          <ChallengeTitle
            isCompleted={isChallengeCompleted}
            translationPending={translationPending}
          >
            {title}
          </ChallengeTitle>
        }
        instructionsPanelRef={instructionsPanelRef}
        toolPanel={toolPanel}
        hasDemo={hasDemo}
        showSidePanelTests={showSidePanelTests}
      />
    );
  };

  const renderEditor = ({
    isMobileLayout,
    isUsingKeyboardInTablist
  }: RenderEditorArgs) => {
    return (
      challengeFiles && (
        <MultifileEditor
          challengeFiles={challengeFiles}
          block={block}
          superBlock={superBlock}
          containerRef={containerRef}
          description={description}
          editorRef={editorRef}
          initialTests={tests}
          isMobileLayout={isMobileLayout}
          isUsingKeyboardInTablist={isUsingKeyboardInTablist}
          resizeProps={resizeProps}
          title={title}
          usesMultifileEditor={usesMultifileEditor}
          showProjectPreview={demoType === 'onLoad'}
          showIndependentLowerJaw={showIndependentLowerJaw}
        />
      )
    );
  };

  const usesTerminal =
    challengeType === challengeTypes.python ||
    challengeType === challengeTypes.multifilePythonCertProject ||
    challengeType === challengeTypes.pyLab ||
    challengeType === challengeTypes.dailyChallengePy;

  return (
    <Hotkeys
      challengeType={challengeType}
      executeChallenge={executeChallenge}
      containerRef={containerRef}
      instructionsPanelRef={instructionsPanelRef}
      usesMultifileEditor={usesMultifileEditor}
      editorRef={editorRef}
      showIndependentLowerJaw={showIndependentLowerJaw}
    >
      <LearnLayout>
        <Helmet title={windowTitle} />
        {isMobile ? (
          <MobileLayout
            editor={renderEditor({
              isMobileLayout: true,
              isUsingKeyboardInTablist: usingKeyboardInTablist
            })}
            hasEditableBoundaries={hasEditableBoundaries}
            hasPreview={hasPreview}
            instructions={renderInstructionsPanel({
              toolPanel: null,
              hasDemo: demoType === 'onClick'
            })}
            notes={notes}
            onPreviewResize={onPreviewResize}
            preview={
              <StepPreview
                challengeType={challengeType}
                disableIframe={resizing}
                previewMounted={previewMounted}
                xtermFitRef={xtermFitRef}
              />
            }
            windowTitle={windowTitle}
            testOutput={
              <Output defaultOutput={defaultOutput} output={output} />
            }
            toolPanel={
              <ToolPanel guideUrl={guideUrl} isMobile videoUrl={videoUrl} />
            }
            updateUsingKeyboardInTablist={updateUsingKeyboardInTablist}
            usesMultifileEditor={usesMultifileEditor}
            usesTerminal={usesTerminal}
          />
        ) : (
          <DesktopLayout
            challengeFiles={challengeFiles}
            challengeType={challengeType}
            editor={renderEditor({
              isMobileLayout: false,
              isUsingKeyboardInTablist: usingKeyboardInTablist
            })}
            hasEditableBoundaries={hasEditableBoundaries}
            hasPreview={hasPreview}
            instructions={renderInstructionsPanel({
              toolPanel: null,
              hasDemo: demoType === 'onClick'
            })}
            isDailyCodingChallenge={isDailyCodingChallenge}
            dailyCodingChallengeLanguage={dailyCodingChallengeLanguage}
            setDailyCodingChallengeLanguage={setDailyCodingChallengeLanguage}
            isFirstStep={isFirstStep}
            layoutState={layout}
            notes={notes}
            onPreviewResize={onPreviewResize}
            preview={
              <StepPreview
                challengeType={challengeType}
                disableIframe={resizing}
                previewMounted={previewMounted}
                xtermFitRef={xtermFitRef}
              />
            }
            resizeProps={resizeProps}
            testOutput={
              <Output defaultOutput={defaultOutput} output={output} />
            }
            windowTitle={windowTitle}
            startWithConsoleShown={openConsole}
            showIndependentLowerJaw={showIndependentLowerJaw}
          />
        )}
        <CompletionModal />
        <HelpModal
          challengeTitle={title}
          challengeBlock={block}
          superBlock={superBlock}
          guideUrl={guideUrl}
          videoUrl={videoUrl}
        />
        <VideoModal videoUrl={videoUrl} />
        <ResetModal
          saveSubmissionToDB={saveSubmissionToDB}
          challengeTitle={title}
        />
        <ProjectPreviewModal
          challengeData={challengeData}
          closeText={t('buttons.start-coding')}
          previewTitle={
            demoType === 'onClick'
              ? t('learn.demo-project-title')
              : t('learn.project-preview-title')
          }
        />
        <ShortcutsModal />
        <MobileAppModal superBlock={superBlock} />
      </LearnLayout>
    </Hotkeys>
  );
}

ShowClassic.displayName = 'ShowClassic';

export default connect(mapStateToProps, mapDispatchToProps)(ShowClassic);

export const query = graphql`
  query ClassicChallenge($id: String!) {
    challengeNode(id: { eq: $id }) {
      challenge {
        block
        demoType
        title
        description
        id
        hasEditableBoundaries
        instructions
        notes
        challengeType
        helpCategory
        videoUrl
        superBlock
        translationPending
        forumTopicId
        hooks {
          beforeAll
          beforeEach
          afterEach
          afterAll
        }
        fields {
          slug
        }
        required {
          link
          src
        }
        usesMultifileEditor
        challengeFiles {
          fileKey
          ext
          name
          contents
          head
          tail
          editableRegionBoundaries
          history
        }
        saveSubmissionToDB
        tests {
          text
          testString
        }
      }
    }
  }
`;
