import ContentAttachmentMime from "../graph/ContentAttachmentMime.js";
import ContentAttachmentOid from "../graph/ContentAttachmentOid.js";
import ContentAttachmentPayload from "../graph/ContentAttachmentPayload.js";
import ContentAttachmentRecord from "../graph/ContentAttachmentRecord.js";
import ContentAttachmentSize from "../graph/ContentAttachmentSize.js";
import EdgeRecord from "../graph/EdgeRecord.js";
import NodeRecord from "../graph/NodeRecord.js";
import WarpError from "../errors/WarpError.js";
import { CONTENT_MIME_PROPERTY_KEY, CONTENT_PROPERTY_KEY, CONTENT_SIZE_PROPERTY_KEY, encodeEdgeKey, } from "./KeyCodec.js";
import { compareEventIds } from "../utils/EventId.js";
import WarpState from "./state/WarpState.js";
/** Projects legacy content properties into typed content attachment records. */
export default class ContentAttachmentProjection {
    /** Returns visible content attachments ordered by deterministic owner id. */
    static fromState(state) {
        const checkedState = requireWarpState(state);
        const records = [];
        for (const owner of checkedState.nodeRecords()) {
            const record = contentRecordForNode(checkedState, owner);
            if (record !== null) {
                records.push(record);
            }
        }
        for (const owner of checkedState.edgeRecords()) {
            const record = contentRecordForEdge(checkedState, owner);
            if (record !== null) {
                records.push(record);
            }
        }
        records.sort(compareContentAttachmentRecords);
        return Object.freeze(records);
    }
    /** Returns the visible content attachment for one node, if present. */
    static forNode(state, nodeId) {
        const checkedState = requireWarpState(state);
        const owner = checkedState.getNodeRecord(nodeId);
        if (owner === null) {
            return null;
        }
        return contentRecordForNode(checkedState, owner);
    }
    /** Returns the visible content attachment for one edge, if present. */
    static forEdge(state, edge) {
        const checkedState = requireWarpState(state);
        const owner = checkedState.getEdgeRecord(EdgeRecord.fromLegacyEdge(edge).id);
        if (owner === null) {
            return null;
        }
        return contentRecordForEdge(checkedState, owner);
    }
}
/** Requires a runtime-backed WarpState projection source. */
function requireWarpState(state) {
    if (!(state instanceof WarpState)) {
        throw new WarpError('ContentAttachmentProjection source must be a WarpState', 'E_VALIDATION');
    }
    return state;
}
/** Builds a typed content attachment record for a visible node owner. */
function contentRecordForNode(state, owner) {
    const nodeId = owner.id.toString();
    const content = state.getNodeProp(nodeId, CONTENT_PROPERTY_KEY);
    if (!isProjectableContentRegister(content)) {
        return null;
    }
    return contentRecordFromRegisters(owner, {
        content,
        mime: state.getNodeProp(nodeId, CONTENT_MIME_PROPERTY_KEY) ?? null,
        size: state.getNodeProp(nodeId, CONTENT_SIZE_PROPERTY_KEY) ?? null,
    });
}
/** Builds a typed content attachment record for a visible edge owner. */
function contentRecordForEdge(state, owner) {
    const from = owner.from.toString();
    const to = owner.to.toString();
    const label = owner.typeId.toString();
    const edgeKey = encodeEdgeKey(from, to, label);
    const birthEvent = state.edgeBirthEvent.get(edgeKey);
    const content = visibleEdgeRegister(state.getEdgeProp(from, to, label, CONTENT_PROPERTY_KEY), birthEvent);
    if (!isProjectableContentRegister(content)) {
        return null;
    }
    return contentRecordFromRegisters(owner, {
        content,
        mime: visibleEdgeRegister(state.getEdgeProp(from, to, label, CONTENT_MIME_PROPERTY_KEY), birthEvent),
        size: visibleEdgeRegister(state.getEdgeProp(from, to, label, CONTENT_SIZE_PROPERTY_KEY), birthEvent),
    });
}
/** Returns a typed content record from visible legacy registers. */
function contentRecordFromRegisters(owner, registers) {
    return new ContentAttachmentRecord({
        owner,
        payload: new ContentAttachmentPayload({
            oid: new ContentAttachmentOid(registers.content.value),
            mime: contentMimeFromRegister(registers.content, registers.mime),
            size: contentSizeFromRegister(registers.content, registers.size),
        }),
    });
}
/** Returns true when a register can supply a content storage reference. */
function isProjectableContentRegister(register) {
    return register !== null
        && register !== undefined
        && isProjectableContentOidValue(register.value);
}
/** Filters edge registers hidden by edge rebirth. */
function visibleEdgeRegister(register, birthEvent) {
    if (register === undefined) {
        return null;
    }
    if (birthEvent !== undefined && register.eventId !== null && compareEventIds(register.eventId, birthEvent) < 0) {
        return null;
    }
    return register;
}
/** Returns true when metadata belongs to the same content write lineage. */
function isSameLineage(left, right) {
    if (!hasEventId(left)) {
        return false;
    }
    if (!hasEventId(right)) {
        return false;
    }
    return isSamePatchIdentity(left, right);
}
/** Returns true when a nullable event slot carries an event id. */
function hasEventId(eventId) {
    return eventId !== null && eventId !== undefined;
}
/** Returns true when two operations belong to the same patch identity. */
function isSamePatchIdentity(left, right) {
    return left.lamport === right.lamport
        && left.writerId === right.writerId
        && left.patchSha === right.patchSha;
}
/** Projects a MIME register into optional typed metadata. */
function contentMimeFromRegister(content, mime) {
    if (mime === null || !isSameLineage(content.eventId, mime.eventId)) {
        return null;
    }
    return contentMimeFromValue(mime.value);
}
/** Projects a raw MIME value into optional typed metadata. */
function contentMimeFromValue(value) {
    if (!isProjectableMimeValue(value)) {
        return null;
    }
    return new ContentAttachmentMime(value);
}
/** Projects a size register into optional typed metadata. */
function contentSizeFromRegister(content, size) {
    if (size === null || !isSameLineage(content.eventId, size.eventId)) {
        return null;
    }
    return contentSizeFromValue(size.value);
}
/** Projects a raw size value into optional typed metadata. */
function contentSizeFromValue(value) {
    if (!isProjectableSizeValue(value)) {
        return null;
    }
    return new ContentAttachmentSize(value);
}
/** Returns true when a value can become attachment MIME metadata. */
function isProjectableMimeValue(value) {
    return typeof value === 'string' && value.length > 0 && !value.includes('\0');
}
/** Returns true when a value can become an attachment storage reference. */
function isProjectableContentOidValue(value) {
    return typeof value === 'string' && value.length > 0 && !value.includes('\0');
}
/** Returns true when a value can become attachment size metadata. */
function isProjectableSizeValue(value) {
    return typeof value === 'number' && Number.isInteger(value) && value >= 0;
}
/** Compares content attachment records by deterministic owner order. */
function compareContentAttachmentRecords(left, right) {
    return compareStrings(contentAttachmentSortKey(left), contentAttachmentSortKey(right));
}
/** Returns the deterministic sort key for a content attachment record. */
function contentAttachmentSortKey(record) {
    if (record.owner instanceof NodeRecord) {
        return `node:${record.owner.id.toString()}`;
    }
    return `edge:${record.owner.id.toString()}`;
}
/** Compares protocol strings without locale-sensitive collation. */
function compareStrings(left, right) {
    if (left < right) {
        return -1;
    }
    if (left > right) {
        return 1;
    }
    return 0;
}
