"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RepresentationMetadata = exports.isRepresentationMetadata = void 0;
const n3_1 = require("n3");
const LogUtil_1 = require("../../logging/LogUtil");
const Header_1 = require("../../util/Header");
const TermUtil_1 = require("../../util/TermUtil");
const Vocabularies_1 = require("../../util/Vocabularies");
const ResourceIdentifier_1 = require("./ResourceIdentifier");
/**
 * Determines whether the object is a `RepresentationMetadata`.
 */
function isRepresentationMetadata(object) {
    return typeof object?.setMetadata === 'function';
}
exports.isRepresentationMetadata = isRepresentationMetadata;
// Caches named node conversions
const cachedNamedNodes = {};
/**
 * Converts the incoming name (URI or shorthand) to a named node.
 * The generated terms get cached to reduce the number of created nodes,
 * so only use this for internal constants!
 * @param name - Predicate to potentially transform.
 */
function toCachedNamedNode(name) {
    if (!(name in cachedNamedNodes)) {
        cachedNamedNodes[name] = n3_1.DataFactory.namedNode(name);
    }
    return cachedNamedNodes[name];
}
/**
 * Stores the metadata triples and provides methods for easy access.
 * Most functions return the metadata object to allow for chaining.
 */
class RepresentationMetadata {
    logger = (0, LogUtil_1.getLoggerFor)(this);
    store;
    id;
    constructor(input, overrides) {
        this.store = new n3_1.Store();
        if ((0, ResourceIdentifier_1.isResourceIdentifier)(input)) {
            this.id = n3_1.DataFactory.namedNode(input.path);
        }
        else if ((0, TermUtil_1.isTerm)(input)) {
            this.id = input;
        }
        else if (isRepresentationMetadata(input)) {
            this.id = input.identifier;
            this.addQuads(input.quads());
        }
        else {
            overrides = input;
            this.id = this.store.createBlankNode();
        }
        if (overrides) {
            if (typeof overrides === 'string') {
                this.contentType = overrides;
            }
            else if (overrides instanceof Header_1.ContentType) {
                this.contentTypeObject = overrides;
            }
            else {
                this.setOverrides(overrides);
            }
        }
    }
    setOverrides(overrides) {
        for (const predicate of Object.keys(overrides)) {
            const namedPredicate = toCachedNamedNode(predicate);
            this.removeAll(namedPredicate);
            let objects = overrides[predicate];
            if (!Array.isArray(objects)) {
                objects = [objects];
            }
            for (const object of objects) {
                this.store.addQuad(this.id, namedPredicate, (0, TermUtil_1.toObjectTerm)(object, true));
            }
        }
    }
    /**
     * @returns All matching metadata quads.
     */
    quads(subject = null, predicate = null, object = null, graph = null) {
        return this.store.getQuads(subject, predicate, object, graph);
    }
    /**
     * Identifier of the resource this metadata is relevant to.
     * Will update all relevant triples if this value gets changed.
     */
    get identifier() {
        return this.id;
    }
    set identifier(id) {
        if (!id.equals(this.id)) {
            // Convert all instances of the old identifier to the new identifier in the stored quads
            const quads = this.quads().map((quad) => {
                if (quad.subject.equals(this.id)) {
                    return n3_1.DataFactory.quad(id, quad.predicate, quad.object, quad.graph);
                }
                if (quad.object.equals(this.id)) {
                    return n3_1.DataFactory.quad(quad.subject, quad.predicate, id, quad.graph);
                }
                return quad;
            });
            this.store = new n3_1.Store(quads);
            this.id = id;
        }
    }
    /**
     * Helper function to import all entries from the given metadata.
     * If the new metadata has a different identifier the internal one will be updated.
     * @param metadata - Metadata to import.
     */
    setMetadata(metadata) {
        this.identifier = metadata.identifier;
        this.addQuads(metadata.quads());
        return this;
    }
    /**
     * @param subject - Subject of quad to add.
     * @param predicate - Predicate of quad to add.
     * @param object - Object of quad to add.
     * @param graph - Optional graph of quad to add.
     */
    addQuad(subject, predicate, object, graph) {
        this.store.addQuad((0, TermUtil_1.toNamedTerm)(subject), predicate, (0, TermUtil_1.toObjectTerm)(object, true), graph ? (0, TermUtil_1.toNamedTerm)(graph) : undefined);
        return this;
    }
    /**
     * @param quads - Quads to add to the metadata.
     */
    addQuads(quads) {
        this.store.addQuads(quads);
        return this;
    }
    /**
     * @param subject - Subject of quad to remove.
     * @param predicate - Predicate of quad to remove.
     * @param object - Object of quad to remove.
     * @param graph - Optional graph of quad to remove.
     */
    removeQuad(subject, predicate, object, graph) {
        const quads = this.quads((0, TermUtil_1.toNamedTerm)(subject), predicate, (0, TermUtil_1.toObjectTerm)(object, true), graph ? (0, TermUtil_1.toNamedTerm)(graph) : undefined);
        return this.removeQuads(quads);
    }
    /**
     * @param quads - Quads to remove from the metadata.
     */
    removeQuads(quads) {
        this.store.removeQuads(quads);
        return this;
    }
    /**
     * Adds a value linked to the identifier. Strings get converted to literals.
     * @param predicate - Predicate linking identifier to value.
     * @param object - Value(s) to add.
     * @param graph - Optional graph of where to add the values to.
     */
    add(predicate, object, graph) {
        return this.forQuads(predicate, object, (pred, obj) => this.addQuad(this.id, pred, obj, graph));
    }
    /**
     * Removes the given value from the metadata. Strings get converted to literals.
     * @param predicate - Predicate linking identifier to value.
     * @param object - Value(s) to remove.
     * @param graph - Optional graph of where to remove the values from.
     */
    remove(predicate, object, graph) {
        return this.forQuads(predicate, object, (pred, obj) => this.removeQuad(this.id, pred, obj, graph));
    }
    /**
     * Helper function to simplify add/remove
     * Runs the given function on all predicate/object pairs, but only converts the predicate to a named node once.
     */
    forQuads(predicate, object, forFn) {
        const objects = Array.isArray(object) ? object : [object];
        for (const obj of objects) {
            forFn(predicate, (0, TermUtil_1.toObjectTerm)(obj, true));
        }
        return this;
    }
    /**
     * Removes all values linked through the given predicate.
     * @param predicate - Predicate to remove.
     * @param graph - Optional graph where to remove from.
     */
    removeAll(predicate, graph) {
        this.removeQuads(this.store.getQuads(this.id, predicate, null, graph ?? null));
        return this;
    }
    /**
     * Verifies if a specific triple can be found in the metadata.
     * Undefined parameters are interpreted as wildcards.
     */
    has(predicate = null, object = null, graph = null) {
        // This works with N3.js but at the time of writing the typings have not been updated yet.
        // If you see this line of code check if the typings are already correct and update this if so.
        // eslint-disable-next-line ts/no-unsafe-call
        return this.store.has(this.id, predicate, object, graph);
    }
    /**
     * Finds all object values matching the given predicate and/or graph.
     * @param predicate - Optional predicate to get the values for.
     * @param graph - Optional graph where to get from.
     *
     * @returns An array with all matches.
     */
    getAll(predicate, graph) {
        return this.store.getQuads(this.id, predicate, null, graph ?? null)
            .map((quad) => quad.object);
    }
    /**
     * @param predicate - Predicate to get the value for.
     * @param graph - Optional graph where the triple should be found.
     *
     * @throws Error
     * If there are multiple matching values.
     *
     * @returns The corresponding value. Undefined if there is no match
     */
    get(predicate, graph) {
        const terms = this.getAll(predicate, graph);
        if (terms.length === 0) {
            return;
        }
        if (terms.length > 1) {
            this.logger.error(`Multiple results for ${predicate.value}`);
            // We can not use an `InternalServerError` here as otherwise errors and metadata files would depend on each other
            throw new Error(`Multiple results for ${predicate.value}`);
        }
        return terms[0];
    }
    /**
     * Sets the value for the given predicate, removing all other instances.
     * In case the object is undefined this is identical to `removeAll(predicate)`.
     * @param predicate - Predicate linking to the value.
     * @param object - Value(s) to set.
     * @param graph - Optional graph where the triple should be stored.
     */
    set(predicate, object, graph) {
        this.removeAll(predicate, graph);
        if (object) {
            this.add(predicate, object, graph);
        }
        return this;
    }
    setContentType(input) {
        // Make sure complete Content-Type RDF structure is gone
        this.removeContentType();
        if (!input) {
            return;
        }
        if (typeof input === 'string') {
            // Simple check to estimate if this is a simple content type.
            // If not, mention that the `contentTypeObject` should be used instead.
            // Not calling `parseContentType` here as that would cause a dependency loop with `HttpError`.
            if (!Header_1.SIMPLE_MEDIA_RANGE.test(input)) {
                // Not using an HttpError as HttpError depends on metadata
                throw new Error('Only simple content types can be set by string. Use the `contentTypeObject` function for complexer types.');
            }
            input = new Header_1.ContentType(input);
        }
        for (const [key, value] of Object.entries(input.parameters)) {
            const node = n3_1.DataFactory.blankNode();
            this.addQuad(this.id, Vocabularies_1.SOLID_META.terms.contentTypeParameter, node);
            this.addQuad(node, Vocabularies_1.RDFS.terms.label, key);
            this.addQuad(node, Vocabularies_1.SOLID_META.terms.value, value);
        }
        // Set base content type string
        this.set(Vocabularies_1.CONTENT_TYPE_TERM, input.value);
    }
    /**
     * Parse the internal RDF structure to retrieve the Record with ContentType Parameters.
     * @returns A {@link ContentType} object containing the value and optional parameters if there is one.
     */
    getContentType() {
        const value = this.get(Vocabularies_1.CONTENT_TYPE_TERM)?.value;
        if (!value) {
            return;
        }
        const params = this.getAll(Vocabularies_1.SOLID_META.terms.contentTypeParameter);
        const parameters = Object.fromEntries(params.map((param) => {
            const labels = this.store.getObjects(param, Vocabularies_1.RDFS.terms.label, null);
            const values = this.store.getObjects(param, Vocabularies_1.SOLID_META.terms.value, null);
            if (labels.length !== 1 || values.length !== 1) {
                this.logger.error(`Detected invalid content-type metadata for ${this.id.value}`);
                return ['invalid', ''];
            }
            return [labels[0].value, values[0].value];
        }));
        return new Header_1.ContentType(value, parameters);
    }
    removeContentType() {
        this.removeAll(Vocabularies_1.CONTENT_TYPE_TERM);
        const params = this.quads(this.id, Vocabularies_1.SOLID_META.terms.contentTypeParameter);
        for (const quad of params) {
            const paramEntries = this.quads(quad.object);
            this.store.removeQuads(paramEntries);
        }
        this.store.removeQuads(params);
    }
    // Syntactic sugar for common predicates
    /**
     * Shorthand for the CONTENT_TYPE predicate.
     */
    get contentType() {
        return this.get(Vocabularies_1.CONTENT_TYPE_TERM)?.value;
    }
    set contentType(input) {
        this.setContentType(input);
    }
    /**
     * Shorthand for the ContentType as an object (with parameters)
     */
    get contentTypeObject() {
        return this.getContentType();
    }
    set contentTypeObject(contentType) {
        this.setContentType(contentType);
    }
    /**
     * Shorthand for the CONTENT_LENGTH predicate.
     */
    get contentLength() {
        const length = this.get(Vocabularies_1.CONTENT_LENGTH_TERM);
        return length?.value ? Number(length.value) : undefined;
    }
    set contentLength(input) {
        if (input) {
            this.set(Vocabularies_1.CONTENT_LENGTH_TERM, (0, TermUtil_1.toLiteral)(input, Vocabularies_1.XSD.terms.integer));
        }
    }
}
exports.RepresentationMetadata = RepresentationMetadata;
//# sourceMappingURL=RepresentationMetadata.js.map