import React, { FC, useState, useEffect, useRef } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import classNames from 'classnames'

import { getOrigin } from 'src/utils'
import { useConfig, useDeepCompareEffect } from 'src/hooks'
import { fetchInstallations, setSelectedInstallation, setHeight as setComposerHeight, setError as setComposerError } from 'src/actions/composer'
import { showNotification } from 'src/actions/notifications'
import { createPost } from 'src/actions/posts'
import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from 'src/selectors/composer'
import { AppState, Installation, ClassDictionary, AppThunkDispatch, NotificationType, Post } from 'src/types'
import { IncomingMessageData, OutgoingMessageData } from 'src/types/communicator'

interface LimiterCollection {
    [key: string]: number
}

interface Props {
    parent?: Post
    onPost?: () => void
}

const Composer: FC<Props> = ({ parent, onPost }) => {
    const installations = useSelector<AppState, Installation[]>(getInstallations)
    const installation = useSelector<AppState, Installation | undefined>(getSelectedInstallation)
    const height = useSelector<AppState, number>(getComposerHeight)
    const error = useSelector<AppState, string | undefined>(getError)
    const config = useConfig()
    const dispatch = useDispatch<AppThunkDispatch>()
    const ref = useRef<HTMLIFrameElement>(null)
    const [limiters, setLimiters] = useState<LimiterCollection>({})

    const composerUrl = installation ? installation.app.composerUrl : undefined
    const showComposer = !!composerUrl && !error

    const composerClasses: ClassDictionary = {
        composer: true,
        'composer-empty': !showComposer,
    }

    useEffect(() => {
        dispatch(fetchInstallations())
    }, [])

    useDeepCompareEffect(() => {
        if (!composerUrl) return
        if (!installation) return
        if (error) return

        const listener = async (event: MessageEvent) => {
            const origin = getOrigin(composerUrl)
            if (event.origin !== origin) return

            const postMessage = (message: OutgoingMessageData) => {
                if (ref.current && ref.current.contentWindow) {
                    ref.current.contentWindow.postMessage(JSON.stringify(message), origin)
                }
            }

            const withRateLimit = async (fn: (data: IncomingMessageData) => Promise<void>, data: IncomingMessageData, ms: number = 2000) => {
                const last = limiters[data.name] || 0
        
                if ((Date.now() - last) > ms) {
                    await fn(data)
                    limiters[data.name] = Date.now()
                    setLimiters(limiters)
                } else {
                    postMessage({
                        name: data.name,
                        error: 'Rate limited.',
                    })
                }
            }

            let data: IncomingMessageData | undefined

            try {
                data = JSON.parse(event.data)
            } catch (err) {
                dispatch(setComposerError('Invalid payload'))
                return
            }

            if (!data) return

            if (data.publicKey !== installation.app.publicKey) {
                const message = 'Invalid publicKey'

                dispatch(setComposerError(message))

                postMessage({
                    name: data.name,
                    error: message,
                })
            }

            switch (data.name) {
                case 'init':
                    postMessage({
                        name: data.name,
                        content: {
                            installationId: installation.id,
                            settings: installation.settings,
                            parent: parent ? {
                                text: parent.text,
                                cover: parent.cover,
                                attachments: parent.attachments,
                                data: parent.data,
                                created: parent.created,
                            } : undefined,
                        },
                    })

                    break
                case 'setHeight':
                    const { height = 0 } = data.content
                    dispatch(setComposerHeight(Math.max(Math.min(height, 400), 100)))

                    postMessage({
                        name: data.name,
                        content: {},
                    })

                    break
                case 'post':
                    withRateLimit(async ({ name, content }) => {
                        try {
                            const postId = await dispatch(createPost({
                                visible: content.visible,
                                text: content.text,
                                cover: content.cover,
                                attachments: content.attachments,
                                data: content.data,
                                parent: parent ? parent.id : undefined,
                            }))
        
                            postMessage({
                                name,
                                content: {
                                    postId,
                                }
                            })

                            dispatch(showNotification(NotificationType.Success, `Posted!`))
                            if (onPost) onPost()
                        } catch (err) {
                            postMessage({
                                name,
                                error: err,
                            })

                            dispatch(showNotification(NotificationType.Error, `Error posting: ${err.message}`))
                        }
                    }, data, 2000)

                    break
            }
        }

        window.addEventListener('message', listener, false)

        return () => {
            window.removeEventListener('message', listener, false)
        }
    }, [installation, parent, error])

    const handleClick = (id: string) => {
        if (installation && installation.id === id) {
            dispatch(setSelectedInstallation())
            return
        }

        dispatch(setSelectedInstallation(id))
    }

    return (
        <div className="container composer-container">
            <div className={classNames(composerClasses)}>
                {showComposer &&
                    <iframe ref={ref} src={composerUrl} scrolling="yes" style={{ height, width: '100%', overflow: 'hidden' }} />
                }
                {error && <span className="has-text-danger">Composer Error: {error}</span>}
                {(!showComposer && !error) && <span>Choose an App.</span>}
            </div>

            <div className="installations is-flex">
                {installations.map(i => (
                    <div key={i.id} className={installation && installation.id === i.id ? 'selected' : ''} onClick={() => handleClick(i.id)}>
                        <img src={`${config.blobUrl}${i.app.iconImageUrl}`} alt={i.app.name} style={{ width: 32 }} />
                        <p className="is-size-7 has-text-weight-bold">{i.app.name}</p>
                    </div>
                ))}
            </div>
        </div>
    )
}

export default Composer
