/**
 * The code block component range class.
 */
export class CodeBlockRange {
  /**
   * Initial the code block range.
   *
   * @param {HTMLElement} codeContainerRef - The code content container ref.
   */
  constructor(codeContainerRef) {
    // The cursor position.
    this.contentOffset = 0;
    // The range selected text, the value will be empty if the range is collapsed.
    this.selectedText = '';
    this.codeContainerRef = codeContainerRef;
  }
  /**
   * Update the code block range when the focus node changed or focus node content changed.
   *
   * @param {Node} startContainer - The focus node instance.
   * @param {number} startOffset - The focus offset.
   * @param {CodeNodeType[]} currentNodes - The current code node array.
   * @param {string} [selectedText] - The range selected text.
   * @returns {CodeBlockRange} - The new code block range instance.
   */
  updateRange(startContainer, startOffset, currentNodes, selectedText = '') {
    const range = new CodeBlockRange(this.codeContainerRef);
    range.selectedText = selectedText;
    range.contentOffset = range.getContentOffset(startContainer, startOffset, currentNodes);
    return range;
  }
  /**
   * Update the cursor position when insert text or delete text...
   *
   * @param {number} newContentOffset - New value for content offset.
   * @returns {number} - The new content offset.
   */
  updateContentOffset(newContentOffset) {
    this.contentOffset = Math.max(0, newContentOffset);
    this.selectedText = '';
    return this.contentOffset;
  }
  /**
   * Create a native range from the current code block range.
   *
   * @param {CodeNodeType[]} nextCodeNodes - The code block node array.
   * @returns {Nullable<Range>} - The range instance.
   */
  createNativeRange(nextCodeNodes) {
    const { nodeIndex, offset } = this.getFocusNodeState(nextCodeNodes, this.contentOffset);
    const range = new Range();
    const focusNode = this.codeContainerRef.querySelector(`span[data-index="${nodeIndex}"]`);
    if (focusNode) {
      const startOffset = Math.min(focusNode.firstChild.textContent.length, offset);
      range.setStart(focusNode.firstChild, startOffset);
      range.setEnd(focusNode.firstChild, startOffset);
      return range;
    }
  }
  /**
   * Calculate the cursor position.
   *
   * @param {Node} startContainer - The cursor node.
   * @param {number} startOffset - The cursor offset.
   * @param {CodeNodeType[]} currentNodes - The code block node array.
   * @returns {number} - A number value.
   */
  getContentOffset(startContainer, startOffset, currentNodes) {
    let startSpan = startContainer.nodeType === Node.TEXT_NODE
      ? startContainer.parentElement
      : startContainer;
    if (startSpan.nodeName === 'CODE' && startSpan.childNodes && startSpan.childNodes.length) {
      startSpan = startSpan.childNodes[Math.min(startOffset, startSpan.childNodes.length - 1)];
      startOffset = 0;
    }
    if (startSpan && startSpan.hasAttribute('data-index')) {
      const index = Number(startSpan.dataset.index);
      let beforeText = '';
      for (let i = 0; i < index; i++) {
        beforeText += currentNodes[i].text;
      }
      return beforeText.length + startOffset;
    }
    return 0;
  }
  /**
   * Get the next focus node of the code block node array.
   *
   * @param {CodeNodeType[]} codeNodes - The current code block array.
   * @param {number} contentOffset - The cursor position.
   * @returns {FocusNodeStateType} - The object contains next focus node index and offset.
   */
  getFocusNodeState(codeNodes, contentOffset) {
    const content = this.getCodeContent(codeNodes);
    const result = { nodeIndex: 0, offset: 0 };
    if (!content || contentOffset >= content.length) {
      result.nodeIndex = codeNodes.length - 1;
      result.offset = codeNodes[result.nodeIndex].text.length;
    }
    else {
      let text = '';
      for (let i = 0; i < codeNodes.length; i++) {
        text += codeNodes[i].text;
        if (text.length >= contentOffset) {
          result.offset = Math.max(0, codeNodes[i].text.length - (text.length - contentOffset));
          result.nodeIndex = i;
          break;
        }
      }
    }
    // The cursor position is incorrect in firefox when cursor node is line break node.
    if (codeNodes[result.nodeIndex].text === '\n' &&
      result.offset > 0 &&
      codeNodes[result.nodeIndex + 1]) {
      result.nodeIndex += 1;
      result.offset = 0;
    }
    return result;
  }
  /**
   * Get all code content from the code block node array.
   *
   * @param {CodeNodeType[]} codeNodes - The code block node array.
   * @returns {string} - The code content.
   */
  getCodeContent(codeNodes) {
    return codeNodes.map((codeNode) => codeNode.text).join('');
  }
}
