import { MonacoEditor, MonacoDiffEditor } from './libs';
import { React, css, Subject, events, GlamorValue } from '../../common';
import {
  CodeEditorChangeEventHandler,
  CodeEditorChangeEvent,
  CodeEditorOptions,
  ICodeEditor,
} from '../../types';
import * as config from '../../config';
import * as themes from '../../themes';

const DARK_BACKGROUND = themes.dark.colors['editor.background'];

export { CodeEditorChangeEventHandler, CodeEditorChangeEvent };

export type CodeEditorDiff = {
  code?: string;
  inline?: boolean; // Default (false, shows side-by-side).
};

export interface ICodeEditorProps {
  options?: Partial<CodeEditorOptions>;
  code?: string;
  diff?: CodeEditorDiff;
  focusOnLoad?: boolean;
  style?: GlamorValue;
  onChange?: CodeEditorChangeEventHandler;
}

export interface ICodeEditorState {
  width?: number;
  height?: number;
  isMounted: boolean;
}

/**
 * A code editor.
 */
export class CodeEditor extends React.PureComponent<
  ICodeEditorProps,
  ICodeEditorState
> {
  public state: ICodeEditorState = { isMounted: false };

  private editor: ICodeEditor;
  private unmounted$ = new Subject();

  private el: HTMLDivElement;
  private elRef = (ref: HTMLDivElement) => (this.el = ref);

  public componentDidMount() {
    this.setState({ isMounted: true });
    const resize$ = events.resize$.takeUntil(this.unmounted$);

    // Keep the editor size in sync with the component.
    this.updateSize();
    resize$.subscribe(e => this.updateSize());
  }

  public componentWillUnmount() {
    this.unmounted$.next();
  }

  public render() {
    const { code = '', diff } = this.props;
    const { isMounted, width, height } = this.state;
    const {
      selectOnLineNumbers,
      fontSize,
      fontFamily,
      theme,
      language,
    } = this.options;

    const styles = {
      base: css({
        backgroundColor: theme === 'DARK' ? DARK_BACKGROUND : undefined,
      }),
    };

    const options = {
      selectOnLineNumbers,
      fontSize,
      fontFamily,
      minimap: {
        enabled: false,
      },
    };

    const elEditor = isMounted &&
      !diff && (
        <MonacoEditor
          width={width}
          height={height}
          language={language}
          value={code}
          options={options}
          editorWillMount={this.handleBeforeMount}
          editorDidMount={this.handleAfterMount}
          onChange={this.handleChange}
        />
      );

    const elDiffEditor = isMounted &&
      diff && (
        <MonacoDiffEditor
          width={width}
          height={height}
          language={language}
          options={{
            ...options,
            renderSideBySide: !diff.inline,
          }}
          value={code}
          original={diff.code}
        />
      );

    return (
      <div
        ref={this.elRef}
        {...css(STYLES.BASE, styles.base, this.props.style)}
      >
        {elEditor}
        {elDiffEditor}
      </div>
    );
  }

  public get isFocused() {
    return this.editor && this.editor.isFocused();
  }

  public get width() {
    return this.state.width || -1;
  }

  public get height() {
    return this.state.height || -1;
  }

  public get size() {
    const width = this.width;
    const height = this.height;
    return { width, height };
  }

  /**
   * All options with default values.
   */
  private get options(): CodeEditorOptions {
    const options = this.props.options || {};
    const {
      selectOnLineNumbers = true,
      fontSize = 14,
      fontFamily,
      tabSize = 2,
      theme = 'DARK',
      language = 'typescript',
      javascriptDefaults,
      typescriptDefaults,
    } = options;

    return {
      selectOnLineNumbers,
      fontSize,
      fontFamily,
      tabSize,
      theme,
      language,
      javascriptDefaults,
      typescriptDefaults,
    };
  }

  public updateSize() {
    const width = this.el.offsetWidth;
    const height = this.el.offsetHeight;
    this.setState({ width, height });
  }

  public focus() {
    if (this.editor) {
      this.editor.focus();
    }
  }

  private handleBeforeMount = (monacoModule: typeof monaco) => {
    const { javascriptDefaults, typescriptDefaults } = this.options;
    const typescript = monacoModule.languages.typescript;

    if (javascriptDefaults) {
      config.configureLanguage(
        javascriptDefaults,
        typescript.javascriptDefaults,
      );
    }

    if (typescriptDefaults) {
      config.configureLanguage(
        typescriptDefaults,
        typescript.typescriptDefaults,
      );
    }
  };

  private handleAfterMount = (editor: ICodeEditor) => {
    const { tabSize, theme } = this.options;
    this.editor = editor;

    // Set initial focus.
    const focusOnLoad =
      this.props.focusOnLoad === undefined ? true : this.props.focusOnLoad;
    if (focusOnLoad) {
      this.focus();
    }

    // Perform configuration.
    config.configureTheme(theme);
    config.configureTabSize(editor, tabSize);
  };

  private handleChange = (
    code: string,
    e: monaco.editor.IModelContentChangedEvent,
  ) => {
    const { onChange } = this.props;
    if (onChange) {
      onChange({ code, ...e });
    }
  };
}

const STYLES = {
  BASE: css({
    flex: 1,
    position: 'relative',
    boxSizing: 'border-box',
  }),
};
