/**
 * @module medium/protocol.js
 * @author Krzyszot Putyra
 * @date Apr 04, 2025
 * @description Classes impementing the message protocol
 */
import { defineEvents } from "../async/events.js";
import { createPromise } from "../async/promise.js";

/**
 * @typedef {string} MessageTypeEnum
 */

/**
 * The enum of types of messages
 * @enum {PromiseStatusEnum}
 */
export const MessageType = {
    MESSAGE: 'message',
    PROGRESS: 'progress',
    REQUEST: 'request',
    RESPONSE: 'response',
    CONTROL: 'ctl'
}

/**
 * @typedef MessageMetaData
 * @property {string} id
 * @property {string} name
 * @property {string} channel
 * @property {number} timestamp
 */

/**
 * @typedef RequestMetaData
 * @extends MessageMetaData
 * @property {string} reqId
 * @property {string} reply
 */


/**
 * Represents a generic notification message that expects no direct response.
 */
export class Message {

    /**
     * @param {*} payload - the content of the message
     * @param {MessageMetaData} meta - an object with meta data of the message
     */
    constructor(payload, meta) {
        this._meta = meta;
        this._payload = payload;
    }

    /** The source channel of the message */
    get channel() { return this._meta.channel; }

    /** The name field of the message */
    get name() { return this._meta.name; }

    /** The publish timestamp of the message */
    get timestamp() { return this._meta.timestamp; }

    /** The identifier of the message */
    get id() { return this._meta.id; }

    /** The content of the message */
    get payload() { return this._payload; }
}

/**
 * Represents a request received by the addressee
 */
export class Request extends Message {
    /**
     * 
     * @param {Messenger} messenger  - the messenger object that controls the communication
     * @param {*} payload  - the content of the request
     * @param {RequestMetaData} meta - an object with meta data of the request    
     */
    constructor(payload, meta, messenger) {
        super(payload, meta);
        this._messenger = messenger;
        this._done = false;
        this.ondone = null;
    }

    /** The channel on which the requester waits for a response */
    get requester() { return this._meta.reply; }

    /** The identifier of the request */
    get requestId() { return this._meta.reqId; }

    /**
     * Sends a progress notification to the requester
     * @param {*} payload - the content of the message
     * @returns {Promise<void>}
     * @throws when a response has been already sent
     */
    async notify(payload) {
        if (this._done) {
            throw new Error('the request is already finilized');
        }
        return this._messenger.sendRaw({
            type: MessageType.PROGRESS,
            reqId: this.requestId,
            payload
        }, [ this.requester ]);
    }

    /**
     * Sends a response and closes the request
     * @param {*} payload - the content of the response
     * @returns {Promise<void>}
     * @throws when a response has been already sent
     */
    async respond(payload) {
        if (this._done) {
            throw new Error('the request is already finilized');
        }
        this._done = true;
        if (typeof this.ondone === 'function') this.ondone();
        return this._messenger.sendRaw({
            type: MessageType.RESPONSE,
            reqId: this.requestId,
            payload
        }, [ this.requester ]);
    }
}

/**
 * Represents a future response. Provides events to monitor the status
 * of the response:
 * - progress(content)  - a progress message sent by the addressee
 * - done(content)      - a reponse message sent by the addressee
 */
export class Response extends EventTarget {
    /**
     * @param {Promise<any>} payloadPromise   a promise of the response
     */
    constructor(payloadPromise) {
        super();
        this._closed = false;
        this._payload = payloadPromise;
    }

    /**
     * Resolves to the response content once one is received.
     */
    async payload() { return await this._payload; }

    /**
     * Returns true if a reponse has been already received
     */
    get closed() { return this._closed; }

}
defineEvents(Response, 'progress');


/**
 * Represents a controller for a response object
 */
export class ResponseController {
    constructor({ signal, timeout }, cleanUpCallback) {
        this._response = new Response(
            createPromise((resolve, reject) => {
                this._resolve = resolve;
                this._reject = reject;
            }, { signal, timeout }).finally(cleanUpCallback)
        );
    }

    get response() { return this._response; }

    notify(content) {
        this.response.closed || this._response.dispatchEvent(new MessageEvent('progress', { data: content }));
    }

    close(content) {
        if (!this.response.closed) {
            this.response._closed = true;
            this._resolve(content);
        };
    }

}