import type { Element, ElementContent, Text } from 'hast';

import type { HtmlAST } from '../types/hast.js';

const isElement = (ast: HtmlAST): ast is Element => {
  return ast.type === 'element';
};

const getTextContent = (ast: HtmlAST | undefined, defaultStr = ''): string => {
  if (!ast) {
    return defaultStr;
  }
  switch (ast.type) {
    case 'text': {
      return ast.value.replace(/\s+/g, ' ');
    }
    case 'element': {
      switch (ast.tagName) {
        case 'br': {
          return '\n';
        }
      }
      return ast.children.map(child => getTextContent(child)).join('');
    }
  }
  return defaultStr;
};

const getElementChildren = (ast: HtmlAST | undefined): Element[] => {
  if (!ast) {
    return [];
  }
  if (ast.type === 'element') {
    return ast.children.filter(child => child.type === 'element') as Element[];
  }
  return [];
};

const getTextChildren = (ast: HtmlAST | undefined): Text[] => {
  if (!ast) {
    return [];
  }
  if (ast.type === 'element') {
    return ast.children.filter(child => child.type === 'text') as Text[];
  }
  return [];
};

const getTextChildrenOnlyAst = (ast: Element): Element => {
  return {
    ...ast,
    children: getTextChildren(ast),
  };
};

const isTagInline = (tagName: string): boolean => {
  // Phrasing content
  const inlineElements = [
    'a',
    'abbr',
    'audio',
    'b',
    'bdi',
    'bdo',
    'br',
    'button',
    'canvas',
    'cite',
    'code',
    'data',
    'datalist',
    'del',
    'dfn',
    'em',
    'embed',
    'i',
    // 'iframe' is not included because it needs special handling
    // 'img' is not included because it needs special handling
    'input',
    'ins',
    'kbd',
    'label',
    'link',
    'map',
    'mark',
    'math',
    'meta',
    'meter',
    'noscript',
    'object',
    'output',
    'picture',
    'progress',
    'q',
    'ruby',
    's',
    'samp',
    'script',
    'select',
    'slot',
    'small',
    'span',
    'strong',
    'sub',
    'sup',
    'svg',
    'template',
    'textarea',
    'time',
    'u',
    'var',
    'video',
    'wbr',
  ];
  return inlineElements.includes(tagName);
};

const isElementInline = (element: Element): boolean => {
  return (
    isTagInline(element.tagName) ||
    // Inline elements
    !!(
      typeof element.properties?.style === 'string' &&
      element.properties.style.match(/display:\s*inline/)
    )
  );
};

const getInlineElementsAndText = (ast: Element): (Element | Text)[] => {
  if (!ast || !ast.children) {
    return [];
  }

  return ast.children.filter((child): child is Element | Text => {
    if (child.type === 'text') {
      return true;
    }
    if (child.type === 'element' && child.tagName && isElementInline(child)) {
      return true;
    }
    return false;
  });
};

const getInlineOnlyElementAST = (ast: Element): Element => {
  return {
    ...ast,
    children: getInlineElementsAndText(ast),
  };
};

const querySelectorTag = (
  ast: HtmlAST,
  tagName: string
): Element | undefined => {
  if (ast.type === 'element') {
    if (ast.tagName === tagName) {
      return ast;
    }
    for (const child of ast.children) {
      const result = querySelectorTag(child, tagName);
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};

const querySelectorClass = (
  ast: HtmlAST,
  className: string
): Element | undefined => {
  if (ast.type === 'element') {
    if (
      Array.isArray(ast.properties?.className) &&
      ast.properties.className.includes(className)
    ) {
      return ast;
    }
    for (const child of ast.children) {
      const result = querySelectorClass(child, className);
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};

const querySelectorId = (ast: HtmlAST, id: string): Element | undefined => {
  if (ast.type === 'element') {
    if (ast.properties.id === id) {
      return ast;
    }
    for (const child of ast.children) {
      const result = querySelectorId(child, id);
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};

const querySelector = (ast: HtmlAST, selector: string): Element | undefined => {
  if (ast.type === 'root') {
    for (const child of ast.children) {
      const result = querySelector(child, selector);
      if (result) {
        return result;
      }
    }
  } else if (ast.type === 'element') {
    if (selector.startsWith('.')) {
      return querySelectorClass(ast, selector.slice(1));
    } else if (selector.startsWith('#')) {
      return querySelectorId(ast, selector.slice(1));
    } else {
      return querySelectorTag(ast, selector);
    }
  }
  return undefined;
};

const flatNodes = (
  ast: HtmlAST,
  expression: (tagName: string) => boolean
): HtmlAST => {
  if (ast.type === 'element') {
    const children = ast.children.map(child => flatNodes(child, expression));
    return {
      ...ast,
      children: children.flatMap(child => {
        if (child.type === 'element') {
          if (expression(child.tagName)) {
            return child.children;
          }
        }
        return child;
      }) as ElementContent[],
    };
  }
  return ast;
};

// Check if it is a paragraph like element
// https://html.spec.whatwg.org/#paragraph
const isParagraphLike = (node: Element): boolean => {
  // Flex container
  return (
    (typeof node.properties?.style === 'string' &&
      node.properties.style.match(/display:\s*flex/) !== null) ||
    getElementChildren(node).every(child => isElementInline(child))
  );
};

export const HastUtils = {
  isElement,
  getTextContent,
  getElementChildren,
  getTextChildren,
  getTextChildrenOnlyAst,
  getInlineOnlyElementAST,
  querySelector,
  flatNodes,
  isParagraphLike,
};
