// 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());
    });
};
import { EventEmitter } from 'events';
import { enableMapSet, enablePatches, produce } from 'immer';
import { ChatError } from './ChatClientState';
import { createClientLogger, getLogLevel } from '@azure/logger';
import { _safeJSONStringify, toFlatCommunicationIdentifier } from "../../acs-ui-common/src";
import { Constants } from './constants';
import { chatStatefulLogger } from './Logger';
import { ResourceDownloadQueue, fetchImageSource } from './ResourceDownloadQueue';
enableMapSet();
// Needed to generate state diff for verbose logging.
enablePatches();
/**
 * @internal
 */
export class ChatContext {
    constructor(maxListeners, credential, endpoint) {
        this._state = {
            userId: {
                id: ''
            },
            displayName: '',
            threads: {},
            latestErrors: {}
        };
        this._batchMode = false;
        this.typingIndicatorInterval = undefined;
        this._inlineImageQueue = undefined;
        this._fullsizeImageQueue = undefined;
        this._logger = createClientLogger('communication-react:chat-context');
        this._emitter = new EventEmitter();
        if (credential) {
            this._inlineImageQueue = new ResourceDownloadQueue(this, {
                credential,
                endpoint: endpoint !== null && endpoint !== void 0 ? endpoint : ''
            });
            this._fullsizeImageQueue = new ResourceDownloadQueue(this, {
                credential,
                endpoint: endpoint !== null && endpoint !== void 0 ? endpoint : ''
            });
        }
        if (maxListeners) {
            this._emitter.setMaxListeners(maxListeners);
        }
    }
    getState() {
        return this._state;
    }
    modifyState(modifier) {
        const priorState = this._state;
        this._state = produce(this._state, modifier, (patches) => {
            if (getLogLevel() === 'verbose') {
                // Log to `info` because AzureLogger.verbose() doesn't show up in console.
                this._logger.info(`State change: ${_safeJSONStringify(patches)}`);
            }
        });
        if (!this._batchMode && this._state !== priorState) {
            this._emitter.emit('stateChanged', this._state);
        }
    }
    dispose() {
        this.modifyState((draft) => {
            var _a, _b;
            (_a = this._inlineImageQueue) === null || _a === void 0 ? void 0 : _a.cancelAllRequests();
            (_b = this._fullsizeImageQueue) === null || _b === void 0 ? void 0 : _b.cancelAllRequests();
            Object.values(draft.threads).forEach(thread => {
                Object.values(thread.chatMessages).forEach(message => {
                    const cache = message.resourceCache;
                    if (cache) {
                        Object.values(cache).forEach(resource => {
                            if (resource.sourceUrl) {
                                URL.revokeObjectURL(resource.sourceUrl);
                            }
                        });
                    }
                    message.resourceCache = undefined;
                });
            });
        });
        // Any item in queue should be removed.
    }
    downloadResourceToCache(threadId, messageId, resourceUrl) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            let message = (_a = this.getState().threads[threadId]) === null || _a === void 0 ? void 0 : _a.chatMessages[messageId];
            if (message && this._fullsizeImageQueue) {
                if (!message.resourceCache) {
                    message = Object.assign(Object.assign({}, message), { resourceCache: {} });
                }
                // Need to discuss retry logic in case of failure
                this._fullsizeImageQueue.addMessage(message);
                yield this._fullsizeImageQueue.startQueue(threadId, fetchImageSource, {
                    singleUrl: resourceUrl
                });
            }
        });
    }
    removeResourceFromCache(threadId, messageId, resourceUrl) {
        this.modifyState((draft) => {
            var _a, _b, _c;
            const message = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.chatMessages[messageId];
            if (message && this._fullsizeImageQueue && this._fullsizeImageQueue.containsMessageWithSameAttachments(message)) {
                (_b = this._fullsizeImageQueue) === null || _b === void 0 ? void 0 : _b.cancelRequest(resourceUrl);
            }
            else if (message && this._inlineImageQueue && this._inlineImageQueue.containsMessageWithSameAttachments(message)) {
                (_c = this._inlineImageQueue) === null || _c === void 0 ? void 0 : _c.cancelRequest(resourceUrl);
            }
            if (message && message.resourceCache && message.resourceCache[resourceUrl]) {
                const resource = message.resourceCache[resourceUrl];
                if (resource === null || resource === void 0 ? void 0 : resource.sourceUrl) {
                    URL.revokeObjectURL(resource.sourceUrl);
                }
                delete message.resourceCache[resourceUrl];
            }
        });
    }
    setThread(threadId, threadState) {
        this.modifyState((draft) => {
            draft.threads[threadId] = threadState;
        });
    }
    createThread(threadId, properties) {
        this.modifyState((draft) => {
            draft.threads[threadId] = {
                chatMessages: {},
                threadId: threadId,
                properties: properties,
                participants: {},
                readReceipts: [],
                typingIndicators: [],
                latestReadTime: new Date(0)
            };
        });
    }
    updateChatConfig(userId, displayName) {
        this.modifyState((draft) => {
            draft.displayName = displayName;
            draft.userId = userId;
        });
    }
    createThreadIfNotExist(threadId, properties) {
        const exists = Object.prototype.hasOwnProperty.call(this.getState().threads, threadId);
        if (!exists) {
            this.createThread(threadId, properties);
            return true;
        }
        return false;
    }
    updateThread(threadId, properties) {
        this.modifyState((draft) => {
            const thread = draft.threads[threadId];
            if (thread) {
                thread.properties = properties;
            }
        });
    }
    updateThreadTopic(threadId, topic) {
        this.modifyState((draft) => {
            if (topic === undefined) {
                return;
            }
            const thread = draft.threads[threadId];
            if (thread && !thread.properties) {
                thread.properties = {
                    topic: topic
                };
            }
            else if (thread && thread.properties) {
                thread.properties.topic = topic;
            }
        });
    }
    deleteThread(threadId) {
        this.modifyState((draft) => {
            const thread = draft.threads[threadId];
            if (thread) {
                delete draft.threads[threadId];
            }
        });
    }
    setChatMessages(threadId, messages) {
        this.modifyState((draft) => {
            const threadState = draft.threads[threadId];
            if (threadState) {
                threadState.chatMessages = messages;
            }
            // remove typing indicator when receive messages
            const thread = draft.threads[threadId];
            if (thread) {
                for (const message of Object.values(messages)) {
                    this.filterTypingIndicatorForUser(thread, message.sender);
                }
            }
        });
    }
    updateChatMessageContent(threadId, messagesId, content) {
        this.modifyState((draft) => {
            var _a;
            const chatMessage = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.chatMessages[messagesId];
            if (chatMessage) {
                if (!chatMessage.content) {
                    chatMessage.content = {};
                }
                chatMessage.content.message = content;
            }
        });
    }
    deleteLocalMessage(threadId, localId) {
        let localMessageDeleted = false;
        this.modifyState((draft) => {
            var _a;
            const chatMessages = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.chatMessages;
            const message = chatMessages ? chatMessages[localId] : undefined;
            if (chatMessages && message && message.clientMessageId) {
                delete chatMessages[message.clientMessageId];
                localMessageDeleted = true;
            }
        });
        return localMessageDeleted;
    }
    deleteMessage(threadId, id) {
        this.modifyState((draft) => {
            var _a;
            const chatMessages = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.chatMessages;
            if (chatMessages) {
                delete chatMessages[id];
            }
        });
    }
    setParticipant(threadId, participant) {
        this.modifyState((draft) => {
            var _a;
            const participants = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.participants;
            if (participants) {
                participants[toFlatCommunicationIdentifier(participant.id)] = participant;
            }
        });
    }
    setParticipants(threadId, participants) {
        this.modifyState((draft) => {
            var _a;
            const participantsMap = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.participants;
            if (participantsMap) {
                for (const participant of participants) {
                    participantsMap[toFlatCommunicationIdentifier(participant.id)] = participant;
                }
            }
        });
    }
    deleteParticipants(threadId, participantIds) {
        this.modifyState((draft) => {
            var _a;
            const participants = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.participants;
            if (participants) {
                participantIds.forEach(id => {
                    delete participants[toFlatCommunicationIdentifier(id)];
                });
            }
        });
    }
    deleteParticipant(threadId, participantId) {
        this.modifyState((draft) => {
            var _a;
            const participants = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.participants;
            if (participants) {
                delete participants[toFlatCommunicationIdentifier(participantId)];
            }
        });
    }
    addReadReceipt(threadId, readReceipt) {
        this.modifyState((draft) => {
            const thread = draft.threads[threadId];
            const readReceipts = thread === null || thread === void 0 ? void 0 : thread.readReceipts;
            if (thread && readReceipts) {
                // TODO(prprabhu): Replace `this.getState()` with `draft`?
                if (readReceipt.sender !== this.getState().userId && thread.latestReadTime < readReceipt.readOn) {
                    thread.latestReadTime = readReceipt.readOn;
                }
                readReceipts.push(readReceipt);
            }
        });
    }
    startTypingIndicatorCleanUp() {
        if (this.typingIndicatorInterval) {
            return;
        }
        this.typingIndicatorInterval = window.setInterval(() => {
            let isTypingActive = false;
            this.modifyState((draft) => {
                for (const thread of Object.values(draft.threads)) {
                    const filteredTypingIndicators = thread.typingIndicators.filter(typingIndicator => {
                        const timeGap = Date.now() - typingIndicator.receivedOn.getTime();
                        return timeGap < Constants.TYPING_INDICATOR_MAINTAIN_TIME;
                    });
                    if (thread.typingIndicators.length !== filteredTypingIndicators.length) {
                        thread.typingIndicators = filteredTypingIndicators;
                    }
                    if (thread.typingIndicators.length > 0) {
                        isTypingActive = true;
                    }
                }
            });
            if (!isTypingActive && this.typingIndicatorInterval) {
                window.clearInterval(this.typingIndicatorInterval);
                this.typingIndicatorInterval = undefined;
            }
        }, 1000);
    }
    addTypingIndicator(threadId, typingIndicator) {
        this.modifyState((draft) => {
            const thread = draft.threads[threadId];
            if (thread) {
                const typingIndicators = thread.typingIndicators;
                typingIndicators.push(typingIndicator);
            }
        });
        // Make sure we only maintain a period of typing indicator for perf purposes
        this.startTypingIndicatorCleanUp();
    }
    setChatMessage(threadId, message) {
        this.parseAttachments(threadId, message);
        const { id: messageId, clientMessageId } = message;
        if (messageId || clientMessageId) {
            this.modifyState((draft) => {
                var _a;
                const threadMessages = (_a = draft.threads[threadId]) === null || _a === void 0 ? void 0 : _a.chatMessages;
                const isLocalIdInMap = threadMessages && clientMessageId && threadMessages[clientMessageId];
                const messageKey = !messageId || isLocalIdInMap ? clientMessageId : messageId;
                if (threadMessages && messageKey) {
                    threadMessages[messageKey] = message;
                }
                // remove typing indicator when receive a message from a user
                const thread = draft.threads[threadId];
                if (thread) {
                    this.filterTypingIndicatorForUser(thread, message.sender);
                }
            });
        }
    }
    parseAttachments(threadId, message) {
        var _a;
        const attachments = (_a = message.content) === null || _a === void 0 ? void 0 : _a.attachments;
        if (message.type === 'html' && attachments && attachments.length > 0) {
            if (this._inlineImageQueue && !this._inlineImageQueue.containsMessageWithSameAttachments(message) && message.resourceCache === undefined) {
                // Need to discuss retry logic in case of failure
                this._inlineImageQueue.addMessage(message);
                this._inlineImageQueue.startQueue(threadId, fetchImageSource);
            }
        }
    }
    /**
     * Tees any errors encountered in an async function to the state.
     *
     * @param f Async function to execute.
     * @param target The error target to tee error to.
     * @returns Result of calling `f`. Also re-raises any exceptions thrown from `f`.
     * @throws ChatError. Exceptions thrown from `f` are tagged with the failed `target.
     */
    withAsyncErrorTeedToState(f, target) {
        return (...args) => __awaiter(this, void 0, void 0, function* () {
            try {
                return yield f(...args);
            }
            catch (error) {
                const chatError = toChatError(target, error);
                this.setLatestError(target, chatError);
                throw chatError;
            }
        });
    }
    /**
     * Tees any errors encountered in an function to the state.
     *
     * @param f Function to execute.
     * @param target The error target to tee error to.
     * @returns Result of calling `f`. Also re-raises any exceptions thrown from `f`.
     * @throws ChatError. Exceptions thrown from `f` are tagged with the failed `target.
     */
    withErrorTeedToState(f, target) {
        return (...args) => {
            try {
                chatStatefulLogger.info(`Chat stateful client target function called: ${target}`);
                return f(...args);
            }
            catch (error) {
                const chatError = toChatError(target, error);
                this.setLatestError(target, chatError);
                throw chatError;
            }
        };
    }
    setLatestError(target, error) {
        this.modifyState((draft) => {
            draft.latestErrors[target] = error;
        });
    }
    // This is a mutating function, only use it inside of a produce() function
    filterTypingIndicatorForUser(thread, userId) {
        if (!userId) {
            return;
        }
        const typingIndicators = thread.typingIndicators;
        const userIdAsKey = toFlatCommunicationIdentifier(userId);
        const filteredTypingIndicators = typingIndicators.filter(typingIndicator => toFlatCommunicationIdentifier(typingIndicator.sender) !== userIdAsKey);
        if (filteredTypingIndicators.length !== typingIndicators.length) {
            thread.typingIndicators = filteredTypingIndicators;
        }
    }
    /**
     * Batch updates to minimize `stateChanged` events across related operations.
     *
     * - A maximum of one `stateChanged` event is emitted, at the end of the operations.
     * - No `stateChanged` event is emitted if the state did not change through the operations.
     * - In case of an exception, state is reset to the prior value and no `stateChanged` event is emitted.
     *
     * All operations finished in this batch should be synchronous.
     * This function is not reentrant -- do not call batch() from within another batch().
     */
    batch(operations) {
        if (this._batchMode) {
            throw new Error('batch() called from within another batch()');
        }
        this._batchMode = true;
        const priorState = this._state;
        try {
            operations();
            if (this._state !== priorState) {
                this._emitter.emit('stateChanged', this._state);
            }
        }
        catch (e) {
            this._state = priorState;
            if (getLogLevel() === 'verbose') {
                this._logger.warning(`State rollback to: ${_safeJSONStringify(priorState)}`);
            }
            throw e;
        }
        finally {
            this._batchMode = false;
        }
    }
    onStateChange(handler) {
        this._emitter.on('stateChanged', handler);
    }
    offStateChange(handler) {
        this._emitter.off('stateChanged', handler);
    }
}
const toChatError = (target, error) => {
    if (error instanceof Error) {
        return new ChatError(target, error);
    }
    return new ChatError(target, new Error(`${error}`));
};
//# sourceMappingURL=ChatContext.js.map