import EventEmitter from "events";
import { Readable, Writable } from "stream";
export class Controller {
    constructor() {
        this.events = new EventEmitter();
        this.sources = new Map();
        this.nextEndpointId = 1;
        this.requests = new Map();
        this.nextRequestId = 1;
        this.onCreate = (payload) => {
            const endpoint = payload.endpoint;
            const source = new Source(endpoint);
            this.sources.set(endpoint.id, source);
            this.events.emit("stream", source);
        };
        this.onFinish = (payload) => {
            const id = payload.endpoint.id;
            const source = this.sources.get(id);
            if (source === undefined) {
                throw new Error("Invalid endpoint ID");
            }
            this.sources.delete(id);
            source.push(null);
        };
        this.onWrite = (payload, data) => {
            const id = payload.endpoint.id;
            const source = this.sources.get(id);
            if (source === undefined) {
                throw new Error("Invalid endpoint ID");
            }
            if (data === null) {
                throw new Error("Invalid request: missing data");
            }
            return source.deliver(data);
        };
        this.handlers = {
            ".create": this.onCreate,
            ".finish": this.onFinish,
            ".write": this.onWrite
        };
    }
    open(label, details = {}) {
        const endpoint = {
            id: this.nextEndpointId++,
            label,
            details
        };
        return new Sink(this, endpoint);
    }
    receive(packet) {
        const stanza = packet.stanza;
        const { id, name, payload } = stanza;
        const type = name[0];
        if (type === ".") {
            this.onRequest(id, name, payload, packet.data);
        }
        else if (type === "+") {
            this.onNotification(id, name, payload);
        }
        else {
            throw new Error("Unknown stanza: " + name);
        }
    }
    _request(name, payload, data) {
        return new Promise((resolve, reject) => {
            const id = this.nextRequestId++;
            this.requests.set(id, {
                resolve: resolve,
                reject: reject
            });
            const stanza = {
                id,
                name,
                payload
            };
            this.events.emit("send", {
                stanza,
                data
            });
        });
    }
    onRequest(id, name, payload, data) {
        const handler = this.handlers[name];
        if (handler === undefined) {
            throw new Error(`Invalid request: ${name}`);
        }
        let result;
        try {
            result = handler(payload, data);
        }
        catch (e) {
            this.reject(id, e);
            return;
        }
        if (result instanceof Promise) {
            result
                .then(value => this.resolve(id, value))
                .catch(error => this.reject(id, error));
        }
        else {
            this.resolve(id, result);
        }
    }
    resolve(id, value) {
        const stanza = {
            id: id,
            name: "+result",
            payload: value
        };
        this.events.emit("send", {
            stanza,
            data: null
        });
    }
    reject(id, error) {
        const stanza = {
            id: id,
            name: "+error",
            payload: {
                message: error.toString()
            }
        };
        this.events.emit("send", {
            stanza,
            data: null
        });
    }
    onNotification(id, name, payload) {
        const request = this.requests.get(id);
        if (request === undefined) {
            throw new Error("Invalid request ID");
        }
        this.requests.delete(id);
        if (name === "+result") {
            request.resolve(payload);
        }
        else if (name === "+error") {
            const response = payload;
            request.reject(new Error(response.message));
        }
        else {
            throw new Error("Unknown notification: " + name);
        }
    }
}
export default Controller;
class Source extends Readable {
    constructor({ label, details }) {
        super();
        this.onReadComplete = null;
        this.delivery = null;
        this.label = label;
        this.details = details;
    }
    _read(size) {
        if (this.onReadComplete !== null) {
            return;
        }
        this.onReadComplete = chunk => {
            this.onReadComplete = null;
            if (chunk.length === 0) {
                this.push(null);
                return false;
            }
            if (this.push(chunk)) {
                this._read(size);
            }
            return true;
        };
        this.tryComplete();
    }
    deliver(chunk) {
        return new Promise((resolve, reject) => {
            if (this.delivery !== null) {
                throw new Error("Protocol violation");
            }
            this.delivery = {
                chunk: chunk,
                resolve: resolve,
                reject: reject
            };
            this.tryComplete();
        });
    }
    tryComplete() {
        const { onReadComplete, delivery } = this;
        if (onReadComplete === null || delivery === null) {
            return;
        }
        this.onReadComplete = null;
        this.delivery = null;
        if (onReadComplete(delivery.chunk)) {
            delivery.resolve();
        }
        else {
            delivery.reject(new Error("Stream closed"));
        }
    }
}
class Sink extends Writable {
    constructor(controller, endpoint) {
        super();
        this.controller = controller;
        this.endpoint = endpoint;
        this.controller._request(".create", { endpoint: this.endpoint }, null);
        this.once("finish", this._onFinish.bind(this));
    }
    _write(chunk, encoding, callback) {
        this.controller._request(".write", { endpoint: this.endpoint }, chunk)
            .then(_ => callback())
            .catch(error => callback(error));
    }
    _onFinish() {
        this.controller._request(".finish", { endpoint: this.endpoint }, null);
    }
}
