///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed 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 'ace-builds';
import { Observable } from 'rxjs/internal/Observable';
import { forkJoin, from, of } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';

let aceDependenciesLoaded = false;
let aceModule: any;

function loadAceDependencies(): Observable<any> {
  if (aceDependenciesLoaded) {
    return of(null);
  } else {
    const aceObservables: Observable<any>[] = [];
    aceObservables.push(from(import('ace-builds/src-noconflict/ext-language_tools')));
    aceObservables.push(from(import('ace-builds/src-noconflict/ext-searchbox')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-java')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-css')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-json')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-javascript')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-c_cpp')));
    aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/javascript')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/c_cpp')));
    aceObservables.push(from(import('ace-builds/src-noconflict/snippets/protobuf')));
    aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate')));
    aceObservables.push(from(import('ace-builds/src-noconflict/theme-github')));
    return forkJoin(aceObservables).pipe(
      tap(() => {
        aceDependenciesLoaded = true;
      })
    );
  }
}

export function getAce(): Observable<any> {
  if (aceModule) {
    return of(aceModule);
  } else {
    return from(import('ace')).pipe(
      mergeMap((module) => {
        return loadAceDependencies().pipe(
         map(() => module)
        );
      }),
      tap((module) => {
        aceModule = module;
      })
    );
  }
}

export class Range implements Ace.Range {

  public start: Ace.Point;
  public end: Ace.Point;

  constructor(startRow: number, startColumn: number, endRow: number, endColumn: number) {
    this.start = {
      row: startRow,
      column: startColumn
    };

    this.end = {
      row: endRow,
      column: endColumn
    };
  }

  static fromPoints(start: Ace.Point, end: Ace.Point): Ace.Range {
    return new Range(start.row, start.column, end.row, end.column);
  }

  clipRows(firstRow: number, lastRow: number): Ace.Range {
    let end: Ace.Point;
    let start: Ace.Point;
    if (this.end.row > lastRow) {
      end = {row: lastRow + 1, column: 0};
    } else if (this.end.row < firstRow) {
      end = {row: firstRow, column: 0};
    }

    if (this.start.row > lastRow) {
      start = {row: lastRow + 1, column: 0};
    } else if (this.start.row < firstRow) {
      start = {row: firstRow, column: 0};
    }
    return Range.fromPoints(start || this.start, end || this.end);
  }

  clone(): Ace.Range {
    return Range.fromPoints(this.start, this.end);
  }

  collapseRows(): Ace.Range {
    if (this.end.column === 0) {
      return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row - 1), 0);
    } else {
      return new Range(this.start.row, 0, this.end.row, 0);
    }
  }

  compare(row: number, column: number): number {
    if (!this.isMultiLine()) {
      if (row === this.start.row) {
        return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0);
      }
    }

    if (row < this.start.row) {
      return -1;
    }

    if (row > this.end.row) {
      return 1;
    }

    if (this.start.row === row) {
      return column >= this.start.column ? 0 : -1;
    }

    if (this.end.row === row) {
      return column <= this.end.column ? 0 : 1;
    }

    return 0;
  }

  compareEnd(row: number, column: number): number {
    if (this.end.row === row && this.end.column === column) {
      return 1;
    } else {
      return this.compare(row, column);
    }
  }

  compareInside(row: number, column: number): number {
    if (this.end.row === row && this.end.column === column) {
      return 1;
    } else if (this.start.row === row && this.start.column === column) {
      return -1;
    } else {
      return this.compare(row, column);
    }
  }

  comparePoint(p: Ace.Point): number {
    return this.compare(p.row, p.column);
  }

  compareRange(range: Ace.Range): number {
    let cmp: number;
    const end = range.end;
    const start = range.start;

    cmp = this.compare(end.row, end.column);
    if (cmp === 1) {
      cmp = this.compare(start.row, start.column);
      if (cmp === 1) {
        return 2;
      } else if (cmp === 0) {
        return 1;
      } else {
        return 0;
      }
    } else if (cmp === -1) {
      return -2;
    } else {
      cmp = this.compare(start.row, start.column);
      if (cmp === -1) {
        return -1;
      } else if (cmp === 1) {
        return 42;
      } else {
        return 0;
      }
    }
  }

  compareStart(row: number, column: number): number {
    if (this.start.row === row && this.start.column === column) {
      return -1;
    } else {
      return this.compare(row, column);
    }
  }

  contains(row: number, column: number): boolean {
    return this.compare(row, column) === 0;
  }

  containsRange(range: Ace.Range): boolean {
    return this.comparePoint(range.start) === 0 && this.comparePoint(range.end) === 0;
  }

  extend(row: number, column: number): Ace.Range {
    const cmp = this.compare(row, column);
    let end: Ace.Point;
    let start: Ace.Point;
    if (cmp === 0) {
      return this;
    } else if (cmp === -1) {
      start = {row, column};
    } else {
      end = {row, column};
    }
    return Range.fromPoints(start || this.start, end || this.end);
  }

  inside(row: number, column: number): boolean {
    if (this.compare(row, column) === 0) {
      if (this.isEnd(row, column) || this.isStart(row, column)) {
        return false;
      } else {
        return true;
      }
    }
    return false;
  }

  insideEnd(row: number, column: number): boolean {
    if (this.compare(row, column) === 0) {
      if (this.isStart(row, column)) {
        return false;
      } else {
        return true;
      }
    }
    return false;
  }

  insideStart(row: number, column: number): boolean {
    if (this.compare(row, column) === 0) {
      if (this.isEnd(row, column)) {
        return false;
      } else {
        return true;
      }
    }
    return false;
  }

  intersects(range: Ace.Range): boolean {
    const cmp = this.compareRange(range);
    return (cmp === -1 || cmp === 0 || cmp === 1);
  }

  isEmpty(): boolean {
    return (this.start.row === this.end.row && this.start.column === this.end.column);
  }

  isEnd(row: number, column: number): boolean {
    return this.end.row === row && this.end.column === column;
  }

  isEqual(range: Ace.Range): boolean {
    return this.start.row === range.start.row &&
      this.end.row === range.end.row &&
      this.start.column === range.start.column &&
      this.end.column === range.end.column;
  }

  isMultiLine(): boolean {
    return (this.start.row !== this.end.row);
  }

  isStart(row: number, column: number): boolean {
    return this.start.row === row && this.start.column === column;
  }

  moveBy(row: number, column: number): void {
    this.start.row += row;
    this.start.column += column;
    this.end.row += row;
    this.end.column += column;
  }

  setEnd(row: number, column: number): void {
    if (typeof row === 'object') {
      this.end.column = (row as Ace.Point).column;
      this.end.row = (row as Ace.Point).row;
    } else {
      this.end.row = row;
      this.end.column = column;
    }
  }

  setStart(row: number, column: number): void {
    if (typeof row === 'object') {
      this.start.column = (row as Ace.Point).column;
      this.start.row = (row as Ace.Point).row;
    } else {
      this.start.row = row;
      this.start.column = column;
    }
  }

  toScreenRange(session: Ace.EditSession): Ace.Range {
    const screenPosStart = session.documentToScreenPosition(this.start);
    const screenPosEnd = session.documentToScreenPosition(this.end);

    return new Range(
      screenPosStart.row, screenPosStart.column,
      screenPosEnd.row, screenPosEnd.column
    );
  }

}
