import React, { Component } from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

import './CodeTester.css';
import Editor from '../Editor/Editor';
import Console from '../Console/Console';
import InstructionsPanel from '../InstructionsPanel/InstructionsPanel';
import CodeTesterHeader from '../CodeTesterHeader/CodeTesterHeader';

import EditModeTabs from '../EditModeTabs/EditModeTabs';
import { defaultFunc } from './helpers';
import { renderIf } from '../../helpers';

class CodeTester extends Component {
  constructor(props) {
    super(props);
    this.state = {
      code: '',
      lintedCode: '',
      consoleLogs: [],
      consoleVisible: props.showConsole,
      consoleStyle: {
        right: '-40%'
      },
      testsVisible: false,
      selectedTabIndex: 0,
      lintErrLineNums: [],
    };
    this.handleLintedCode = this.handleLintedCode.bind(this);
    this.handleConsoleLog = this.handleConsoleLog.bind(this);
    this.handleToggleConsole = this.handleToggleConsole.bind(this);
    this.handleLintErrors = this.handleLintErrors.bind(this);
    this.handleRunClick = this.handleRunClick.bind(this);
    this.handlePaste = this.handlePaste.bind(this);
    this.openConsole = this.openConsole.bind(this);
    this.updateCode = this.updateCode.bind(this);
    this.renderEditorPanel = this.renderEditorPanel.bind(this);
    this.toggleInstructions = this.toggleInstructions.bind(this);
    this.onTabSelect = this.onTabSelect.bind(this);
  }

  onTabSelect(selectedTabIndex, prevIndex, event, cb = () => { }) {
    this.setState(({ editMode: selectedTabIndex === 2, selectedTabIndex }), cb);
  }

  handleLintedCode(lintedCode) {
    // This function is only called if code passes linting rules
    const prevCode = this.state.lintedCode;
    // setState causes a re-render, so we need to make sure the code is different
    // from the code we already have.
    if (lintedCode !== prevCode) {
      this.setState({ lintedCode });
      this.props.onLintedCode(lintedCode);
    }
  }

  updateCode(code) {
    /* This function is called when any code is entered,
      whether it passes linting rules or not.
    */
    const { handleBoilerplateChange, onAnyCode } = this.props;
    const { code: prevCode, editMode } = this.state;
    if (prevCode !== code) {
      this.setState({ code });
      onAnyCode(code);
    }
    if (editMode) {
      handleBoilerplateChange(code);
    }
  }

  toggleInstructions() {
    this.setState({ testsVisible: !this.state.testsVisible });
  }

  handleToggleConsole() {
    const { consoleVisible } = this.state;
    this.openConsole(!consoleVisible);
  }

  openConsole(toOpen) {
    this.setState(({ consoleStyle }) => ({
      consoleVisible: toOpen,
      consoleStyle: Object.assign({}, consoleStyle, { right: toOpen ? '0%' : '-40%' })
    }));
  }

  handleConsoleLog(logs) {
    this.setState({ consoleLogs: logs });
  }

  handleRunClick() {
    this.onTabSelect(1, null, null, () => {
      this.TestRunnerRef.handleNewCode();
    });
  }

  handlePaste(e) {
    const { disablePasting, onPaste } = this.props;

    if (disablePasting) {
      e.preventDefault();
    }
    onPaste(e);
  }

  handleLintErrors(lintErrLineNums) {
    this.setState({ lintErrLineNums });
  }

  renderEditorPanel() {
    const {
      initialCode,
      instructions,
      handleInstructionsChange,
      handleTestSpecChange,
      handleReset,
      showLints,
      testSpec,
      hideEditorButtons,
      headerText,
      subHeaderText,
    } = this.props;
    const { editMode } = this.state;
    return editMode ? (
      <EditModeTabs
        initialCode={initialCode}
        showLints={showLints}
        instructions={instructions}
        testSpec={testSpec}
        handleInstructionsChange={handleInstructionsChange}
        handleTestSpecChange={handleTestSpecChange}
        handleBoilerplateChange={this.updateCode}
        handleLintedBoilerplateCode={this.handleLintedCode}
        handleReset={handleReset}
      />
    ) : (
      <div>
        <CodeTesterHeader headerText={headerText} subHeaderText={subHeaderText} />
        <Editor
          code={initialCode}
          onValidCode={this.handleLintedCode}
          onAnyCode={this.updateCode}
          onLintErrors={this.handleLintErrors}
          onPaste={this.handlePaste}
          showLints={showLints}
          handleReset={handleReset}
          hideButtons={hideEditorButtons}
        />
      </div>
    );
  }

  renderRunButton(lintLineNumber) {
    return lintLineNumber.length ? (
      <button className="btn btn-lg btn-danger" disabled>Run</button>
    ) : (
      <button className="btn btn-lg btn-info" onClick={this.handleRunClick}>Run</button>
    );
  }

  render() {
    const {
      lintedCode,
      consoleStyle,
      selectedTabIndex,
      consoleVisible,
      consoleLogs,
      lintErrLineNums,
    } = this.state;
    const {
      onTestResults,
      testSpec,
      instructions,
      autoRun,
      iFrameUrl,
      testHandlerUrl,
      onError,
      infiniteLoopTimeout,
      submitEnabled,
      onTestRunnerSubmitClick,
      hideEditorButtons, // eslint-disable-line no-unused-vars
      markdownOptions,
      className,
      toggleEditMode,
      handleReset,
      editMode,
      handleUrlInput,
      handleGithubSubmit,
      committing,
    } = this.props;

    return (
      <div className={`CodeTester ${className}`}>
        <InstructionsPanel
          handleUrlInput={handleUrlInput}
          handleGithubSubmit={handleGithubSubmit}
          editMode={editMode}
          tabIndex={selectedTabIndex}
          onTabSelect={this.onTabSelect}
          instructions={instructions}
          markdownOptions={markdownOptions}
          testSpec={testSpec}
          lintedCode={lintedCode}
          onTestResults={onTestResults}
          handleConsoleLog={this.handleConsoleLog}
          autoRun={autoRun}
          iFrameUrl={iFrameUrl}
          testHandlerUrl={testHandlerUrl}
          onError={onError}
          infiniteLoopTimeout={infiniteLoopTimeout}
          submitEnabled={submitEnabled}
          onTestRunnerSubmitClick={onTestRunnerSubmitClick}
          toggleEditMode={toggleEditMode}
          handleReset={handleReset}
          onTestRunnerRef={ref => this.TestRunnerRef = ref}
          committing={committing}
        />
        <div className="right-code">
          <div className="console-panel">
            <Console
              style={consoleStyle}
              logs={consoleLogs}
              onToggleConsole={this.handleToggleConsole}
              show={consoleVisible}
              // eslint-disable-next-line
              onError={this.openConsole.bind(this, true)}
            />
          </div>
          <div className="right-panel-main">
            {this.renderEditorPanel()}
          </div>
          <div className="test-panel">
            {renderIf(lintErrLineNums.length)(<h5 className="syntax-message m-0">{`Syntax error: line ${_.first(lintErrLineNums)}`}</h5>)}
            {this.renderRunButton(lintErrLineNums)}
            <button className="btn btn-lg btn-success" onClick={onTestRunnerSubmitClick}>Submit</button>
          </div>
        </div>
      </div>
    );
  }
}

CodeTester.propTypes = {
  autoRun: PropTypes.bool,
  editMode: PropTypes.bool,
  toggleEditMode: PropTypes.func,
  handleUrlInput: PropTypes.func,
  handleGithubSubmit: PropTypes.func,
  handleBoilerplateChange: PropTypes.func,
  handleInstructionsChange: PropTypes.func,
  handleReset: PropTypes.func,
  handleTestSpecChange: PropTypes.func,
  infiniteLoopTimeout: PropTypes.number,
  initialCode: PropTypes.string,
  instructions: PropTypes.string,
  iFrameUrl: PropTypes.string.isRequired,
  onAnyCode: PropTypes.func,
  onError: PropTypes.func,
  onLintedCode: PropTypes.func,
  onTestResults: PropTypes.func,
  onTestRunnerSubmitClick: PropTypes.func,
  showConsole: PropTypes.bool,
  showLints: PropTypes.bool,
  submitEnabled: PropTypes.bool,
  testSpec: PropTypes.string.isRequired,
  testHandlerUrl: PropTypes.string,
  className: PropTypes.string,
  hideEditorButtons: PropTypes.bool,
  markdownOptions: PropTypes.objectOf(PropTypes.bool),
  headerText: PropTypes.string,
  subHeaderText: PropTypes.string,
  committing: PropTypes.bool,
  onPaste: PropTypes.func,
  disablePasting: PropTypes.bool,
};

CodeTester.defaultProps = {
  onTestResults: (passCount, failCount, testDuration) => {}, // eslint-disable-line no-unused-vars
  onLintedCode: code => {}, // eslint-disable-line no-unused-vars
  onAnyCode: code => {}, // eslint-disable-line no-unused-vars
  initialCode: '',
  instructions: '',
  autoRun: false,
  showLints: true,
  editMode: false,
  submitEnabled: true,
  onError: err => {
    const message = _.get(err, 'message') ? err.message : 'Unknown';
    console.error('Code Tester Error: ', message);
  },
  infiniteLoopTimeout: 3000,
  showConsole: false,
  hideEditorButtons: false,
  markdownOptions: {},
  className: '',
  testHandlerUrl: '',
  committing: false,
  headerText: '',
  subHeaderText: '',
  toggleEditMode: defaultFunc,
  handleUrlInput: defaultFunc,
  handleGithubSubmit: defaultFunc,
  handleBoilerplateChange: defaultFunc,
  handleInstructionsChange: defaultFunc,
  handleReset: defaultFunc,
  handleTestSpecChange: defaultFunc,
  onTestRunnerSubmitClick: defaultFunc,
  onPaste: defaultFunc,
  disablePasting: false,
};

export default CodeTester;
