import { throttle } from 'lodash-es';
import React, { RefObject } from 'react';
import { scrollIntoView } from 'seamless-scroll-polyfill';

import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
import AbstractMessageContainer, { IProps } from '../AbstractMessageContainer';

import ChatMessageGroup from './ChatMessageGroup';
import NewMessagesButton from './NewMessagesButton';

interface IState {

    /**
     * Whether or not message container has received new messages.
     */
    hasNewMessages: boolean;

    /**
     * Whether or not scroll position is at the bottom of container.
     */
    isScrolledToBottom: boolean;

    /**
     * The id of the last read message.
     */
    lastReadMessageId: string | null;
}

/**
 * Displays all received chat messages, grouped by sender.
 *
 * @augments AbstractMessageContainer
 */
export default class MessageContainer extends AbstractMessageContainer<IProps, IState> {
    /**
     * Component state used to decide when the hasNewMessages button to appear
     * and where to scroll when click on hasNewMessages button.
     */
    override state: IState = {
        hasNewMessages: false,
        isScrolledToBottom: true,
        lastReadMessageId: ''
    };

    /**
     * Reference to the HTML element at the end of the list of displayed chat
     * messages. Used for scrolling to the end of the chat messages.
     */
    _messagesListEndRef: RefObject<HTMLDivElement>;

    /**
     * A React ref to the HTML element containing all {@code ChatMessageGroup}
     * instances.
     */
    _messageListRef: RefObject<HTMLDivElement>;

    /**
     * Intersection observer used to detect intersections of messages with the bottom of the message container.
     */
    _bottomListObserver: IntersectionObserver;

    /**
     * Initializes a new {@code MessageContainer} instance.
     *
     * @param {IProps} props - The React {@code Component} props to initialize
     * the new {@code MessageContainer} instance with.
     */
    constructor(props: IProps) {
        super(props);

        this._messageListRef = React.createRef<HTMLDivElement>();
        this._messagesListEndRef = React.createRef<HTMLDivElement>();

        // Bind event handlers so they are only bound once for every instance.
        this._handleIntersectBottomList = this._handleIntersectBottomList.bind(this);
        this._findFirstUnreadMessage = this._findFirstUnreadMessage.bind(this);
        this._isMessageVisible = this._isMessageVisible.bind(this);
        this._onChatScroll = throttle(this._onChatScroll.bind(this), 300, { leading: true });
        this._onGoToFirstUnreadMessage = this._onGoToFirstUnreadMessage.bind(this);
    }

    /**
     * Implements {@code Component#render}.
     *
     * @inheritdoc
     */
    override render() {
        const groupedMessages = this._getMessagesGroupedBySender();
        const messages = groupedMessages.map((group, index) => {
            const messageType = group[0]?.messageType;

            return (
                <ChatMessageGroup
                    className = { messageType || MESSAGE_TYPE_REMOTE }
                    key = { index }
                    messages = { group } />
            );
        });

        return (
            <div id = 'chat-conversation-container'>
                <div
                    aria-labelledby = 'chat-header'
                    id = 'chatconversation'
                    onScroll = { this._onChatScroll }
                    ref = { this._messageListRef }
                    role = 'log'
                    tabIndex = { 0 }>
                    { messages }

                    { !this.state.isScrolledToBottom && this.state.hasNewMessages
                        && <NewMessagesButton
                            onGoToFirstUnreadMessage = { this._onGoToFirstUnreadMessage } /> }
                    <div
                        id = 'messagesListEnd'
                        ref = { this._messagesListEndRef } />
                </div>
            </div>
        );
    }


    /**
     * Implements {@code Component#componentDidMount}.
     * When Component mount scroll message container to bottom.
     * Create observer to react when scroll position is at bottom or leave the bottom.
     *
     * @inheritdoc
     */
    override componentDidMount() {
        this.scrollToElement(false, null);
        this._createBottomListObserver();
    }

    /**
     * Implements {@code Component#componentDidUpdate}.
     * If the user receive a new message or the local user send a new message,
     * scroll automatically to the bottom if scroll position was at the bottom.
     * Otherwise update hasNewMessages from component state.
     *
     * @inheritdoc
     * @returns {void}
     */
    override componentDidUpdate(prevProps: IProps) {
        const newMessages = this.props.messages.filter(message => !prevProps.messages.includes(message));
        const hasLocalMessage = newMessages.map(message => message.messageType).includes(MESSAGE_TYPE_LOCAL);

        if (newMessages.length > 0) {
            if (this.state.isScrolledToBottom || hasLocalMessage) {
                this.scrollToElement(false, null);
            } else {
                // eslint-disable-next-line react/no-did-update-set-state
                this.setState({ hasNewMessages: true });
            }
        }
    }

    /**
     * Implements React's {@link Component#componentWillUnmount()}. Invoked
     * immediately before this component is unmounted and destroyed.
     *
     * @inheritdoc
     */
    override componentWillUnmount() {
        const target = document.querySelector('#messagesListEnd');

        this._bottomListObserver.unobserve(target as Element);
    }

    /**
     * Automatically scrolls the displayed chat messages to bottom or to a specific element if it is provided.
     *
     * @param {boolean} withAnimation - Whether or not to show a scrolling.
     * @param {TMLElement} element - Where to scroll.
     * Animation.
     * @returns {void}
     */
    scrollToElement(withAnimation: boolean, element: Element | null) {
        const scrollTo = element ? element : this._messagesListEndRef.current;
        const block = element ? 'center' : 'nearest';

        scrollIntoView(scrollTo as Element, {
            behavior: withAnimation ? 'smooth' : 'auto',
            block
        });
    }


    /**
     * Callback invoked to listen to current scroll position and update next unread message.
     * The callback is invoked inside a throttle with 300 ms to decrease the number of function calls.
     *
     * @private
     * @returns {void}
     */
    _onChatScroll() {
        const firstUnreadMessage = this._findFirstUnreadMessage();

        if (firstUnreadMessage && firstUnreadMessage.id !== this.state.lastReadMessageId) {
            this.setState({ lastReadMessageId: firstUnreadMessage?.id });
        }
    }

    /**
     * Find the first unread message.
     * Update component state and scroll to element.
     *
     * @private
     * @returns {void}
     */
    _onGoToFirstUnreadMessage() {
        const firstUnreadMessage = this._findFirstUnreadMessage();

        this.setState({ lastReadMessageId: firstUnreadMessage?.id || null });
        this.scrollToElement(true, firstUnreadMessage as Element);
    }

    /**
    * Create observer to react when scroll position is at bottom or leave the bottom.
    *
    * @private
    * @returns {void}
    */
    _createBottomListObserver() {
        const options = {
            root: document.querySelector('#chatconversation'),
            rootMargin: '35px',
            threshold: 0.5
        };

        const target = document.querySelector('#messagesListEnd');

        if (target) {
            this._bottomListObserver = new IntersectionObserver(this._handleIntersectBottomList, options);
            this._bottomListObserver.observe(target);
        }
    }

    /** .
    * _HandleIntersectBottomList.
    * When entry is intersecting with bottom of container set last message as last read message.
    * When entry is not intersecting update only isScrolledToBottom with false value.
    *
    * @param {Array} entries - List of entries.
    * @private
    * @returns {void}
    */
    _handleIntersectBottomList(entries: IntersectionObserverEntry[]) {
        entries.forEach((entry: IntersectionObserverEntry) => {
            if (entry.isIntersecting && this.props.messages.length) {
                const lastMessageIndex = this.props.messages.length - 1;
                const lastMessage = this.props.messages[lastMessageIndex];
                const lastReadMessageId = lastMessage.messageId;

                this.setState(
                    {
                        isScrolledToBottom: true,
                        hasNewMessages: false,
                        lastReadMessageId
                    });
            }

            if (!entry.isIntersecting) {
                this.setState(
                    {
                        isScrolledToBottom: false
                    });
            }
        });
    }

    /**
    * Find first unread message.
    * MessageIsAfterLastSeenMessage filter elements which are not visible but are before the last read message.
    *
    * @private
    * @returns {Element}
    */
    _findFirstUnreadMessage() {
        const messagesNodeList = document.querySelectorAll('.chatmessage-wrapper');

        // @ts-ignore
        const messagesToArray = [ ...messagesNodeList ];

        const previousIndex = messagesToArray.findIndex((message: Element) =>
            message.id === this.state.lastReadMessageId);

        if (previousIndex !== -1) {
            for (let i = previousIndex; i < messagesToArray.length; i++) {
                if (!this._isMessageVisible(messagesToArray[i])) {
                    return messagesToArray[i];
                }
            }
        }
    }

    /**
     * Check if a message is visible in view.
     *
     * @param {Element} message - The message.
     *
     * @returns {boolean}
     */
    _isMessageVisible(message: Element): boolean {
        const { bottom, height, top } = message.getBoundingClientRect();

        if (this._messageListRef.current) {
            const containerRect = this._messageListRef.current.getBoundingClientRect();

            return top <= containerRect.top
                ? containerRect.top - top <= height : bottom - containerRect.bottom <= height;
        }

        return false;
    }
}
