// Licensed to Cloudera, Inc. under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  Cloudera, Inc. licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import ace from '../../../../ext/aceHelper';
import { Ace } from '../../../../ext/ace';

import { Disposable } from '../../../../components/utils/SubscriptionTracker';
import { ParsedLocation } from '../../../../parse/types';

const removeGutterDecoration = (
  cssClass: string,
  session: Ace.Session,
  startRow: number,
  endRow: number
): void => {
  for (let row = startRow; row <= endRow; row++) {
    session.removeGutterDecoration(row, cssClass);
  }
};

const addGutterDecoration = (
  cssClass: string,
  session: Ace.Session,
  startRow: number,
  endRow: number
): void => {
  for (let row = startRow; row <= endRow; row++) {
    session.addGutterDecoration(row, cssClass);
  }
};

export default class AceAnchoredRange implements Disposable {
  editor: Ace.Editor;
  gutterStart: Ace.Anchor;
  gutterEnd: Ace.Anchor;
  rowStart: Ace.Anchor;
  rowEnd: Ace.Anchor;

  changed = false;
  rowMarkerSpec?: { cssClass: string; rowOffset: number; marker: number };
  gutterSpec?: { cssClass: string; span: { start: number; end: number } };
  refreshThrottle = -1;

  constructor(editor: Ace.Editor) {
    this.editor = editor;
    const doc = this.editor.getSession().doc;
    this.gutterStart = doc.createAnchor(0, 0);
    this.gutterEnd = doc.createAnchor(0, 0);
    this.rowStart = doc.createAnchor(0, 0);
    this.rowEnd = doc.createAnchor(0, 0);

    this.attachChangeHandler();
  }

  attachChangeHandler(): void {
    const throttledRefresh = () => {
      window.clearTimeout(this.refreshThrottle);
      this.refreshThrottle = window.setTimeout(this.refresh.bind(this), 10);
    };

    this.gutterStart.on('change', throttledRefresh);
    this.gutterEnd.on('change', throttledRefresh);
    this.rowStart.on('change', throttledRefresh);
    this.rowEnd.on('change', throttledRefresh);
  }

  refresh(): void {
    const session = this.editor.getSession();
    const newStart = this.gutterStart.getPosition();
    const newEnd = this.gutterEnd.getPosition();
    if (this.gutterSpec) {
      const rowSpan = this.gutterSpec.span;
      removeGutterDecoration(this.gutterSpec.cssClass, session, rowSpan.start, rowSpan.end);
      rowSpan.start = newStart.row;
      rowSpan.end = newEnd.row;
      addGutterDecoration(this.gutterSpec.cssClass, session, rowSpan.start, rowSpan.end);
    }
    if (this.rowMarkerSpec) {
      const offset = this.rowMarkerSpec.rowOffset;
      const cssClass = this.rowMarkerSpec.cssClass;
      this.removeMarkerRowCss();
      this.setMarkerRowCss(cssClass, offset);
    }
  }

  move(parseLocation: ParsedLocation, leadingEmptyLineCount?: number): void {
    const lastRow = parseLocation.last_line - 1;
    const firstRow = Math.min(lastRow, parseLocation.first_line - 1 + (leadingEmptyLineCount || 0));
    const firstCol = leadingEmptyLineCount ? 0 : parseLocation.first_column;
    this.gutterStart.setPosition(firstRow, firstCol);
    this.gutterEnd.setPosition(lastRow, parseLocation.last_column);
    if (this.rowMarkerSpec) {
      this.refreshRowAnchors(this.rowMarkerSpec.rowOffset);
    }
  }

  setGutterCss(cssClass: string): void {
    if (this.gutterSpec) {
      this.removeGutterCss();
    }
    const session = this.editor.getSession();
    const startRow = this.gutterStart.getPosition().row;
    const endRow = this.gutterEnd.getPosition().row;
    this.gutterSpec = { cssClass, span: { start: startRow, end: endRow } };
    addGutterDecoration(cssClass, session, startRow, endRow);
  }

  refreshRowAnchors(rowOffset: number): void {
    const markerRow = this.gutterStart.row + rowOffset;
    this.rowStart.setPosition(markerRow, 0);
    this.rowEnd.setPosition(markerRow, this.editor.getSession().getLine(markerRow).length);
  }

  setMarkerRowCss(cssClass: string, rowOffset: number): void {
    if (this.rowMarkerSpec) {
      this.removeMarkerRowCss();
    }
    this.refreshRowAnchors(rowOffset);
    const AceRange = ace.require('ace/range').Range;
    const range: Ace.Range = new AceRange(0, 0, 0, 0);
    range.start = this.rowStart;
    range.end = this.rowEnd;
    const marker = this.editor.getSession().addMarker(range, cssClass);
    this.rowMarkerSpec = { cssClass, rowOffset, marker };
    this.rowMarkerSpec.marker = marker;
  }

  removeMarkerRowCss(): void {
    if (this.rowMarkerSpec) {
      this.editor.getSession().removeMarker(this.rowMarkerSpec.marker);
      this.rowMarkerSpec = undefined;
    }
  }

  removeGutterCss(): void {
    if (this.gutterSpec) {
      const session = this.editor.getSession();
      const rowSpan = this.gutterSpec.span;
      removeGutterDecoration(this.gutterSpec.cssClass, session, rowSpan.start, rowSpan.end);
      this.gutterSpec = undefined;
    }
  }

  dispose(): void {
    window.clearTimeout(this.refreshThrottle);
    this.gutterStart.detach();
    this.gutterEnd.detach();
    this.rowStart.detach();
    this.rowEnd.detach();

    this.removeGutterCss();
    this.removeMarkerRowCss();
  }
}
