import Attributor from './attributor/attributor.js';
import {
  type Blot,
  type BlotConstructor,
  type Root,
} from './blot/abstract/blot.js';
import ParchmentError from './error.js';
import Scope from './scope.js';

export type RegistryDefinition = Attributor | BlotConstructor;

export interface RegistryInterface {
  create(scroll: Root, input: Node | string | Scope, value?: any): Blot;
  query(query: string | Node | Scope, scope: Scope): RegistryDefinition | null;
  register(...definitions: any[]): any;
}

export default class Registry implements RegistryInterface {
  public static blots = new WeakMap<Node, Blot>();

  public static find(node?: Node | null, bubble = false): Blot | null {
    if (node == null) {
      return null;
    }
    if (this.blots.has(node)) {
      return this.blots.get(node) || null;
    }
    if (bubble) {
      let parentNode: Node | null = null;
      try {
        parentNode = node.parentNode;
      } catch (err) {
        // Probably hit a permission denied error.
        // A known case is in Firefox, event targets can be anonymous DIVs
        // inside an input element.
        // https://bugzilla.mozilla.org/show_bug.cgi?id=208427
        return null;
      }
      return this.find(parentNode, bubble);
    }
    return null;
  }

  private attributes: { [key: string]: Attributor } = {};
  private classes: { [key: string]: BlotConstructor } = {};
  private tags: { [key: string]: BlotConstructor } = {};
  private types: { [key: string]: RegistryDefinition } = {};

  public create(scroll: Root, input: Node | string | Scope, value?: any): Blot {
    const match = this.query(input);
    if (match == null) {
      throw new ParchmentError(`Unable to create ${input} blot`);
    }
    const blotClass = match as BlotConstructor;
    const node =
      // @ts-expect-error Fix me later
      input instanceof Node || input.nodeType === Node.TEXT_NODE
        ? input
        : blotClass.create(value);

    const blot = new blotClass(scroll, node as Node, value);
    Registry.blots.set(blot.domNode, blot);
    return blot;
  }

  public find(node: Node | null, bubble = false): Blot | null {
    return Registry.find(node, bubble);
  }

  public query(
    query: string | Node | Scope,
    scope: Scope = Scope.ANY,
  ): RegistryDefinition | null {
    let match;
    if (typeof query === 'string') {
      match = this.types[query] || this.attributes[query];
      // @ts-expect-error Fix me later
    } else if (query instanceof Text || query.nodeType === Node.TEXT_NODE) {
      match = this.types.text;
    } else if (typeof query === 'number') {
      if (query & Scope.LEVEL & Scope.BLOCK) {
        match = this.types.block;
      } else if (query & Scope.LEVEL & Scope.INLINE) {
        match = this.types.inline;
      }
    } else if (query instanceof Element) {
      const names = (query.getAttribute('class') || '').split(/\s+/);
      names.some((name) => {
        match = this.classes[name];
        if (match) {
          return true;
        }
        return false;
      });
      match = match || this.tags[query.tagName];
    }
    if (match == null) {
      return null;
    }
    if (
      'scope' in match &&
      scope & Scope.LEVEL & match.scope &&
      scope & Scope.TYPE & match.scope
    ) {
      return match;
    }
    return null;
  }

  public register(...definitions: RegistryDefinition[]): RegistryDefinition[] {
    return definitions.map((definition) => {
      const isBlot = 'blotName' in definition;
      const isAttr = 'attrName' in definition;
      if (!isBlot && !isAttr) {
        throw new ParchmentError('Invalid definition');
      } else if (isBlot && definition.blotName === 'abstract') {
        throw new ParchmentError('Cannot register abstract class');
      }
      const key = isBlot
        ? definition.blotName
        : isAttr
          ? definition.attrName
          : (undefined as never); // already handled by above checks
      this.types[key] = definition;

      if (isAttr) {
        if (typeof definition.keyName === 'string') {
          this.attributes[definition.keyName] = definition;
        }
      } else if (isBlot) {
        if (definition.className) {
          this.classes[definition.className] = definition;
        }
        if (definition.tagName) {
          if (Array.isArray(definition.tagName)) {
            definition.tagName = definition.tagName.map((tagName: string) => {
              return tagName.toUpperCase();
            });
          } else {
            definition.tagName = definition.tagName.toUpperCase();
          }
          const tagNames = Array.isArray(definition.tagName)
            ? definition.tagName
            : [definition.tagName];
          tagNames.forEach((tag: string) => {
            if (this.tags[tag] == null || definition.className == null) {
              this.tags[tag] = definition;
            }
          });
        }
      }
      return definition;
    });
  }
}
