import { generateUUID } from '../../../../../../utils/misc';
import { RTETextBlock } from '../../../../dto/blocks/text-block';
import { RTETextNode } from '../../../../dto/nodes/text-node';
import htmlElementToAnnotationGenerator from './annotation-handler/html-element-to-annotation-generator';
import { BlockToRTEBlockStrategyAbstract } from './block-to-rte-block-strategy.abstract';
import { ALWAYS_INLINE_TAGS, DEDICATED_STRATEGY_TAGS, PHRASING_CONTENT_TAGS, isInBlockTextContext, } from './strategy-tags';
/** Transform continue inline block elements to text block DTO */
export class ContinueInlineToRTETextBlockStrategy extends BlockToRTEBlockStrategyAbstract {
  /**
   * @inheritDoc
   */
  canHandle(elementsArray, currentIndex, options) {
    let count = 0;
    for (let i = currentIndex; i < elementsArray.length; i++) {
      const element = elementsArray[i];
      if (this.isInlineCandidate(element, options)) {
        count++;
      }
      else {
        break;
      }
    }
    return count;
  }
  /**
   * Parse to a text block.
   *
   * @param {HTMLElement[]} elementsArray - Current elements.
   * @param {VegaRTETransformOptions} [options] - Options for transformation.
   * @returns {Nullable<RTEContentBlock>} - Text block.
   */
  handle(elementsArray, options) {
    const parent = elementsArray[0].parentElement;
    const autoMatchFormat = options && options.autoMatchFormat;
    const nodes = this.generateChildNodes(elementsArray, autoMatchFormat
      ? htmlElementToAnnotationGenerator.generate(RTETextNode, parent, Object.assign(Object.assign({}, options), { skipCustomAnnotations: true }))
      : {}, options);
    const blockType = autoMatchFormat
      ? super.matchTextBlockType(parent)
      : this.matchTextBlockTypeByTagName(parent);
    const internalWrapper = !autoMatchFormat && !blockType;
    return RTETextBlock.from(Object.assign({ id: generateUUID(), type: blockType || 'paragraph', nodes: nodes }, (internalWrapper
      ? { annotations: { internalWrapper: internalWrapper } }
      : {
        annotations: htmlElementToAnnotationGenerator.generate(RTETextBlock, parent, options),
      })), options);
  }
  /* istanbul ignore next */
  /**
   * Append child blocks.
   */
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  appendChildBlocks() { }
  /**
   * Should continue process to child elements.
   *
   * @returns {boolean} - boolean.
   */
  shouldProceedToElementChildren() {
    return false;
  }
  /**
   * Match the block type by tag name.
   *
   * @param {HTMLElement} element - Target element.
   * @returns {Nullable<VegaRTETextBlockType>} - Block type.
   */
  matchTextBlockTypeByTagName(element) {
    switch (element.nodeName) {
      case 'P':
        return 'paragraph';
      default:
        return null;
    }
  }
  /**
   * Check if the element is an inline candidate to be included in the text block.
   *
   * @param {HTMLElement} element - The element to check.
   * @param {VegaRTETransformOptions} [options] - Options for transformation.
   * @returns {boolean} - Whether the element is an inline candidate.
   */
  isInlineCandidate(element, options) {
    if (element.nodeType === Node.TEXT_NODE)
      return true;
    if (ALWAYS_INLINE_TAGS.has(element.nodeName))
      return true;
    if (this.isDedicatedBlockTag(element.nodeName))
      return false;
    if (options.autoMatchFormat === false && isInBlockTextContext(element))
      return true;
    return this.isInlineLeafOrWrapper(element);
  }
  /**
   * Returns true when the tag has a dedicated block strategy (excludes inline annotation tags CODE and A).
   *
   * @param {string} nodeName - The node name to check.
   * @returns {boolean} - Whether the tag is a dedicated block tag.
   */
  isDedicatedBlockTag(nodeName) {
    return DEDICATED_STRATEGY_TAGS.has(nodeName) && !ALWAYS_INLINE_TAGS.has(nodeName);
  }
  /**
   * Returns true for empty elements, text-only elements, or elements whose entire
   * descendant tree contains no block-level elements (e.g. {@code <u><i><b>text</b></i></u>}).
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} - Whether the element is an inline leaf or wrapper.
   */
  isInlineLeafOrWrapper(element) {
    if (element.childNodes.length === 0) {
      // An empty element is inline only if it is a known HTML phrasing-content
      // element (SPAN, EM, STRONG, …). Block containers (DIV, SECTION, …)
      // and unknown/custom elements default to block separators.
      return PHRASING_CONTENT_TAGS.has(element.nodeName);
    }
    if (this.isTextOnlyElement(element)) {
      return PHRASING_CONTENT_TAGS.has(element.nodeName) || ALWAYS_INLINE_TAGS.has(element.nodeName);
    }
    return this.hasOnlyInlineDescendants(element);
  }
  /**
   * Returns true when the element has text-node children but no element children.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} - Whether the element contains only text nodes.
   */
  isTextOnlyElement(element) {
    const childNodes = Array.from(element.childNodes);
    return (!childNodes.some((child) => child.nodeType === Node.ELEMENT_NODE) &&
      childNodes.some((child) => child.nodeType === Node.TEXT_NODE));
  }
  /**
   * Returns true if the element's subtree contains only inline formatting wrappers.
   * Instead of maintaining a hard coded tag allowlist, the check derives from:
   *  1. {@link DEDICATED_STRATEGY_TAGS} — rejects known block/media tags
   *  2. Void-element check — elements with no child nodes (INPUT, COL, HR, …)
   *     are replaced/embedded content, not formatting wrappers
   *
   * @param {HTMLElement} element - Element to inspect.
   * @returns {boolean} True if the element has no block-level descendants.
   */
  hasOnlyInlineDescendants(element) {
    for (let i = 0; i < element.children.length; i++) {
      const child = element.children[i];
      if (this.isDedicatedBlockTag(child.nodeName))
        return false;
      // An empty element is not a formatting wrapper — it is likely a void/embedded
      // element (INPUT, HR, COL, empty SPAN) that acts as a block separator.
      if (!child.hasChildNodes())
        return false;
      if (!this.hasOnlyInlineDescendants(child))
        return false;
    }
    return true;
  }
}
