// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator], i;
    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
import { _createStatefulChatClientInner } from "../../../../../chat-stateful-client/src";
import { createDefaultChatHandlers } from "../../../../../chat-component-bindings/src";
import { toFlatCommunicationIdentifier } from "../../../../../acs-ui-common/src";
import { EventEmitter } from 'events';
import { useEffect, useRef, useState } from 'react';
import { _isValidIdentifier } from "../../../../../acs-ui-common/src";
import { TEAMS_LIMITATION_LEARN_MORE, UNSUPPORTED_CHAT_THREAD_TYPE } from '../../common/constants';
import { createProfileStateModifier } from './OnFetchProfileCallback';
/**
 * Context of Chat, which is a centralized context for all state updates
 * @private
 */
export class ChatContext {
    constructor(clientState, threadId, chatAdapterOptions) {
        this.emitter = new EventEmitter();
        const thread = clientState.threads[threadId];
        this.threadId = threadId;
        if (!thread) {
            throw 'Cannot find threadId, please initialize thread before use!';
        }
        this.state = {
            userId: clientState.userId,
            displayName: clientState.displayName,
            thread,
            latestErrors: clientState.latestErrors
        };
        this.displayNameModifier = (chatAdapterOptions === null || chatAdapterOptions === void 0 ? void 0 : chatAdapterOptions.onFetchProfile) ? createProfileStateModifier(chatAdapterOptions.onFetchProfile, () => {
            this.setState(this.getState());
        }) : undefined;
    }
    onStateChange(handler) {
        this.emitter.on('stateChanged', handler);
    }
    offStateChange(handler) {
        this.emitter.off('stateChanged', handler);
    }
    setState(state) {
        this.state = state;
        this.state = this.displayNameModifier ? this.displayNameModifier(state) : state;
        this.emitter.emit('stateChanged', this.state);
    }
    getState() {
        return this.state;
    }
    setError(error) {
        this.setState(Object.assign(Object.assign({}, this.state), { error }));
    }
    updateClientState(clientState) {
        const thread = clientState.threads[this.threadId];
        if (!thread) {
            throw 'Cannot find threadId, please make sure thread state is still in Stateful ChatClient.';
        }
        let updatedState = {
            userId: clientState.userId,
            displayName: clientState.displayName,
            thread,
            latestErrors: clientState.latestErrors
        };
        this.setState(updatedState);
    }
}
/**
 * @private
 */
export class AzureCommunicationChatAdapter {
    constructor(chatClient, chatThreadClient, chatAdapterOptions) {
        this.emitter = new EventEmitter();
        this.bindAllPublicMethods();
        this.chatClient = chatClient;
        this.chatThreadClient = chatThreadClient;
        this.context = new ChatContext(chatClient.getState(), chatThreadClient.threadId, chatAdapterOptions);
        const onStateChange = (clientState) => {
            // unsubscribe when the instance gets disposed
            if (!this) {
                chatClient.offStateChange(onStateChange);
                return;
            }
            this.context.updateClientState(clientState);
        };
        this.handlers = createDefaultChatHandlers(chatClient, chatThreadClient);
        this.chatClient.onStateChange(onStateChange);
        this.subscribeAllEvents();
    }
    bindAllPublicMethods() {
        this.onStateChange = this.onStateChange.bind(this);
        this.offStateChange = this.offStateChange.bind(this);
        this.getState = this.getState.bind(this);
        this.dispose = this.dispose.bind(this);
        this.fetchInitialData = this.fetchInitialData.bind(this);
        this.sendMessage = this.sendMessage.bind(this);
        this.sendReadReceipt = this.sendReadReceipt.bind(this);
        this.sendTypingIndicator = this.sendTypingIndicator.bind(this);
        this.updateMessage = this.updateMessage.bind(this);
        this.deleteMessage = this.deleteMessage.bind(this);
        this.removeParticipant = this.removeParticipant.bind(this);
        this.setTopic = this.setTopic.bind(this);
        this.loadPreviousChatMessages = this.loadPreviousChatMessages.bind(this);
        this.on = this.on.bind(this);
        this.off = this.off.bind(this);
        this.downloadResourceToCache = this.downloadResourceToCache.bind(this);
        this.removeResourceFromCache = this.removeResourceFromCache.bind(this);
    }
    dispose() {
        this.unsubscribeAllEvents();
        this.chatClient.dispose();
    }
    fetchInitialData() {
        return __awaiter(this, void 0, void 0, function* () {
            // If get properties fails we dont want to try to get the participants after.
            yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                var _a, e_1, _b, _c;
                yield this.chatThreadClient.getProperties();
                try {
                    // Fetch all participants who joined before the local user.
                    for (var _d = true, _e = __asyncValues(this.chatThreadClient.listParticipants().byPage({
                        // Fetch 100 participants per page by default.
                        maxPageSize: 100
                    })), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
                        _c = _f.value;
                        _d = false;
                        const _page = _c;
                        ;
                    }
                }
                catch (e_1_1) { e_1 = { error: e_1_1 }; }
                finally {
                    try {
                        if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
                    }
                    finally { if (e_1) throw e_1.error; }
                }
            }));
        });
    }
    getState() {
        return this.context.getState();
    }
    onStateChange(handler) {
        this.context.onStateChange(handler);
    }
    offStateChange(handler) {
        this.context.offStateChange(handler);
    }
    sendMessage(content, options) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                return yield this.handlers.onSendMessage(content, options);
            }));
        });
    }
    sendReadReceipt(chatMessageId) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                yield this.handlers.onMessageSeen(chatMessageId);
            }));
        });
    }
    sendTypingIndicator() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                yield this.handlers.onTyping();
            }));
        });
    }
    removeParticipant(userId) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                yield this.handlers.onRemoveParticipant(userId);
            }));
        });
    }
    setTopic(topicName) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                yield this.handlers.updateThreadTopicName(topicName);
            }));
        });
    }
    loadPreviousChatMessages(messagesToLoad) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                return yield this.handlers.onLoadPreviousChatMessages(messagesToLoad);
            }));
        });
    }
    updateMessage(messageId, content, options) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                // metadata is never used in the current stable
                return yield this.handlers.onUpdateMessage(messageId, content);
            }));
        });
    }
    deleteMessage(messageId) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.asyncTeeErrorToEventEmitter(() => __awaiter(this, void 0, void 0, function* () {
                return yield this.handlers.onDeleteMessage(messageId);
            }));
        });
    }
    downloadResourceToCache(resourceDetails) {
        return __awaiter(this, void 0, void 0, function* () {
            this.chatClient.downloadResourceToCache(resourceDetails.threadId, resourceDetails.messageId, resourceDetails.resourceUrl);
        });
    }
    removeResourceFromCache(resourceDetails) {
        this.chatClient.removeResourceFromCache(resourceDetails.threadId, resourceDetails.messageId, resourceDetails.resourceUrl);
    }
    messageReceivedListener(event) {
        const isCurrentChatAdapterThread = event.threadId === this.chatThreadClient.threadId;
        if (!isCurrentChatAdapterThread) {
            return;
        }
        const message = convertEventToChatMessage(event);
        this.emitter.emit('messageReceived', {
            message
        });
        const currentUserId = toFlatCommunicationIdentifier(this.chatClient.getState().userId);
        if ((message === null || message === void 0 ? void 0 : message.sender) && toFlatCommunicationIdentifier(message.sender) === currentUserId) {
            this.emitter.emit('messageSent', {
                message
            });
        }
    }
    messageEditedListener(event) {
        const isCurrentChatAdapterThread = event.threadId === this.chatThreadClient.threadId;
        if (!isCurrentChatAdapterThread) {
            return;
        }
        const message = convertEventToChatMessage(event);
        this.emitter.emit('messageEdited', {
            message
        });
    }
    messageDeletedListener(event) {
        const isCurrentChatAdapterThread = event.threadId === this.chatThreadClient.threadId;
        if (!isCurrentChatAdapterThread) {
            return;
        }
        const message = convertEventToChatMessage(event);
        this.emitter.emit('messageDeleted', {
            message
        });
    }
    messageReadListener({ chatMessageId, recipient }) {
        const message = this.getState().thread.chatMessages[chatMessageId];
        if (message) {
            this.emitter.emit('messageRead', {
                message,
                readBy: recipient
            });
        }
    }
    participantsAddedListener({ addedBy, participantsAdded }) {
        this.emitter.emit('participantsAdded', {
            addedBy,
            participantsAdded
        });
    }
    participantsRemovedListener({ removedBy, participantsRemoved }) {
        this.emitter.emit('participantsRemoved', {
            removedBy,
            participantsRemoved
        });
    }
    chatThreadPropertiesUpdatedListener(event) {
        this.emitter.emit('topicChanged', {
            topic: event.properties.topic
        });
    }
    subscribeAllEvents() {
        this.chatClient.on('chatThreadPropertiesUpdated', this.chatThreadPropertiesUpdatedListener.bind(this));
        this.chatClient.on('participantsAdded', this.participantsAddedListener.bind(this));
        this.chatClient.on('participantsRemoved', this.participantsRemovedListener.bind(this));
        this.chatClient.on('chatMessageReceived', this.messageReceivedListener.bind(this));
        this.chatClient.on('chatMessageEdited', this.messageEditedListener.bind(this));
        this.chatClient.on('chatMessageDeleted', this.messageDeletedListener.bind(this));
        this.chatClient.on('readReceiptReceived', this.messageReadListener.bind(this));
        this.chatClient.on('participantsRemoved', this.participantsRemovedListener.bind(this));
    }
    unsubscribeAllEvents() {
        this.chatClient.off('chatThreadPropertiesUpdated', this.chatThreadPropertiesUpdatedListener.bind(this));
        this.chatClient.off('participantsAdded', this.participantsAddedListener.bind(this));
        this.chatClient.off('participantsRemoved', this.participantsRemovedListener.bind(this));
        this.chatClient.off('chatMessageReceived', this.messageReceivedListener.bind(this));
        this.chatClient.off('chatMessageEdited', this.messageEditedListener.bind(this));
        this.chatClient.off('chatMessageDeleted', this.messageDeletedListener.bind(this));
        this.chatClient.off('readReceiptReceived', this.messageReadListener.bind(this));
        this.chatClient.off('participantsRemoved', this.participantsRemovedListener.bind(this));
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    on(event, listener) {
        this.emitter.on(event, listener);
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    off(event, listener) {
        this.emitter.off(event, listener);
    }
    asyncTeeErrorToEventEmitter(f) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                return yield f();
            }
            catch (error) {
                if (isChatError(error)) {
                    this.emitter.emit('error', error);
                }
                throw error;
            }
        });
    }
}
const convertEventToChatMessage = (event) => {
    return {
        id: event.id,
        version: event.version,
        content: isChatMessageDeletedEvent(event) ? undefined : {
            message: event.message
        },
        type: convertEventType(event.type),
        sender: event.sender,
        senderDisplayName: event.senderDisplayName,
        sequenceId: '',
        createdOn: new Date(event.createdOn),
        editedOn: isChatMessageEditedEvent(event) ? event.editedOn : undefined,
        deletedOn: isChatMessageDeletedEvent(event) ? event.deletedOn : undefined
    };
};
const isChatMessageEditedEvent = (event) => {
    return 'editedOn' in event;
};
const isChatMessageDeletedEvent = (event) => {
    return 'deletedOn' in event;
};
// only text/html message type will be received from event
const convertEventType = (type) => {
    const lowerCaseType = type.toLowerCase();
    if (lowerCaseType === 'richtext/html' || lowerCaseType === 'html') {
        return 'html';
    }
    else {
        return 'text';
    }
};
/**
 * Create a {@link ChatAdapter} backed by Azure Communication Services.
 *
 * This is the default implementation of {@link ChatAdapter} provided by this library.
 *
 * @public
 */
export const createAzureCommunicationChatAdapter = (_a) => __awaiter(void 0, [_a], void 0, function* ({ endpoint: endpointUrl, userId, displayName, credential, threadId, chatAdapterOptions }) {
    return _createAzureCommunicationChatAdapterInner(endpointUrl, userId, displayName, credential, threadId, 'Chat', chatAdapterOptions);
});
/**
 * This inner function is used to allow injection of TelemetryImplementationHint without changing the public API.
 *
 * @internal
 */
export const _createAzureCommunicationChatAdapterInner = function (endpoint_1, userId_1, displayName_1, credential_1, threadId_1) {
    return __awaiter(this, arguments, void 0, function* (endpoint, userId, displayName, credential, threadId, telemetryImplementationHint = 'Chat', chatAdapterOptions) {
        if (!_isValidIdentifier(userId)) {
            throw new Error('Provided userId is invalid. Please provide valid identifier object.');
        }
        const chatClient = _createStatefulChatClientInner({
            userId,
            displayName,
            endpoint,
            credential
        }, undefined, telemetryImplementationHint);
        const chatThreadClient = yield chatClient.getChatThreadClient(threadId);
        yield chatClient.startRealtimeNotifications();
        const adapter = yield createAzureCommunicationChatAdapterFromClient(chatClient, chatThreadClient, chatAdapterOptions);
        return adapter;
    });
};
/**
 * This inner function to create ChatAdapterPromise in case when threadID is not avaialble.
 * ThreadId is a promise to allow for lazy initialization of the adapter.
 * @internal
 */
export const _createLazyAzureCommunicationChatAdapterInner = function (endpoint_1, userId_1, displayName_1, credential_1, threadId_1) {
    return __awaiter(this, arguments, void 0, function* (endpoint, userId, displayName, credential, threadId, telemetryImplementationHint = 'Chat', chatAdapterOptions) {
        if (!_isValidIdentifier(userId)) {
            throw new Error('Provided userId is invalid. Please provide valid identifier object.');
        }
        const chatClient = _createStatefulChatClientInner({
            userId,
            displayName,
            endpoint,
            credential
        }, undefined, telemetryImplementationHint);
        return threadId.then((threadId) => __awaiter(this, void 0, void 0, function* () {
            if (UNSUPPORTED_CHAT_THREAD_TYPE.some(t => threadId.includes(t))) {
                console.error(`Invalid Chat ThreadId: ${threadId}. Please note with Teams Channel Meetings, only Calling is supported and Chat is not currently supported. Read more: ${TEAMS_LIMITATION_LEARN_MORE}.`);
            }
            const chatThreadClient = yield chatClient.getChatThreadClient(threadId);
            yield chatClient.startRealtimeNotifications();
            const adapter = yield createAzureCommunicationChatAdapterFromClient(chatClient, chatThreadClient, chatAdapterOptions);
            return adapter;
        }));
    });
};
/**
 * A custom React hook to simplify the creation of {@link ChatAdapter}.
 *
 * Similar to {@link createAzureCommunicationChatAdapter}, but takes care of asynchronous
 * creation of the adapter internally.
 *
 * Allows arguments to be undefined so that you can respect the rule-of-hooks and pass in arguments
 * as they are created. The adapter is only created when all arguments are defined.
 *
 * Note that you must memoize the arguments to avoid recreating adapter on each render.
 * See storybook for typical usage examples.
 *
 * @public
 */
export const useAzureCommunicationChatAdapter = (args, afterCreate, beforeDispose) => {
    const { credential, displayName, endpoint, threadId, userId } = args;
    // State update needed to rerender the parent component when a new adapter is created.
    const [adapter, setAdapter] = useState(undefined);
    // Ref needed for cleanup to access the old adapter created asynchronously.
    const adapterRef = useRef(undefined);
    const creatingAdapterRef = useRef(false);
    const afterCreateRef = useRef(undefined);
    const beforeDisposeRef = useRef(undefined);
    // These refs are updated on *each* render, so that the latest values
    // are used in the `useEffect` closures below.
    // Using a Ref ensures that new values for the callbacks do not trigger the
    // useEffect blocks, and a new adapter creation / distruction is not triggered.
    afterCreateRef.current = afterCreate;
    beforeDisposeRef.current = beforeDispose;
    useEffect(() => {
        if (!credential || !displayName || !endpoint || !threadId || !userId) {
            return;
        }
        (() => __awaiter(void 0, void 0, void 0, function* () {
            if (adapterRef.current) {
                // Dispose the old adapter when a new one is created.
                //
                // This clean up function uses `adapterRef` because `adapter` can not be added to the dependency array of
                // this `useEffect` -- we do not want to trigger a new adapter creation because of the first adapter
                // creation.
                if (beforeDisposeRef.current) {
                    yield beforeDisposeRef.current(adapterRef.current);
                }
                adapterRef.current.dispose();
                adapterRef.current = undefined;
            }
            if (creatingAdapterRef.current) {
                console.warn('Adapter is already being created, please see storybook for more information: https://azure.github.io/communication-ui-library/?path=/story/troubleshooting--page');
                return;
            }
            creatingAdapterRef.current = true;
            let newAdapter = yield createAzureCommunicationChatAdapter({
                credential,
                displayName,
                endpoint,
                threadId,
                userId
            });
            if (afterCreateRef.current) {
                newAdapter = yield afterCreateRef.current(newAdapter);
            }
            adapterRef.current = newAdapter;
            creatingAdapterRef.current = false;
            setAdapter(newAdapter);
        }))();
    }, 
    // Explicitly list all arguments so that caller doesn't have to memoize the `args` object.
    [adapterRef, afterCreateRef, beforeDisposeRef, credential, displayName, endpoint, threadId, userId]);
    // Dispose any existing adapter when the component unmounts.
    useEffect(() => {
        return () => {
            (() => __awaiter(void 0, void 0, void 0, function* () {
                if (adapterRef.current) {
                    if (beforeDisposeRef.current) {
                        yield beforeDisposeRef.current(adapterRef.current);
                    }
                    adapterRef.current.dispose();
                    adapterRef.current = undefined;
                }
            }))();
        };
    }, []);
    return adapter;
};
/**
 * Create a {@link ChatAdapter} using the provided {@link StatefulChatClient}.
 *
 * Useful if you want to keep a reference to {@link StatefulChatClient}.
 * Consider using {@link createAzureCommunicationChatAdapter} for a simpler API.
 *
 * @public
 */
export function createAzureCommunicationChatAdapterFromClient(chatClient, chatThreadClient, chatAdapterOptions) {
    return __awaiter(this, void 0, void 0, function* () {
        return new AzureCommunicationChatAdapter(chatClient, chatThreadClient, chatAdapterOptions);
    });
}
const isChatError = (e) => {
    return 'target' in e && e['target'] !== undefined && 'innerError' in e && e['innerError'] !== undefined;
};
//# sourceMappingURL=AzureCommunicationChatAdapter.js.map