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

import { getOrigin } from '../utils'
import { useDeepCompareEffect, useTheme } from '../hooks'
import {
    fetchInstallations,
    setSelectedInstallation,
    setHeight as setComposerHeight,
    setError as setComposerError,
    saveInstallationSettings,
} from '../actions/composer'
import { showNotification } from '../actions/notifications'
import { createPost } from '../actions/posts'
import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from '../selectors/composer'
import { getColorScheme } from '../selectors/theme'
import { AppThunkDispatch, NotificationType, Post } from '../types'
import { IncomingMessageData, OutgoingMessageData } from '../types/communicator'

interface LimiterCollection {
    [key: string]: number
}

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

const Composer: FC<Props> = ({ parent, onPost }) => {
    const theme = useTheme()
    const colorScheme = useSelector(getColorScheme)
    const installations = useSelector(getInstallations)
    const installation = useSelector(getSelectedInstallation)
    const height = useSelector(getComposerHeight)
    const error = useSelector(getError)
    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

    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,
                            theme,
                            colorScheme,
                            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 'saveSettings':
                    withRateLimit(async ({ name, content }) => {
                        try {
                            await dispatch(saveInstallationSettings(installation.id, content.settings))

                            postMessage({
                                name,
                                content: {
                                    settings: content.settings,
                                },
                            })
                        } catch (error) {
                            postMessage({
                                name,
                                error,
                            })

                            dispatch(showNotification(NotificationType.Error, `Error saving settings: ${error.message}`))
                        }
                    }, data, 2000)

                    break
                case 'post':
                    withRateLimit(async ({ name, content }) => {
                        try {
                            const postId = await dispatch(createPost({
                                installation: installation.id,
                                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 (error) {
                            postMessage({
                                name,
                                error,
                            })

                            dispatch(showNotification(NotificationType.Error, `Error posting: ${error.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="composer-container" style={{ borderColor: theme.backgroundSecondary }}>
            <div className="composer">
                {showComposer &&
                    <iframe ref={ref} src={composerUrl} scrolling="yes" style={{ height }} />
                }
                {error && <div className="composer-error" style={{ backgroundColor: theme.backgroundSecondary, color: theme.red }}>Composer Error: {error}</div>}
                {(!showComposer && !error) && <div className="composer-empty" style={{ backgroundColor: theme.backgroundSecondary, color: theme.secondary }}>Choose an App.</div>}
            </div>

            <div className="installations" style={{ backgroundColor: theme.backgroundPrimary }}>
                {installations.map(i => (
                    <div key={i.id} className={installation && installation.id === i.id ? 'selected' : ''} style={{ borderColor: theme.backgroundSecondary }} onClick={() => handleClick(i.id)}>
                        <img src={i.app.iconImageUrl} alt={i.app.name} style={{ width: 32 }} />
                        <p style={{ color: theme.text }}>{i.app.name}</p>
                    </div>
                ))}
            </div>
        </div>
    )
}

export default Composer
