// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {
  IKernel, KernelMessage
} from 'jupyter-js-services';

import {
  defineSignal, ISignal
} from 'phosphor/lib/core/signaling';

import {
  Message
} from 'phosphor/lib/core/messaging';

import {
  PanelLayout
} from 'phosphor/lib/ui/panel';

import {
  Widget
} from 'phosphor/lib/ui/widget';

import {
  IChangedArgs
} from '../../common/interfaces';

import {
  RenderMime
} from '../../rendermime';

import {
  IMetadataCursor
} from '../common/metadata';

import {
  OutputAreaWidget
} from '../output-area';

import {
  ICellEditorWidget
} from './editor';

import {
  ICellModel, ICodeCellModel,
  IMarkdownCellModel, IRawCellModel
} from './model';


/**
 * The class name added to cell widgets.
 */
const CELL_CLASS = 'jp-Cell';

/**
 * The class name added to input area widgets.
 */
const INPUT_CLASS = 'jp-InputArea';

/**
 * The class name added to the prompt area of cell.
 */
const PROMPT_CLASS = 'jp-InputArea-prompt';

/**
 * The class name added to the editor area of the cell.
 */
const EDITOR_CLASS = 'jp-InputArea-editor';

/**
 * The class name added to the cell when collapsed.
 */
const COLLAPSED_CLASS = 'jp-mod-collapsed';

/**
 * The class name added to the cell when readonly.
 */
const READONLY_CLASS = 'jp-mod-readOnly';

/**
 * The class name added to code cells.
 */
const CODE_CELL_CLASS = 'jp-CodeCell';

/**
 * The class name added to markdown cells.
 */
const MARKDOWN_CELL_CLASS = 'jp-MarkdownCell';

/**
 * The class name added to the rendered markdown widget.
 */
const MARKDOWN_CONTENT_CLASS = 'jp-MarkdownCell-content';

/**
 * The class name added to raw cells.
 */
const RAW_CELL_CLASS = 'jp-RawCell';

/**
 * The class name added to a rendered markdown cell.
 */
const RENDERED_CLASS = 'jp-mod-rendered';

/**
 * The text applied to an empty markdown cell.
 */
const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $';


/**
 * A base cell widget.
 */
export
class BaseCellWidget extends Widget {
  /**
   * Construct a new base cell widget.
   */
  constructor(options: BaseCellWidget.IOptions) {
    super();
    this.addClass(CELL_CLASS);
    this.layout = new PanelLayout();

    let renderer = options.renderer;
    this._editor = renderer.createCellEditor();
    this._input = renderer.createInputArea(this._editor);

    (this.layout as PanelLayout).addWidget(this._input);
  }

  /**
   * A signal emitted when the model of the cell changes.
   */
  modelChanged: ISignal<BaseCellWidget, void>;

  /**
   * The model used by the widget.
   */
  get model(): ICellModel {
    return this._model;
  }
  set model(newValue: ICellModel) {
    if (!newValue && !this._model || newValue === this._model) {
      return;
    }

    let oldValue: ICellModel = this._model;
    this._model = newValue;
    // Trigger private, protected, and public updates.
    this._onModelChanged(oldValue, newValue);
    this.onModelChanged(oldValue, newValue);
    this.modelChanged.emit(void 0);
  }

  /**
   * Get the editor widget used by the cell.
   *
   * #### Notes
   * This is a ready-only property.
   */
  get editor(): ICellEditorWidget {
    return this._editor;
  }

  /**
   * The mimetype used by the cell.
   */
  get mimetype(): string {
    return this._mimetype;
  }
  set mimetype(value: string) {
    if (!value) {
      return;
    }
    if (this._mimetype === value) {
      return;
    }
    this._mimetype = value;
    this.editor.setMimeType(value);
  }

  /**
   * The read only state of the cell.
   */
  get readOnly(): boolean {
    return this._readOnly;
  }
  set readOnly(value: boolean) {
    if (value === this._readOnly) {
      return;
    }
    this._readOnly = value;
    this.update();
  }

  /**
   * The trusted state of the cell.
   */
  get trusted(): boolean {
    return this._trusted;
  }
  set trusted(value: boolean) {
    if (!this._model) {
      return;
    }
    this._trustedCursor.setValue(value);
    this._trusted = value;
  }

  /**
   * Set the prompt for the widget.
   */
  setPrompt(value: string): void {
    this._input.setPrompt(value);
  }

  /**
   * Toggle whether the input area is shown.
   */
  toggleInput(value: boolean): void {
    if (value) {
      this._input.show();
      this.activate();
    } else {
      this._input.hide();
    }
  }

  /**
   * Dispose of the resources held by the widget.
   */
  dispose() {
    // Do nothing if already disposed.
    if (this.isDisposed) {
      return;
    }
    this._model = null;
    this._input = null;
    this._editor = null;
    this._trustedCursor = null;
    super.dispose();
  }

  /**
   * Handle `after-attach` messages.
   */
  protected onAfterAttach(msg: Message): void {
    this.update();
  }

  /**
   * Handle `'activate-request'` messages.
   */
  protected onActivateRequest(msg: Message): void {
    this._editor.activate();
  }

  /**
   * Handle `update_request` messages.
   */
  protected onUpdateRequest(msg: Message): void {
    if (!this._model) {
      return;
    }
    // Handle read only state.
    this._editor.setReadOnly(this._readOnly);
    this.toggleClass(READONLY_CLASS, this._readOnly);
  }

  /**
   * Handle changes in the model.
   *
   * #### Notes
   * Subclasses may reimplement this method as needed.
   */
  protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
    // no-op
  }

  /**
   * Handle changes in the model.
   */
  protected onMetadataChanged(model: ICellModel, args: IChangedArgs<any>): void {
    switch (args.name) {
      case 'trusted':
        this._trusted = !!this._trustedCursor.getValue();
        this.update();
        break;
      default:
        break;
    }
  }

  /**
   * Handle a new model.
   *
   * #### Notes
   * This method is called after the model change has been handled
   * internally and before the `modelChanged` signal is emitted.
   * The default implementation is a no-op.
   */
  protected onModelChanged(oldValue: ICellModel, newValue: ICellModel): void {
    // no-op
  }

  private _onModelChanged(oldValue: ICellModel, newValue: ICellModel): void {
    // If the model is being replaced, disconnect the old signal handler.
    if (oldValue) {
      oldValue.stateChanged.disconnect(this.onModelStateChanged, this);
      oldValue.metadataChanged.disconnect(this.onMetadataChanged, this);
    }

    // Reset the editor model and set its mode to be the default MIME type.
    this._editor.model = this._model;
    this._editor.setMimeType(this._mimetype);

    // Handle trusted cursor.
    this._trustedCursor = this._model.getMetadata('trusted');
    this._trusted = !!this._trustedCursor.getValue();

    // Connect signal handlers.
    this._model.metadataChanged.connect(this.onMetadataChanged, this);
    this._model.stateChanged.connect(this.onModelStateChanged, this);
  }

  private _input: InputAreaWidget = null;
  private _editor: ICellEditorWidget = null;
  private _model: ICellModel = null;
  private _mimetype = 'text/plain';
  private _readOnly = false;
  private _trustedCursor: IMetadataCursor = null;
  private _trusted = false;
}


// Define the signals for the `BaseCellWidget` class.
defineSignal(BaseCellWidget.prototype, 'modelChanged');


/**
 * The namespace for the `BaseCellWidget` class statics.
 */
export
namespace BaseCellWidget {
  /**
   * An options object for initializing a base cell widget.
   */
  export
  interface IOptions {
    /**
     * A renderer for creating cell widgets.
     *
     * The default is a shared renderer instance.
     */
    renderer: IRenderer;
  }

  /**
   * A renderer for creating cell widgets.
   */
  export
  interface IRenderer {
    /**
     * Create a new cell editor for the widget.
     */
    createCellEditor(): ICellEditorWidget;

    /**
     * Create a new input area for the widget.
     */
    createInputArea(editor: ICellEditorWidget): InputAreaWidget;
  }

  /**
   * The default implementation of an `IRenderer`.
   */
  export
  abstract class Renderer implements IRenderer {
    /**
     * Create a new cell editor for the widget.
     */
    abstract createCellEditor(): ICellEditorWidget;

    /**
     * Create a new input area for the widget.
     */
    createInputArea(editor: ICellEditorWidget): InputAreaWidget {
      return new InputAreaWidget(editor);
    }
  }
}


/**
 * A widget for a code cell.
 */
export
class CodeCellWidget extends BaseCellWidget {
  /**
   * Construct a code cell widget.
   */
  constructor(options: CodeCellWidget.IOptions) {
    super(options);
    this.addClass(CODE_CELL_CLASS);
    this._rendermime = options.rendermime;
    this._renderer = options.renderer;
  }

  /**
   * The model used by the widget.
   */
  model: ICodeCellModel;

  /**
   * Dispose of the resources used by the widget.
   */
  dispose(): void {
    if (this.isDisposed) {
      return;
    }
    this._collapsedCursor = null;
    this._scrolledCursor = null;
    this._output = null;
    super.dispose();
  }

  /**
   * Execute the cell given a kernel.
   */
  execute(kernel: IKernel): Promise<KernelMessage.IExecuteReplyMsg> {
    let model = this.model as ICodeCellModel;
    let code = model.source;
    if (!code.trim()) {
      model.executionCount = null;
      model.outputs.clear();
      return Promise.resolve(null);
    }
    model.executionCount = null;
    this.setPrompt('*');
    this.trusted = true;
    let outputs = model.outputs;
    return outputs.execute(code, kernel).then(reply => {
      let status = reply.content.status;
      if (status === 'aborted') {
        model.executionCount = null;
        this.setPrompt(' ');
      } else {
        model.executionCount = reply.content.execution_count;
      }
      return reply;
    });
  }

  /**
   * Handle `update_request` messages.
   */
  protected onUpdateRequest(msg: Message): void {
    if (this._collapsedCursor) {
      this.toggleClass(COLLAPSED_CLASS, this._collapsedCursor.getValue());
    }
    if (this._output) {
      // TODO: handle scrolled state.
      this._output.trusted = this.trusted;
    }
    super.onUpdateRequest(msg);
  }

  /**
   * Handle the widget receiving a new model.
   */
  protected onModelChanged(oldValue: ICellModel, newValue: ICellModel): void {
    let model = newValue as ICodeCellModel;
    let renderer = this._renderer;

    if (!this._output) {
      this._output = renderer.createOutputArea(this._rendermime);
      (this.layout as PanelLayout).addWidget(this._output);
    }

    this._output.model = model.outputs;
    this._output.trusted = this.trusted;
    this._collapsedCursor = model.getMetadata('collapsed');
    this._scrolledCursor = model.getMetadata('scrolled');
    this.setPrompt(`${model.executionCount}`);
  }

  /**
   * Handle changes in the model.
   */
  protected onModelStateChanged(model: ICellModel, args: IChangedArgs<any>): void {
    switch (args.name) {
    case 'executionCount':
      this.setPrompt(`${(model as ICodeCellModel).executionCount}`);
      break;
    default:
      break;
    }
    super.onModelStateChanged(model, args);
  }

  /**
   * Handle changes in the metadata.
   */
  protected onMetadataChanged(model: ICellModel, args: IChangedArgs<any>): void {
    switch (args.name) {
    case 'collapsed':
    case 'scrolled':
      this.update();
      break;
    default:
      break;
    }
    super.onMetadataChanged(model, args);
  }

  private _renderer: CodeCellWidget.IRenderer;
  private _rendermime: RenderMime = null;
  private _output: OutputAreaWidget = null;
  private _collapsedCursor: IMetadataCursor = null;
  private _scrolledCursor: IMetadataCursor = null;
}


/**
 * The namespace for the `CodeCellWidget` class statics.
 */
export
namespace CodeCellWidget {
  /**
   * An options object for initializing a base cell widget.
   */
  export
  interface IOptions {
    /**
     * A renderer for creating cell widgets.
     *
     * The default is a shared renderer instance.
     */
    renderer: IRenderer;

    /**
     * The mime renderer for the cell widget.
     */
    rendermime: RenderMime;
  }

  /**
   * A renderer for creating code cell widgets.
   */
  export
  interface IRenderer extends BaseCellWidget.IRenderer {
    /**
     * Create a new output area for the widget.
     */
    createOutputArea(rendermime: RenderMime): OutputAreaWidget;
  }

  /**
   * The default implementation of an `IRenderer`.
   */
  export
  abstract class Renderer extends BaseCellWidget.Renderer implements IRenderer {
    /**
     * Create an output area widget.
     */
    createOutputArea(rendermime: RenderMime): OutputAreaWidget {
      return new OutputAreaWidget({ rendermime });
    }
  }
}


/**
 * A widget for a Markdown cell.
 *
 * #### Notes
 * Things get complicated if we want the rendered text to update
 * any time the text changes, the text editor model changes,
 * or the input area model changes.  We don't support automatically
 * updating the rendered text in all of these cases.
 */
export
class MarkdownCellWidget extends BaseCellWidget {
  /**
   * Construct a Markdown cell widget.
   */
  constructor(options: MarkdownCellWidget.IOptions) {
    super(options);
    this.addClass(MARKDOWN_CELL_CLASS);
    // Insist on the Github-flavored markdown mode.
    this.mimetype = 'text/x-ipythongfm';
    this._rendermime = options.rendermime;
    this._markdownWidget = new Widget();
    this._markdownWidget.addClass(MARKDOWN_CONTENT_CLASS);
    (this.layout as PanelLayout).addWidget(this._markdownWidget);
  }

  /**
   * The model used by the widget.
   */
  model: IMarkdownCellModel;

  /**
   * Whether the cell is rendered.
   */
  get rendered(): boolean {
    return this._rendered;
  }
  set rendered(value: boolean) {
    if (value === this._rendered) {
      return;
    }
    this._rendered = value;
    this.update();
  }

  /**
   * Dispose of the resource held by the widget.
   */
  dispose(): void {
    if (this.isDisposed) {
      return;
    }
    this._markdownWidget = null;
    super.dispose();
  }

  /**
   * Handle `update_request` messages.
   */
  protected onUpdateRequest(msg: Message): void {
    let model = this.model;
    if (this.rendered) {
      let text = model && model.source || DEFAULT_MARKDOWN_TEXT;
      // Do not re-render if the text has not changed.
      if (text !== this._prev) {
        let bundle: RenderMime.MimeMap<string> = { 'text/markdown': text };
        this._markdownWidget.dispose();
        let widget = this._rendermime.render(bundle, this.trusted);
        this._markdownWidget = widget || new Widget();
        this._markdownWidget.addClass(MARKDOWN_CONTENT_CLASS);
        (this.layout as PanelLayout).addWidget(this._markdownWidget);
      } else {
        this._markdownWidget.show();
      }
      this._prev = text;
      this.toggleInput(false);
      this.addClass(RENDERED_CLASS);
    } else {
      this._markdownWidget.hide();
      this.toggleInput(true);
      this.removeClass(RENDERED_CLASS);
    }
    super.onUpdateRequest(msg);
  }

  private _rendermime: RenderMime = null;
  private _markdownWidget: Widget = null;
  private _rendered = true;
  private _prev = '';
}


/**
 * The namespace for the `CodeCellWidget` class statics.
 */
export
namespace MarkdownCellWidget {
  /**
   * An options object for initializing a base cell widget.
   */
  export
  interface IOptions {
    /**
     * A renderer for creating cell widgets.
     *
     * The default is a shared renderer instance.
     */
    renderer: BaseCellWidget.IRenderer;

    /**
     * The mime renderer for the cell widget.
     */
    rendermime: RenderMime;
  }
}


/**
 * A widget for a raw cell.
 */
export
class RawCellWidget extends BaseCellWidget {
  /**
   * Construct a raw cell widget.
   */
  constructor(options: BaseCellWidget.IOptions) {
    super(options);
    this.addClass(RAW_CELL_CLASS);
  }

  /**
   * The model used by the widget.
   */
  model: IRawCellModel;
}


/**
 * An input area widget, which hosts a prompt and an editor widget.
 */
export
class InputAreaWidget extends Widget {
  /**
   * Construct an input area widget.
   */
  constructor(editor: ICellEditorWidget) {
    super();
    this.addClass(INPUT_CLASS);
    editor.addClass(EDITOR_CLASS);
    this.layout = new PanelLayout();
    let prompt = new Widget();
    prompt.addClass(PROMPT_CLASS);
    let layout = this.layout as PanelLayout;
    layout.addWidget(prompt);
    layout.addWidget(editor);
  }

  /**
   * Set the prompt of the input area.
   */
  setPrompt(value: string): void {
    let prompt = (this.layout as PanelLayout).widgets.at(0);
    if (value === 'null') {
      value = ' ';
    }
    let text = `In [${value || ' '}]:`;
    prompt.node.textContent = text;
  }
}
