/* eslint-disable @typescript-eslint/no-use-before-define */
export abstract class NodeSelector {
  private pattern: string;

  private root: Document;

  constructor(pattern: string) {
    this.pattern = pattern;
    this.root = document;
  }

  getPattern() {
    return this.pattern;
  }

  setRoot(root: Document) {
    this.root = root;
  }

  getRoot() {
    return this.root;
  }

  abstract parse(): Node | null;
}

class QuerySelector extends NodeSelector {
  parse(): Node | null {
    return this.getRoot().querySelector(this.getPattern());
  }
}

class IDSelector extends NodeSelector {
  parse(): Node | null {
    const root = this.getRoot();
    if (typeof root?.getElementById !== 'function') {
      return null;
    }
    return root.getElementById(this.getPattern()) as Node;
  }
}

class XpathSelector extends NodeSelector {
  parse(): Node | null {
    return this.getRoot()?.evaluate(
      this.getPattern(),
      this.getRoot(),
      null,
      XPathResult.FIRST_ORDERED_NODE_TYPE,
      null,
    )?.singleNodeValue as Node;
  }
}

class ChainSelector extends NodeSelector {
  parse(): Node | null {
    const selectors = this.getPattern().split('@');
    let currentRoot: Node | null = null;

    for (let i = 0; i < selectors.length; i += 1) {
      const selector = SelectorFactory.parse(selectors[i].trim());

      if (currentRoot) {
        selector.setRoot(currentRoot as Document);
      }

      const currentNode = selector.parse();

      if (i === selectors.length - 1) {
        return currentNode;
      }

      let node: Node | null | undefined = currentNode || document;
      if ((currentNode as HTMLElement)?.shadowRoot) {
        node = (currentNode as HTMLElement).shadowRoot;
      }

      if ((currentNode as HTMLIFrameElement)?.contentWindow) {
        node = (currentNode as HTMLIFrameElement).contentWindow?.document;
      }

      currentRoot = node ?? null;
    }

    return null;
  }
}

class Empty extends NodeSelector {
  constructor() {
    super('');
  }

  parse(): Node | null {
    return null;
  }
}

export class SelectorFactory {
  static parse(composedSelector?: string) {
    if (!composedSelector) {
      return new Empty();
    }

    const split = composedSelector.split('|');
    const type = split[0];
    const selector = split[1];

    switch (type) {
      case 'id':
        return this.id(selector);
      case 'query':
        return this.query(selector);
      case 'xpath':
        return this.xpath(selector);
      default:
        return new Empty();
    }
  }

  static check(selector?: NodeSelector) {
    return selector ?? new Empty();
  }

  static chain(pattern?: string) {
    return pattern ? new ChainSelector(pattern) : new Empty();
  }

  static id(pattern?: string): NodeSelector {
    return pattern ? new IDSelector(pattern) : new Empty();
  }

  static query(pattern?: string): NodeSelector {
    return pattern ? new QuerySelector(pattern) : new Empty();
  }

  static xpath(pattern?: string): NodeSelector {
    return pattern ? new XpathSelector(pattern) : new Empty();
  }
}
