/**
 * @copyright Sister Software
 * @license AGPL-3.0
 * @author Teffen Ellis, et al.
 */
import { ClassificationsMatchMap } from "mailwoman/core/classification";
import { Graph } from "./Graph.js";
const MAX_SPAN_LENGTH = 140;
const kSpanID = Symbol("SpanID");
/**
 * A span of text, i.e. a token or a phrase.
 */
export class Span extends Graph {
    #body = "";
    /**
     * The start index of the span.
     */
    start;
    /**
     * The end index of the span.
     */
    end;
    /**
     * The unique identifier for this span.
     */
    static IDCounter = 0;
    /**
     * The unique identifier for this span.
     */
    [kSpanID] = Span.IDCounter++;
    get id() {
        return this[kSpanID];
    }
    /**
     * The normalized body of the span.
     */
    normalized = "";
    /**
     * Classifications for this span.
     */
    classifications = new ClassificationsMatchMap();
    #flags = new Set();
    /**
     * Boolean-like indications that hint at the nature of the span.
     *
     * Unlike classifications, these are not exposed in the final output.
     */
    get flags() {
        return this.#flags;
    }
    is(classification) {
        return this.classifications.has(classification);
    }
    static from(input = "", options = {}) {
        const span = input instanceof Span ? input : new Span(input, options.start);
        for (const classification of options.classifications ?? []) {
            span.classifications.add(classification);
        }
        span.children.add(...(options.children ?? []));
        return span;
    }
    constructor(body = "", start = 0) {
        super();
        // Note that `start` should be set first to ensure that `end` is calculated correctly.
        this.start = start;
        this.body = body;
    }
    get body() {
        return this.#body;
    }
    /**
     * Set the body of the Span
     */
    set body(nextBody) {
        this.#flags.clear();
        this.#body = nextBody.slice(0, MAX_SPAN_LENGTH);
        this.normalized = this.#body.toLowerCase();
        this.end = this.start + this.#body.length;
        for (const [pattern, flag] of PatternMatchers) {
            if (pattern.test(this.normalized)) {
                this.#flags.add(flag);
                break;
            }
        }
        if (this.flags.has("numeric") || this.flags.has("alphanumeric")) {
            this.#flags.add("numeral");
        }
        if (this.#body.slice(-1) === ".") {
            this.#flags.add("ends_with_period");
        }
    }
    /**
     * Predicate to determine if this Span intersects another Span
     */
    intersects(that) {
        return this.start < that.end && this.end > that.start;
    }
    /**
     * Predicate to determine if this Span covers another Span
     */
    covers(that) {
        return this.start <= that.start && this.end >= that.end;
    }
    /**
     * Returns the distance between two Spans
     *
     * @todo Use graph to find prev and next spans for a more accurate result
     *
     * @todo Or base 'distance' on word distance (slop) rather than characters
     */
    distance(that) {
        if (this.intersects(that))
            return 0;
        if (this.end < that.start) {
            return that.start - this.end;
        }
        return this.start - that.end;
    }
    /**
     * Returns the coverage of the span, i.e. the number of characters covered by the span and its
     * children.
     */
    get coverage() {
        if (this.children.size) {
            return (Iterator
                // ---
                .from(this.children)
                .reduce((sum, child) => sum + (child.end - child.start), 0));
        }
        return this.end - this.start;
    }
    /**
     * The combined languages of the span's children.
     */
    get languages() {
        // Spread children langs to the parent...
        const languages = new Set();
        const displayNames = new Set();
        for (const child of this.children) {
            for (const classification of child.classifications.values()) {
                const childLanguages = classification.languages;
                if (!childLanguages)
                    continue;
                if (childLanguages.displayName) {
                    displayNames.add(childLanguages.displayName);
                }
                for (const language of childLanguages) {
                    languages.add(language);
                }
            }
        }
        languages.displayName = displayNames.size ? Array.from(displayNames).join(", ") : undefined;
        return languages;
    }
    /**
     * Serialize the span to JSON.
     */
    toJSON() {
        return {
            [kSpanID]: this.id,
            body: this.body,
            start: this.start,
            end: this.end,
            normalized: this.normalized,
            classifications: this.classifications.toJSON(),
            children: Iterator.from(this.children)
                .map((c) => c.toJSON())
                .toArray(),
            phrases: Iterator.from(this.phrases)
                .map((p) => p.toJSON())
                .toArray(),
        };
    }
    toString() {
        const classifications = Array.from(this.classifications.keys()).join(", ");
        const flags = Array.from(this.#flags).join(", ");
        return `Span(${this.children.size}) "${this.body}" [${classifications}][${flags}]`;
    }
    [Symbol.for("nodejs.util.inspect.custom")]() {
        return this.toString();
    }
    /**
     * Connect siblings in the graph.
     */
    static connectSiblings(...spans) {
        for (const [i, span] of spans.entries()) {
            if (spans[i - 1]) {
                span.previousSiblings.add(spans[i - 1]);
            }
            if (spans[i + 1]) {
                span.nextSiblings.add(spans[i + 1]);
            }
        }
        return spans;
    }
}
/**
 * Patterns to test and apply classifications to spans.
 *
 * Note that order here is important, as the first pattern that matches will be used.
 */
export const PatternMatchers = [
    // Entirely numeric, i.e. contains only digits.
    [/^\d+$/, "numeric"],
    // Entirely special characters, i.e. contains only punctuation, symbols, or other non-alphanumeric characters.
    [/^[@&/\\#,+()$~%.!^'";:*?[\]<>{}]+$/, "punctuation"],
    // Entirely alpha, i.e. contains only letters.
    [/^[A-Za-z\W]+$/, "alpha"],
    // Entirely alphanumeric, i.e. contains only letters and digits.
    [/^[A-Za-z0-9\W]+$/, "alphanumeric"],
];
//# sourceMappingURL=Span.js.map