/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */

import { Channel, ChannelType, UdpNetworkChannel } from "#net/Channel.js";
import { Network, NetworkError } from "#net/Network.js";
import { Transport } from "#net/Transport.js";
import { Bytes } from "#util/Bytes.js";
import { Observable } from "#util/index.js";
import { ServerAddress, ServerAddressUdp } from "../ServerAddress.js";
import { UdpSocket } from "./UdpSocket.js";

export class UdpTransport implements Transport {
    readonly #server: UdpSocket;

    static async create(network: Network, type: "udp4" | "udp6", port?: number, host?: string, netInterface?: string) {
        return new UdpTransport(
            await network.createUdpSocket({ listeningPort: port, type, netInterface, listeningAddress: host }),
        );
    }

    constructor(server: UdpSocket) {
        this.#server = server;
    }

    protected get server() {
        return this.#server;
    }

    supports(type: ChannelType, address: string) {
        return this.#server.supports(type, address);
    }

    async openChannel(address: ServerAddress, _options?: Transport.OpenChannelOptions) {
        if (!ServerAddress.isIp(address)) {
            throw new NetworkError(`Unsupported address type for UDP interface`);
        }
        const { ip, port } = address;
        return Promise.resolve(new UdpChannel(this.#server, ip, port));
    }

    onData(listener: (channel: Channel<Bytes>, messageBytes: Bytes) => void): Transport.Listener {
        return this.#server.onData((_netInterface, peerHost, peerPort, data) => {
            listener(new UdpChannel(this.#server, peerHost, peerPort), data);
        });
    }

    get port() {
        return this.#server.port;
    }

    close() {
        return this.#server.close();
    }

    addMembership(address: string) {
        return this.#server.addMembership(address);
    }

    dropMembership(address: string) {
        return this.#server.dropMembership(address);
    }
}

export class UdpChannel implements UdpNetworkChannel<Bytes> {
    readonly isReliable = false;
    readonly supportsLargeMessages = false;
    readonly type = ChannelType.UDP;
    readonly #server: UdpSocket;
    #peerAddress: string;
    #peerPort: number;
    readonly networkAddressChanged = Observable<[ServerAddressUdp]>();

    constructor(server: UdpSocket, peerAddress: string, peerPort: number) {
        this.#server = server;
        this.#peerAddress = peerAddress;
        this.#peerPort = peerPort;
    }

    get maxPayloadSize() {
        return this.#server.maxPayloadSize;
    }

    send(data: Bytes, addressOverride?: ServerAddressUdp) {
        if (addressOverride) {
            return this.#server.send(addressOverride.ip, addressOverride.port, data);
        }
        return this.#server.send(this.#peerAddress, this.#peerPort, data);
    }

    get name() {
        return `${this.type}://${this.#peerAddress.includes(":") ? `[${this.#peerAddress}]` : this.#peerAddress}:${this.#peerPort}`;
    }

    get networkAddress(): ServerAddressUdp {
        return { type: "udp", ip: this.#peerAddress, port: this.#peerPort };
    }

    set networkAddress(address: ServerAddressUdp) {
        if (address.type !== "udp" || (address.ip === this.#peerAddress && address.port === this.#peerPort)) {
            return;
        }
        this.#peerAddress = address.ip;
        this.#peerPort = address.port;
        this.networkAddressChanged.emit(this.networkAddress);
    }

    async close() {
        // UDP is connectionless, so nothing to do here
    }
}
