import { Utils } from './Utils';
import { IHighlight } from '../rest/Highlight';
import { Assert } from '../misc/Assert';
import * as _ from 'underscore';
import { $$ } from './Dom';

export interface IStringHole {
  begin: number;
  size: number;
  replacementSize: number;
}

export class StringAndHoles {
  value: string;
  holes: IStringHole[];

  static SHORTEN_END: string = '...';
  static WORD_SHORTER: number = 10;

  static replace(str: string, find: string, replace: string): StringAndHoles {
    const strAndHoles = new StringAndHoles();

    if (Utils.isNullOrEmptyString(str)) {
      return strAndHoles;
    }

    let index = str.lastIndexOf(find);
    if (index == -1) {
      strAndHoles.value = str;
      return strAndHoles;
    }

    const holes: IStringHole[] = [];
    while (index >= 0) {
      const hole = <IStringHole>{
        begin: index,
        size: find.length,
        replacementSize: replace.length
      };
      holes.push(hole);
      str = str.slice(0, index) + replace + str.slice(index + find.length);
      index = str.lastIndexOf(find);
    }

    strAndHoles.holes = holes;
    strAndHoles.value = str;
    return strAndHoles;
  }

  /**
   * Shorten the passed path intelligently (path-aware).
   * Works with *local paths* and *network paths*
   * @param uriOrig The path to shorten
   * @param length The length to which the path will be shortened.
   */
  static shortenPath(uriOrig: string, length: number): StringAndHoles {
    const strAndHoles = new StringAndHoles();
    let uri = uriOrig;
    if (Utils.isNullOrEmptyString(uri) || uri.length <= length) {
      strAndHoles.value = uri;
      return strAndHoles;
    }

    const holes: IStringHole[] = [];

    let first = -1;
    if (Utils.stringStartsWith(uri, '\\\\')) {
      first = uri.indexOf('\\', first + 2);
    } else {
      first = uri.indexOf('\\');
    }

    if (first !== -1) {
      let removed = 0;
      let next = uri.indexOf('\\', first + 1);
      while (next !== -1 && uri.length - removed + StringAndHoles.SHORTEN_END.length > length) {
        removed = next - first - 1;
        next = uri.indexOf('\\', next + 1);
      }

      if (removed > 0) {
        uri = uri.slice(0, first + 1) + StringAndHoles.SHORTEN_END + uri.slice(removed);
        const hole = <IStringHole>{
          begin: first + 1,
          size: removed - StringAndHoles.SHORTEN_END.length,
          replacementSize: StringAndHoles.SHORTEN_END.length
        };
        holes.push(hole);
      }
    }

    if (uri.length > length) {
      const over = uri.length - length + StringAndHoles.SHORTEN_END.length;
      const start = uri.length - over;
      uri = uri.slice(0, start) + StringAndHoles.SHORTEN_END;
      const hole = <IStringHole>{
        begin: start,
        size: over,
        replacementSize: StringAndHoles.SHORTEN_END.length
      };
      holes.push(hole);
    }

    strAndHoles.holes = holes;
    strAndHoles.value = uri;
    return strAndHoles;
  }

  /**
   * Shorten the passed string.
   * @param toShortenOrig The string to shorten
   * @param length The length to which the string will be shortened.
   * @param toAppend The string to append at the end (usually, it is set to '...')
   */
  static shortenString(toShortenOrig: string, length: number = 200, toAppend?: string): StringAndHoles {
    const toShorten = toShortenOrig;
    toAppend = Utils.toNotNullString(toAppend);
    const strAndHoles = new StringAndHoles();
    if (Utils.isNullOrEmptyString(toShorten) || length <= toAppend.length) {
      strAndHoles.value = toShorten;
      return strAndHoles;
    }

    if (toShorten.length <= length) {
      strAndHoles.value = toShorten;
      return strAndHoles;
    }

    let str = toShorten;

    length = length - toAppend.length;
    str = str.slice(0, length);

    if (toShorten.charAt(str.length) !== ' ') {
      const pos = str.lastIndexOf(' ');
      if (pos !== -1 && str.length - pos < StringAndHoles.WORD_SHORTER) {
        str = str.slice(0, pos);
      }
    }
    const holes: IStringHole[] = [];
    holes[0] = <IStringHole>{
      begin: str.length,
      size: toShorten.length - str.length,
      replacementSize: toAppend.length
    };
    str += toAppend;
    strAndHoles.value = str;
    strAndHoles.holes = holes;
    return strAndHoles;
  }

  /**
   * Shorten the passed URI intelligently (path-aware).
   * @param toShortenOrig The URI to shorten
   * @param length The length to which the URI will be shortened.
   */
  static shortenUri(uri: string, length: number): StringAndHoles {
    const strAndHoles = new StringAndHoles();
    if (Utils.isNullOrEmptyString(uri) || uri.length <= length) {
      strAndHoles.value = uri;
      return strAndHoles;
    }

    const holes: IStringHole[] = [];

    let first = uri.indexOf('//');
    if (first !== -1) {
      first = uri.indexOf('/', first + 2);
    }

    if (first !== -1) {
      let removed = 0;
      let next = uri.indexOf('/', first + 1);
      while (next !== -1 && uri.length - removed + StringAndHoles.SHORTEN_END.length > length) {
        removed = next - first - 1;
        next = uri.indexOf('/', next + 1);
      }

      if (removed > 0) {
        uri = uri.slice(0, first + 1) + StringAndHoles.SHORTEN_END + uri.slice(first + 1 + removed);
        const hole = <IStringHole>{
          begin: first + 1,
          size: removed,
          replacementSize: StringAndHoles.SHORTEN_END.length
        };
        holes.push(hole);
      }
    }

    if (uri.length > length) {
      const over = uri.length - length + StringAndHoles.SHORTEN_END.length;
      const start = uri.length - over;
      uri = uri.slice(0, start) + StringAndHoles.SHORTEN_END;
      const hole = <IStringHole>{
        begin: start,
        size: over,
        replacementSize: StringAndHoles.SHORTEN_END.length
      };
      holes.push(hole);
    }

    strAndHoles.holes = holes;
    strAndHoles.value = uri;
    return strAndHoles;
  }
}

export class HighlightUtils {
  /**
   * Highlight the passed string using specified highlights and holes.
   * @param content The string to highlight items in.
   * @param highlights The highlighted positions to highlight in the string.
   * @param holes Possible holes which are used to skip highlighting.
   * @param cssClass The css class to use on the highlighting `span`.
   */
  static highlightString(content: string, highlights: IHighlight[], holes: IStringHole[], cssClass: string): string {
    Assert.isNotUndefined(highlights);
    Assert.isNotNull(highlights);
    Assert.isNonEmptyString(cssClass);
    if (Utils.isNullOrEmptyString(content)) {
      return content;
    }
    const maxIndex = content.length;
    let highlighted = '';
    let last = 0;
    for (let i = 0; i < highlights.length; i++) {
      const highlight = highlights[i];
      let start: number = highlight.offset;
      let end: number = start + highlight.length;

      if (holes !== null) {
        let skip = false;
        for (let j = 0; j < holes.length; j++) {
          const hole = holes[j];
          const holeBegin = hole.begin;
          const holeEnd = holeBegin + hole.size;
          if (start < holeBegin && end >= holeBegin && end < holeEnd) {
            end = holeBegin;
          } else if (start >= holeBegin && end < holeEnd) {
            skip = true;
            break;
          } else if (start >= holeBegin && start < holeEnd && end >= holeEnd) {
            start = holeBegin + hole.replacementSize;
            end -= hole.size - hole.replacementSize;
          } else if (start < holeBegin && end >= holeEnd) {
            end -= hole.size - hole.replacementSize;
          } else if (start >= holeEnd) {
            const offset = hole.size - hole.replacementSize;
            start -= offset;
            end -= offset;
          }
        }
        if (skip || start === end) {
          continue;
        }
      }

      if (end > maxIndex) {
        break;
      }

      if (last <= start) {
        highlighted += _.escape(content.slice(last, start));
        highlighted += `<span class="${cssClass}"`;
        if (highlight.dataHighlightGroup) {
          highlighted += ` data-highlight-group="${highlight.dataHighlightGroup.toString()}"`;
        }
        if (highlight.dataHighlightGroupTerm) {
          highlighted += ` data-highlight-group-term="${highlight.dataHighlightGroupTerm}"`;
        }
        highlighted += '>';
        highlighted += _.escape(content.slice(start, end));
        highlighted += '</span>';
      }
      last = end;
    }
    if (last != maxIndex) {
      highlighted += _.escape(content.slice(last));
    }
    return highlighted;
  }

  static highlight(text: string, match: string, className: string): HTMLElement[] {
    const elements: HTMLElement[] = [];
    const regex = RegExp(match, 'i');
    const parts = text.split(regex);
    const lastPart = parts.pop();

    let index = 0;

    parts.forEach(part => {
      if (part) {
        const unhighlighted = createSpanWithText(part);

        elements.push(unhighlighted.el);
        index += part.length;
      }

      const matchedSubstring = text.substring(index, index + match.length);
      const highlighted = createSpanWithText(matchedSubstring);
      highlighted.addClass(className);

      elements.push(highlighted.el);
      index += match.length;
    });

    if (lastPart) {
      const last = createSpanWithText(lastPart);
      elements.push(last.el);
    }

    return elements;
  }
}

function createSpanWithText(text: string) {
  const span = $$('span');
  span.text(text);
  return span;
}
