/**
 * @license
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.
 */

// Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;

export interface NormalizedRange {
  endContainer: Node;
  endOffset: number;
  startContainer: Node;
  startOffset: number;
}

/**
 * Remap DOM range to whole lines of a diff if necessary. If the start or
 * end containers are DOM elements that are singular pieces of syntax
 * highlighting, the containers are remapped to the .contentText divs that
 * contain the entire line of code.
 *
 * @param range - the standard DOM selector range.
 * @return A modified version of the range that correctly accounts
 *     for syntax highlighting.
 */
export function normalize(range: Range): NormalizedRange {
  const startContainer = _getContentTextParent(range.startContainer);
  const startOffset =
    range.startOffset + _getTextOffset(startContainer, range.startContainer);
  const endContainer = _getContentTextParent(range.endContainer);
  const endOffset =
    range.endOffset + _getTextOffset(endContainer, range.endContainer);
  return {
    startContainer,
    startOffset,
    endContainer,
    endOffset,
  };
}

function _getContentTextParent(target: Node): Node {
  if (!target.parentElement) return target;

  let element: Element | null;
  if (target instanceof Element) {
    element = target;
  } else {
    element = target.parentElement;
  }

  while (element && !element.classList.contains('contentText')) {
    if (element.parentElement === null) {
      return target;
    }
    element = element.parentElement;
  }
  return element ? element : target;
}

/**
 * Gets the character offset of the child within the parent.
 * Performs a synchronous in-order traversal from top to bottom of the node
 * element, counting the length of the syntax until child is found.
 *
 * @param node The root DOM element to be searched through.
 * @param child The child element being searched for.
 */
// TODO(TS): Only export for test.
export function _getTextOffset(node: Node | null, child: Node): number {
  let count = 0;
  let stack = [node];
  while (stack.length) {
    const n = stack.pop();
    if (n === child) {
      break;
    }
    if (n?.childNodes && n.childNodes.length !== 0) {
      const arr = [];
      for (const childNode of n.childNodes) {
        arr.push(childNode);
      }
      arr.reverse();
      stack = stack.concat(arr);
    } else {
      count += _getLength(n);
    }
  }
  return count;
}

/**
 * The DOM API textContent.length calculation is broken when the text
 * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
 *
 * @param node A text node.
 * @return The length of the text.
 */
function _getLength(node?: Node | null) {
  return node && node.textContent
    ? node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length
    : 0;
}
