"use strict";

import { TypedPort } from "./lib/TypedPort";
import { BridgeInfo } from "./lib/bridge";

import { ReceiverSelectorMediaType } from "./background/receiverSelector";
import {
    ReceiverSelection,
    ReceiverSelectionCast,
    ReceiverSelectionStop
} from "./background/receiverSelector";

import {
    CastSessionCreatedDetails,
    CastSessionUpdatedDetails,
    MediaStatus,
    ReceiverStatus,
    SenderMessage
} from "./cast/api/types";

import { ReceiverDevice } from "./types";

/**
 * Messages are JSON objects with a `subject` string key and a
 * generic `data` key:
 *   { subject: "...", data: ... }
 *
 * Message subjects may include an optional destination and
 * response name formatted like this:
 *   ^(destination:)?messageName(\/responseName)?$
 *
 * Message formats are specified with subject as a key and data
 * as the value in the message tables.
 */

/**
 * Messages exclusively used internally between extension
 * components.
 */
type ExtMessageDefinitions = {
    "popup:init": {
        appId?: string;
    };
    "popup:update": {
        receivers: ReceiverDevice[];
        defaultMediaType?: ReceiverSelectorMediaType;
        availableMediaTypes?: ReceiverSelectorMediaType;
    };
    "popup:close": {};

    "receiverSelector:selected": ReceiverSelection;
    "receiverSelector:stop": ReceiverSelection;

    "main:selectReceiver": {};
    "cast:selectReceiver/selected": ReceiverSelectionCast;
    "cast:selectReceiver/stopped": ReceiverSelectionStop;
    "cast:selectReceiver/cancelled": {};

    "main:closeReceiverSelector": {};

    "main:initializeCast": { appId: string };
    "cast:initialized": BridgeInfo;

    "cast:receiverDeviceUp": { receiverDevice: ReceiverDevice };
    "cast:receiverDeviceDown": { receiverDeviceId: ReceiverDevice["id"] };
    "cast:launchApp": { receiver: ReceiverDevice };
};

/**
 * IMPORTANT:
 * Messages that cross the native messaging channel. MUST keep
 * in-sync with the bridge's version at:
 *   app/src/bridge/messaging.ts > MessageDefinitions
 */
type AppMessageDefinitions = {
    /**
     * First message sent by the extension to the bridge.
     * Includes extension version string. Responds directly with version
     * string of the bridge to compare.
     *
     * Still uses `:/` message separator for compat talking to older
     * bridge versions.
     */
    "bridge:getInfo": string;
    "bridge:/getInfo": string;

    /**
     * Tells a bridge to begin service discovery (and whether to
     * establish connections to monitor the status of the receiver
     * devices).
     */
    "bridge:startDiscovery": {
        shouldWatchStatus: boolean;
    };

    /**
     * Sent to extension from the bridge whenever a receiver device is
     * found.
     */
    "main:receiverDeviceUp": { deviceId: string; deviceInfo: ReceiverDevice };
    /**
     * Sent to extension from the bridge whenever a previously found
     * receiver device is lost.
     */
    "main:receiverDeviceDown": { deviceId: string };

    /**
     * Sent to the extension from the bridge whenever a
     * `RECEIVER_STATUS` message (`NS_RECEIVER`) is received.
     */
    "main:receiverDeviceStatusUpdated": {
        deviceId: string;
        status: ReceiverStatus;
    };
    /**
     * Sent to the extension from the bridge whenever a
     * `MEDIA_STATUS` message (`NS_RECEIVER`) is received.
     */
    "main:receiverDeviceMediaStatusUpdated": {
        deviceId: string;
        status: MediaStatus;
    };

    /**
     * Sent to bridge from cast API instance when a session request is
     * initiated.
     */
    "bridge:createCastSession": {
        appId: string;
        receiverDevice: ReceiverDevice;
    };
    /**
     * Connects to, and sends a `STOP` message on the `NS_RECEIVER`
     * channel for the given receiver device.
     */
    "bridge:stopCastSession": {
        receiverDevice: ReceiverDevice;
    };

    /**
     * Sent to cast API instances whenever a session is created or
     * updates. Updated details is a mutable subset of session details
     * otherwise fixed on creation.
     */
    "cast:sessionCreated": CastSessionCreatedDetails;
    "cast:sessionUpdated": CastSessionUpdatedDetails;
    /**
     * Sent to cast API instances whenever a session is stopped.
     */
    "cast:sessionStopped": {
        sessionId: string;
    };

    /**
     * Sent to bridge from cast API instance whenever an `NS_RECEIVER`
     * message needs to be sent.
     */
    "bridge:sendCastReceiverMessage": {
        sessionId: string;
        messageData: SenderMessage;
        messageId: string;
    };

    /**
     * Sent to bridge from cast API instance whenever a application
     * session message needs to be sent (via
     * `chrome.cast.Session#sendMessage`).
     */
    "bridge:sendCastSessionMessage": {
        sessionId: string;
        namespace: string;
        messageData: object | string;
        messageId: string;
    };
    /**
     * Sent to cast API instance from bridge when session message
     * received from a receiver device.
     */
    "cast:receivedSessionMessage": {
        sessionId: string;
        namespace: string;
        messageData: string;
    };

    /**
     * Sent to cast API instance from bridge whenever a message
     * operation is completed. If an error ocurred, an error string will
     * be passed as the `error` data property.
     *
     * TODO: Check how/if this works with receiver messages (via
     * `bridge:sendCastReceiverMessage`).
     */
    "cast:impl_sendMessage": {
        sessionId: string;
        messageId: string;
        error?: string;
    };

    /**
     * Sent to the bridge to start an HTTP media server at a given file
     * path on the given port.
     */
    "bridge:startMediaServer": {
        filePath: string;
        port: number;
    };
    /**
     * Sent to media sender from bridge when the media server is ready
     * to serve files.
     */
    "mediaCast:mediaServerStarted": {
        mediaPath: string;
        subtitlePaths: string[];
        localAddress: string;
    };
    /**
     * Sent to bridge to stop HTTP media server.
     */
    "bridge:stopMediaServer": {};
    /**
     * Sent to media sender from bridge when the media server has
     * stopped.
     */
    "mediaCast:mediaServerStopped": {};
    /**
     * Sent to media sender from bridge when the media server has
     * encountered an error.
     */
    "mediaCast:mediaServerError": {};
};

type MessageDefinitions = ExtMessageDefinitions & AppMessageDefinitions;

interface MessageBase<K extends keyof MessageDefinitions> {
    subject: K;
    data: MessageDefinitions[K];
}

type Messages = {
    [K in keyof MessageDefinitions]: MessageBase<K>;
};

/**
 * Make message data key optional if specified as blank or with
 * all-optional keys.
 */
type NarrowedMessage<L extends MessageBase<keyof MessageDefinitions>> =
    L extends any
        ? {} extends L["data"]
            ? Omit<L, "data"> & Partial<L>
            : L
        : never;

export type Port = TypedPort<Message>;
export type Message = NarrowedMessage<Messages[keyof Messages]>;

/**
 * Typed WebExtension-style messaging utility class.
 */
export default new (class Messenger {
    connect(connectInfo: { name: string }) {
        return browser.runtime.connect(connectInfo) as unknown as Port;
    }

    connectTab(tabId: number, connectInfo: { name: string; frameId: number }) {
        return browser.tabs.connect(tabId, connectInfo) as unknown as Port;
    }

    onConnect = {
        addListener(cb: (port: Port) => void) {
            browser.runtime.onConnect.addListener(cb as any);
        },
        removeListener(cb: (port: Port) => void) {
            browser.runtime.onConnect.removeListener(cb as any);
        },
        hasListener(cb: (port: Port) => void) {
            return browser.runtime.onConnect.hasListener(cb as any);
        }
    };
})();
