package chat.rocket.android.chatroom.ui

import android.app.Activity
import android.app.AlertDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.text.SpannableStringBuilder
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.text.bold
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.adapter.CommandSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.bottomsheet.MessageActionsBottomSheet
import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.draw.main.ui.DRAWING_BYTE_ARRAY_EXTRA_DATA
import chat.rocket.android.draw.main.ui.DrawingActivity
import chat.rocket.android.emoji.ComposerEditText
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiKeyboardListener
import chat.rocket.android.emoji.EmojiKeyboardPopup
import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiPickerPopup
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.server.domain.AnalyticsTrackingInteractor
import chat.rocket.android.util.extension.asObservable
import chat.rocket.android.util.extensions.circularRevealOrUnreveal
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.hideKeyboard
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.helper.analytics.AnalyticsManager
import chat.rocket.android.util.helper.analytics.event.ScreenViewEvent
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject

fun newInstance(
    chatRoomId: String,
    chatRoomName: String,
    chatRoomType: String,
    isReadOnly: Boolean,
    chatRoomLastSeen: Long,
    isSubscribed: Boolean = true,
    isCreator: Boolean = false,
    isFavorite: Boolean = false,
    chatRoomMessage: String? = null
): Fragment {
    return ChatRoomFragment().apply {
        arguments = Bundle(1).apply {
            putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
            putString(BUNDLE_CHAT_ROOM_NAME, chatRoomName)
            putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
            putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isReadOnly)
            putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
            putBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED, isSubscribed)
            putBoolean(BUNDLE_CHAT_ROOM_IS_CREATOR, isCreator)
            putBoolean(BUNDLE_CHAT_ROOM_IS_FAVORITE, isFavorite)
            putString(BUNDLE_CHAT_ROOM_MESSAGE, chatRoomMessage)
        }
    }
}

internal const val TAG_CHAT_ROOM_FRAGMENT = "ChatRoomFragment"

private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_NAME = "chat_room_name"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
private const val REQUEST_CODE_FOR_DRAW = 101
private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed"
private const val BUNDLE_CHAT_ROOM_IS_CREATOR = "chat_room_is_creator"
private const val BUNDLE_CHAT_ROOM_IS_FAVORITE = "chat_room_is_favorite"
private const val BUNDLE_CHAT_ROOM_MESSAGE = "chat_room_message"

internal const val MENU_ACTION_FAVORITE_UNFAVORITE_CHAT = 1
internal const val MENU_ACTION_MEMBER = 2
internal const val MENU_ACTION_MENTIONS = 3
internal const val MENU_ACTION_PINNED_MESSAGES = 4
internal const val MENU_ACTION_FAVORITE_MESSAGES = 5
internal const val MENU_ACTION_FILES = 6

class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener,
    ChatRoomAdapter.OnActionSelected {
    @Inject
    lateinit var presenter: ChatRoomPresenter
    @Inject
    lateinit var parser: MessageParser
    @Inject
    lateinit var analyticsTrackingInteractor: AnalyticsTrackingInteractor
    private lateinit var adapter: ChatRoomAdapter
    internal lateinit var chatRoomId: String
    private lateinit var chatRoomName: String
    internal lateinit var chatRoomType: String
    private var newMessageCount: Int = 0
    private var chatRoomMessage: String? = null
    private var isSubscribed: Boolean = true
    private var isReadOnly: Boolean = false
    private var isCreator: Boolean = false
    internal var isFavorite: Boolean = false
    private var isBroadcastChannel: Boolean = false
    private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
    private var chatRoomLastSeen: Long = -1
    private lateinit var actionSnackbar: ActionSnackbar
    internal var citation: String? = null
    private var editingMessageId: String? = null
    internal var disableMenu: Boolean = false

    private val compositeDisposable = CompositeDisposable()
    private var playComposeMessageButtonsAnimation = true

    internal var isSearchTermQueried = false

    // For reveal and unreveal anim.
    private val hypotenuse by lazy {
        Math.hypot(
            root_layout.width.toDouble(),
            root_layout.height.toDouble()
        ).toFloat()
    }
    private val max by lazy {
        Math.max(
            layout_message_attachment_options.width.toDouble(),
            layout_message_attachment_options.height.toDouble()
        ).toFloat()
    }
    private val centerX by lazy { recycler_view.right }
    private val centerY by lazy { recycler_view.bottom }
    private val handler = Handler()
    private var verticalScrollOffset = AtomicInteger(0)

    private val dialogView by lazy { View.inflate(context, R.layout.file_attachments_dialog, null) }
    internal val alertDialog by lazy { AlertDialog.Builder(activity).setView(dialogView).create() }
    internal val imagePreview by lazy { dialogView.findViewById<ImageView>(R.id.image_preview) }
    internal val sendButton by lazy { dialogView.findViewById<Button>(R.id.button_send) }
    internal val cancelButton by lazy { dialogView.findViewById<Button>(R.id.button_cancel) }
    internal val description by lazy { dialogView.findViewById<EditText>(R.id.text_file_description) }
    internal val audioVideoAttachment by lazy { dialogView.findViewById<FrameLayout>(R.id.audio_video_attachment) }
    internal val textFile by lazy { dialogView.findViewById<TextView>(R.id.text_file_name) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        AndroidSupportInjection.inject(this)
        setHasOptionsMenu(true)

        val bundle = arguments
        if (bundle != null) {
            chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
            chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME)
            chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
            isReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY)
            isSubscribed = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED)
            chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN)
            isCreator = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_CREATOR)
            isFavorite = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_FAVORITE)
            chatRoomMessage = bundle.getString(BUNDLE_CHAT_ROOM_MESSAGE)
        } else {
            requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
        }

        adapter = ChatRoomAdapter(chatRoomType, chatRoomName, this,
                reactionListener = this)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return container?.inflate(R.layout.fragment_chat_room)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupToolbar(chatRoomName)

        presenter.setupChatRoom(chatRoomId, chatRoomName, chatRoomType, chatRoomMessage)
        presenter.loadChatRooms()
        setupRecyclerView()
        setupFab()
        setupSuggestionsView()
        setupActionSnackbar()
        (activity as ChatRoomActivity).let {
            it.showToolbarTitle(chatRoomName)
            it.showToolbarChatRoomIcon(chatRoomType)
        }

        if (analyticsTrackingInteractor.get()) {
            AnalyticsManager.logScreenView(ScreenViewEvent.ChatRoom)
        }
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        text_message.addTextChangedListener(EmojiKeyboardPopup.EmojiTextWatcher(text_message))
    }

    override fun onDestroyView() {
        recycler_view.removeOnScrollListener(endlessRecyclerViewScrollListener)
        recycler_view.removeOnScrollListener(onScrollListener)
        recycler_view.removeOnLayoutChangeListener(layoutChangeListener)

        presenter.disconnect()
        handler.removeCallbacksAndMessages(null)
        unsubscribeComposeTextMessage()

        // Hides the keyboard (if it's opened) before going to any view.
        activity?.apply {
            hideKeyboard()
        }
        super.onDestroyView()
    }

    override fun onPause() {
        super.onPause()
        setReactionButtonIcon(R.drawable.ic_reaction_24dp)
        dismissEmojiKeyboard()
        activity?.invalidateOptionsMenu()
    }

    private fun dismissEmojiKeyboard() {
        // Check if the keyboard was ever initialized.
        // It may be the case when you are looking a not joined room
        if (::emojiKeyboardPopup.isInitialized) {
            emojiKeyboardPopup.dismiss()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        if (resultData != null && resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_FOR_PERFORM_SAF -> showFileAttachmentDialog(resultData.data)
                REQUEST_CODE_FOR_DRAW -> showDrawAttachmentDialog(
                    resultData.getByteArrayExtra(DRAWING_BYTE_ARRAY_EXTRA_DATA)
                )
            }
        }
    }

    override fun onPrepareOptionsMenu(menu: Menu) {
        menu.clear()
        setupMenu(menu)
        super.onPrepareOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        setOnMenuItemClickListener(item)
        return true
    }

    override fun showFavoriteIcon(isFavorite: Boolean) {
        this.isFavorite = isFavorite
        activity?.invalidateOptionsMenu()
    }

    override fun showMessages(dataSet: List<BaseUiModel<*>>, clearDataSet: Boolean) {
        ui {
            if (clearDataSet) {
                adapter.clearData()
            }

            if (dataSet.isNotEmpty()) {
                var prevMsgModel = dataSet[0]

                // track the message sent immediately after the current message
                var prevMessageUiModel: MessageUiModel? = null

                // Checking for all messages to assign true to the required showDayMaker
                // Loop over received messages to determine first unread
                var firstUnread = false
                for (i in dataSet.indices) {
                    val msgModel = dataSet[i]

                    if (i > 0) {
                        prevMsgModel = dataSet[i - 1]
                    }

                    val currentDayMarkerText = msgModel.currentDayMarkerText
                    val previousDayMarkerText = prevMsgModel.currentDayMarkerText
                    println("$previousDayMarkerText then $currentDayMarkerText")
                    if (previousDayMarkerText != currentDayMarkerText) {
                        prevMsgModel.showDayMarker = true
                    }

                    if (!firstUnread && msgModel is MessageUiModel) {
                        val msg = msgModel.rawData
                        if (msg.timestamp < chatRoomLastSeen) {
                            // This message was sent before the last seen of the room. Hence, it was seen.
                            // if there is a message after (below) this, mark it firstUnread.
                            if (prevMessageUiModel != null) {
                                prevMessageUiModel.isFirstUnread = true
                            }
                            // Found first unread message.
                            firstUnread = true
                        }
                        prevMessageUiModel = msgModel
                    }
                }
            }

            if (recycler_view.adapter == null) {
                recycler_view.adapter = adapter
                if (dataSet.size >= 30) {
                    recycler_view.addOnScrollListener(endlessRecyclerViewScrollListener)
                }
                recycler_view.addOnLayoutChangeListener(layoutChangeListener)
                recycler_view.addOnScrollListener(onScrollListener)

                // Load just once, on the first page...
                presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
            }

            val oldMessagesCount = adapter.itemCount
            adapter.appendData(dataSet)
            if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
                recycler_view.scrollToPosition(0)
                verticalScrollOffset.set(0)
            }
            presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
            empty_chat_view.isVisible = adapter.itemCount == 0
        }
    }

    override fun showSearchedMessages(dataSet: List<BaseUiModel<*>>) {
        recycler_view.removeOnScrollListener(endlessRecyclerViewScrollListener)
        adapter.clearData()
        adapter.prependData(dataSet)
        empty_chat_view.isVisible = adapter.itemCount == 0
    }

    override fun onRoomUpdated(
        userCanPost: Boolean,
        channelIsBroadcast: Boolean,
        userCanMod: Boolean
    ) {
        // TODO: We should rely solely on the user being able to post, but we cannot guarantee
        // that the "(channels|groups).roles" endpoint is supported by the server in use.
        ui {
            setupMessageComposer(userCanPost)
            isBroadcastChannel = channelIsBroadcast
            if (isBroadcastChannel && !userCanMod) {
                disableMenu = true
                activity?.invalidateOptionsMenu()
            }
        }
    }

    override fun openDirectMessage(chatRoom: ChatRoom, permalink: String) {

    }

    private val layoutChangeListener =
        View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
            val y = oldBottom - bottom
            if (Math.abs(y) > 0 && isAdded) {
                // if y is positive the keyboard is up else it's down
                recycler_view.post {
                    if (y > 0 || Math.abs(verticalScrollOffset.get()) >= Math.abs(y)) {
                        ui { recycler_view.scrollBy(0, y) }
                    } else {
                        ui { recycler_view.scrollBy(0, verticalScrollOffset.get()) }
                    }
                }
            }
        }

    private lateinit var endlessRecyclerViewScrollListener: EndlessRecyclerViewScrollListener

    private val onScrollListener = object : RecyclerView.OnScrollListener() {
        var state = AtomicInteger(RecyclerView.SCROLL_STATE_IDLE)

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            state.compareAndSet(RecyclerView.SCROLL_STATE_IDLE, newState)
            when (newState) {
                RecyclerView.SCROLL_STATE_IDLE -> {
                    if (!state.compareAndSet(RecyclerView.SCROLL_STATE_SETTLING, newState)) {
                        state.compareAndSet(RecyclerView.SCROLL_STATE_DRAGGING, newState)
                    }
                }
                RecyclerView.SCROLL_STATE_DRAGGING -> {
                    state.compareAndSet(RecyclerView.SCROLL_STATE_IDLE, newState)
                }
                RecyclerView.SCROLL_STATE_SETTLING -> {
                    state.compareAndSet(RecyclerView.SCROLL_STATE_DRAGGING, newState)
                }
            }
        }

        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            if (state.get() != RecyclerView.SCROLL_STATE_IDLE) {
                verticalScrollOffset.getAndAdd(dy)
            }
        }
    }

    private val fabScrollListener = object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            if (!recyclerView.canScrollVertically(1)) {
                text_count.isVisible = false
                button_fab.hide()
                newMessageCount = 0
            } else {
                if (dy < 0 && !button_fab.isVisible) {
                    button_fab.show()
                    if (newMessageCount != 0) text_count.isVisible = true
                }
            }
        }
    }

    override fun sendMessage(text: String) {
        ui {
            if (!text.isBlank()) {
                if (!text.startsWith("/")) {
                    presenter.sendMessage(chatRoomId, text, editingMessageId)
                } else {
                    presenter.runCommand(text, chatRoomId)
                }
            }
        }
    }

    override fun showTypingStatus(usernameList: List<String>) {
        ui {
            when (usernameList.size) {
                1 -> {
                    text_typing_status.text =
                        SpannableStringBuilder()
                            .bold { append(usernameList[0]) }
                            .append(getString(R.string.msg_is_typing))
                }
                2 -> {
                    text_typing_status.text =
                        SpannableStringBuilder()
                            .bold { append(usernameList[0]) }
                            .append(getString(R.string.msg_and))
                            .bold { append(usernameList[1]) }
                            .append(getString(R.string.msg_are_typing))
                }
                else -> {
                    text_typing_status.text = getString(R.string.msg_several_users_are_typing)
                }
            }
            text_typing_status.isVisible = true
        }
    }

    override fun hideTypingStatusView() {
        ui {
            text_typing_status.isVisible = false
        }
    }

    override fun showInvalidFileMessage() {
        showMessage(getString(R.string.msg_invalid_file))
    }

    override fun showNewMessage(message: List<BaseUiModel<*>>, isMessageReceived: Boolean) {
        ui {
            adapter.prependData(message)
            if (isMessageReceived && button_fab.isVisible) {
                newMessageCount++

                if (newMessageCount <= 99)
                    text_count.text = newMessageCount.toString()
                else
                    text_count.text = "99+"

                text_count.isVisible = true
            } else if (!button_fab.isVisible)
                recycler_view.scrollToPosition(0)
            verticalScrollOffset.set(0)
            empty_chat_view.isVisible = adapter.itemCount == 0
        }
    }

    override fun disableSendMessageButton() {
        ui {
            button_send.isEnabled = false
        }
    }

    override fun enableSendMessageButton() {
        ui {
            button_send.isEnabled = true
            text_message.isEnabled = true
            clearMessageComposition(true)
        }
    }


    override fun clearMessageComposition(deleteMessage: Boolean) {
        ui {
            citation = null
            editingMessageId = null
            if (deleteMessage) {
                text_message.textContent = ""
            }
            actionSnackbar.dismiss()
        }
    }

    override fun dispatchUpdateMessage(index: Int, message: List<BaseUiModel<*>>) {
        ui {
            adapter.updateItem(message.last())
            if (message.size > 1) {
                adapter.prependData(listOf(message.first()))
            }
        }
    }

    override fun dispatchDeleteMessage(msgId: String) {
        ui {
            adapter.removeItem(msgId)
        }
    }

    override fun showReplyingAction(
        username: String,
        replyMarkdown: String,
        quotedMessage: String
    ) {
        ui {
            citation = replyMarkdown
            actionSnackbar.title = username
            actionSnackbar.text = quotedMessage
            actionSnackbar.show()
            KeyboardHelper.showSoftKeyboard(text_message)
        }
    }

    override fun showLoading() {
        ui { view_loading.isVisible = true }
    }

    override fun hideLoading() {
        ui { view_loading.isVisible = false }
    }

    override fun showMessage(message: String) {
        ui {
            showToast(message)
        }
    }

    override fun showMessage(resId: Int) {
        ui {
            showToast(resId)
        }
    }

    override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))

    override fun populatePeopleSuggestions(members: List<PeopleSuggestionUiModel>) {
        ui {
            suggestions_view.addItems("@", members)
        }
    }

    override fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionUiModel>) {
        ui {
            suggestions_view.addItems("#", chatRooms)
        }
    }

    override fun populateCommandSuggestions(commands: List<CommandSuggestionUiModel>) {
        ui {
            suggestions_view.addItems("/", commands)
        }
    }

    override fun copyToClipboard(message: String) {
        ui {
            val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
            clipboard.primaryClip = ClipData.newPlainText("", message)
            showToast(R.string.msg_message_copied)
        }
    }

    override fun showEditingAction(roomId: String, messageId: String, text: String) {
        ui {
            actionSnackbar.title = getString(R.string.action_title_editing)
            actionSnackbar.text = text
            actionSnackbar.show()
            text_message.textContent = text
            editingMessageId = messageId
            KeyboardHelper.showSoftKeyboard(text_message)
        }
    }

    override fun onEmojiAdded(emoji: Emoji) {
        val cursorPosition = text_message.selectionStart
        if (cursorPosition > -1) {
            text_message.text?.insert(cursorPosition, EmojiParser.parse(emoji.shortname))
            text_message.setSelection(cursorPosition + emoji.unicode.length)
        }
    }

    override fun onNonEmojiKeyPressed(keyCode: Int) {
        when (keyCode) {
            KeyEvent.KEYCODE_BACK -> with(text_message) {
                if (selectionStart > 0) text?.delete(selectionStart - 1, selectionStart)
            }
            else -> throw IllegalArgumentException("pressed key not expected")
        }
    }

    override fun onReactionTouched(messageId: String, emojiShortname: String) {
        presenter.react(messageId, emojiShortname)
    }

    override fun onReactionAdded(messageId: String, emoji: Emoji) {
        presenter.react(messageId, emoji.shortname)
    }

    override fun showReactionsPopup(messageId: String) {
        ui {
            val emojiPickerPopup = EmojiPickerPopup(it)
            emojiPickerPopup.listener = object : EmojiKeyboardListener {
                override fun onEmojiAdded(emoji: Emoji) {
                    onReactionAdded(messageId, emoji)
                }
            }
            emojiPickerPopup.show()
        }
    }

    private fun setReactionButtonIcon(@DrawableRes drawableId: Int) {
        button_add_reaction.setImageResource(drawableId)
        button_add_reaction.tag = drawableId
    }

    override fun showFileSelection(filter: Array<String>?) {
        ui {
            val intent = Intent(Intent.ACTION_GET_CONTENT)

            // Must set a type otherwise the intent won't resolve
            intent.type = "*/*"
            intent.addCategory(Intent.CATEGORY_OPENABLE)

            // Filter selectable files to those that match the whitelist for this particular server
            if (filter != null) {
                intent.putExtra(Intent.EXTRA_MIME_TYPES, filter)
            }
            startActivityForResult(intent, REQUEST_CODE_FOR_PERFORM_SAF)
        }
    }

    override fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) {
        showMessage(getString(R.string.max_file_size_exceeded, fileSize, maxFileSize))
    }

    override fun showConnectionState(state: State) {
        ui {
            text_connection_status.fadeIn()
            handler.removeCallbacks(dismissStatus)
            when (state) {
                is State.Connected -> {
                    text_connection_status.text = getString(R.string.status_connected)
                    handler.postDelayed(dismissStatus, 2000)
                }
                is State.Disconnected -> text_connection_status.text =
                    getString(R.string.status_disconnected)
                is State.Connecting -> text_connection_status.text =
                    getString(R.string.status_connecting)
                is State.Authenticating -> text_connection_status.text =
                    getString(R.string.status_authenticating)
                is State.Disconnecting -> text_connection_status.text =
                    getString(R.string.status_disconnecting)
                is State.Waiting -> text_connection_status.text =
                    getString(R.string.status_waiting, state.seconds)
            }
        }
    }

    override fun onJoined(userCanPost: Boolean) {
        ui {
            input_container.isVisible = true
            button_join_chat.isVisible = false
            isSubscribed = true
            setupMessageComposer(userCanPost)
        }
    }

    private val dismissStatus = {
        text_connection_status.fadeOut()
    }

    private fun setupRecyclerView() {
        // Initialize the endlessRecyclerViewScrollListener so we don't NPE at onDestroyView
        val linearLayoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
        linearLayoutManager.stackFromEnd = true
        recycler_view.layoutManager = linearLayoutManager
        recycler_view.itemAnimator = DefaultItemAnimator()
        endlessRecyclerViewScrollListener = object :
            EndlessRecyclerViewScrollListener(recycler_view.layoutManager as LinearLayoutManager) {
            override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView) {
                presenter.loadMessages(chatRoomId, chatRoomType, page * 30L)
            }
        }
        recycler_view.addOnScrollListener(fabScrollListener)
    }

    private fun setupFab() {
        button_fab.setOnClickListener {
            recycler_view.scrollToPosition(0)
            verticalScrollOffset.set(0)
            text_count.isVisible = false
            button_fab.hide()
            newMessageCount = 0
        }
    }

    private fun setupMessageComposer(canPost: Boolean) {
        if (isReadOnly && !canPost) {
            text_room_is_read_only.isVisible = true
            input_container.isVisible = false
        } else if (!isSubscribed && roomTypeOf(chatRoomType) !is RoomType.DirectMessage) {
            input_container.isVisible = false
            button_join_chat.isVisible = true
            button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) }
        } else {
            button_send.isVisible = false
            button_show_attachment_options.alpha = 1f
            button_show_attachment_options.isVisible = true
            activity?.supportFragmentManager?.addOnBackStackChangedListener {
                println("attach")
            }
            activity?.supportFragmentManager?.registerFragmentLifecycleCallbacks(
                object : FragmentManager.FragmentLifecycleCallbacks() {
                    override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
                        if (f is MessageActionsBottomSheet) {
                            setReactionButtonIcon(R.drawable.ic_reaction_24dp)
                            emojiKeyboardPopup.dismiss()
                        }
                    }
                },
                true
            )
            subscribeComposeTextMessage()
            emojiKeyboardPopup =
                EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
            emojiKeyboardPopup.listener = this
            text_message.listener = object : ComposerEditText.ComposerEditTextListener {
                override fun onKeyboardOpened() {
                }

                override fun onKeyboardClosed() {
                    activity?.let {
                        if (!emojiKeyboardPopup.isKeyboardOpen) {
                            it.onBackPressed()
                        }
                        KeyboardHelper.hideSoftKeyboard(it)
                        emojiKeyboardPopup.dismiss()
                    }
                    setReactionButtonIcon(R.drawable.ic_reaction_24dp)
                }
            }

            button_send.setOnClickListener {
                var textMessage = citation ?: ""
                textMessage += text_message.textContent
                sendMessage(textMessage)
                clearMessageComposition(true)
            }

            button_show_attachment_options.setOnClickListener {
                if (layout_message_attachment_options.isShown) {
                    hideAttachmentOptions()
                } else {
                    showAttachmentOptions()
                }
            }

            view_dim.setOnClickListener {
                hideAttachmentOptions()
            }

            button_files.setOnClickListener {
                handler.postDelayed({
                    presenter.selectFile()
                }, 200)

                handler.postDelayed({
                    hideAttachmentOptions()
                }, 400)
            }

            button_add_reaction.setOnClickListener { view ->
                openEmojiKeyboardPopup()
            }

            button_drawing.setOnClickListener {
                activity?.let {
                    if (!ImageHelper.canWriteToExternalStorage(it)) {
                        ImageHelper.checkWritingPermission(it)
                    } else {
                        val intent = Intent(it, DrawingActivity::class.java)
                        startActivityForResult(intent, REQUEST_CODE_FOR_DRAW)
                    }
                }

                handler.postDelayed({
                    hideAttachmentOptions()
                }, 400)
            }
        }
    }

    private fun setupSuggestionsView() {
        suggestions_view.anchorTo(text_message)
            .setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height))
            .addTokenAdapter(PeopleSuggestionsAdapter(context!!))
            .addTokenAdapter(CommandSuggestionsAdapter())
            .addTokenAdapter(RoomSuggestionsAdapter())
            .addSuggestionProviderAction("@") { query ->
                if (query.isNotEmpty()) {
                    presenter.spotlight(query, PEOPLE, true)
                }
            }
            .addSuggestionProviderAction("#") { query ->
                if (query.isNotEmpty()) {
                    presenter.loadChatRooms()
                }
            }
            .addSuggestionProviderAction("/") { _ ->
                presenter.loadCommands()
            }

        presenter.loadCommands()
    }

    private fun openEmojiKeyboardPopup() {
        if (!emojiKeyboardPopup.isShowing) {
            // If keyboard is visible, simply show the  popup
            if (emojiKeyboardPopup.isKeyboardOpen) {
                emojiKeyboardPopup.showAtBottom()
            } else {
                // Open the text keyboard first and immediately after that show the emoji popup
                text_message.isFocusableInTouchMode = true
                text_message.requestFocus()
                emojiKeyboardPopup.showAtBottomPending()
                KeyboardHelper.showSoftKeyboard(text_message)
            }
            setReactionButtonIcon(R.drawable.ic_keyboard_black_24dp)
        } else {
            // If popup is showing, simply dismiss it to show the underlying text keyboard
            emojiKeyboardPopup.dismiss()
            setReactionButtonIcon(R.drawable.ic_reaction_24dp)
        }
    }

    private fun setupActionSnackbar() {
        actionSnackbar = ActionSnackbar.make(message_list_container, parser = parser)
        actionSnackbar.cancelView.setOnClickListener {
            clearMessageComposition(false)
            if (text_message.textContent.isEmpty()) {
                KeyboardHelper.showSoftKeyboard(text_message)
            }
        }
    }

    private fun subscribeComposeTextMessage() {
        val editTextObservable = text_message.asObservable()

        compositeDisposable.addAll(
            subscribeComposeButtons(editTextObservable),
            subscribeComposeTypingStatus(editTextObservable)
        )
    }

    private fun unsubscribeComposeTextMessage() {
        compositeDisposable.clear()
    }

    private fun subscribeComposeButtons(observable: Observable<CharSequence>): Disposable {
        return observable.subscribe { t -> setupComposeButtons(t) }
    }

    private fun subscribeComposeTypingStatus(observable: Observable<CharSequence>): Disposable {
        return observable.debounce(300, TimeUnit.MILLISECONDS)
            .skip(1)
            .subscribe { t -> sendTypingStatus(t) }
    }

    private fun setupComposeButtons(charSequence: CharSequence) {
        if (charSequence.isNotEmpty() && playComposeMessageButtonsAnimation) {
            button_show_attachment_options.isVisible = false
            button_send.isVisible = true
            playComposeMessageButtonsAnimation = false
        }

        if (charSequence.isEmpty()) {
            button_send.isVisible = false
            button_show_attachment_options.isVisible = true
            playComposeMessageButtonsAnimation = true
        }
    }

    private fun sendTypingStatus(charSequence: CharSequence) {
        if (charSequence.isNotBlank()) {
            presenter.sendTyping()
        } else {
            presenter.sendNotTyping()
        }
    }

    private fun showAttachmentOptions() {
        view_dim.isVisible = true

        // Play anim.
        button_show_attachment_options.rotateBy(45F)
        layout_message_attachment_options.circularRevealOrUnreveal(centerX, centerY, 0F, hypotenuse)
    }

    private fun hideAttachmentOptions() {
        // Play anim.
        button_show_attachment_options.rotateBy(-45F)
        layout_message_attachment_options.circularRevealOrUnreveal(centerX, centerY, max, 0F)

        view_dim.isVisible = false
    }

    private fun setupToolbar(toolbarTitle: String) {
        (activity as ChatRoomActivity).showToolbarTitle(toolbarTitle)
    }

    override fun showMessageInfo(id: String) {
        presenter.messageInfo(id)
    }

    override fun citeMessage(roomName: String, roomType: String, messageId: String, mentionAuthor: Boolean) {
        presenter.citeMessage(roomName, roomType, messageId, mentionAuthor)
    }

    override fun copyMessage(id: String) {
        presenter.copyMessage(id)
    }

    override fun editMessage(roomId: String, messageId: String, text: String) {
        presenter.editMessage(roomId, messageId, text)
    }

    override fun toogleStar(id: String, star: Boolean) {
        if (star) {
            presenter.starMessage(id)
        } else {
            presenter.unstarMessage(id)
        }
    }

    override fun tooglePin(id: String, pin: Boolean) {
        if (pin) {
            presenter.pinMessage(id)
        } else {
            presenter.unpinMessage(id)
        }
    }

    override fun deleteMessage(roomId: String, id: String) {
        ui {
            val builder = AlertDialog.Builder(it)
            builder.setTitle(it.getString(R.string.msg_delete_message))
                    .setMessage(it.getString(R.string.msg_delete_description))
                    .setPositiveButton(it.getString(R.string.msg_ok)) { _, _ -> presenter.deleteMessage(roomId, id) }
                    .setNegativeButton(it.getString(R.string.msg_cancel)) { _, _ ->  }
                    .show()
        }
    }

    override fun showReactions(id: String) {
        presenter.showReactions(id)
    }

    override fun openDirectMessage(roomName: String, message: String) {
        presenter.openDirectMessage(roomName, message)
    }
}
