package chat.rocket.android.chatroom.viewmodel

import DateTimeHelper
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import chat.rocket.android.R
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.*
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import okhttp3.HttpUrl
import java.security.InvalidParameterException
import javax.inject.Inject

class ViewModelMapper @Inject constructor(private val context: Context,
                                          private val parser: MessageParser,
                                          private val messagesRepository: MessagesRepository,
                                          tokenRepository: TokenRepository,
                                          localRepository: LocalRepository,
                                          serverInteractor: GetCurrentServerInteractor,
                                          getSettingsInteractor: GetSettingsInteractor) {

    private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
    private val baseUrl = settings.baseUrl()
    private val currentUsername: String? = localRepository.get(LocalRepository.USERNAME_KEY)
    private val token = tokenRepository.get()

    suspend fun map(message: Message): List<BaseViewModel<*>> {
        return translate(message)
    }

    suspend fun map(messages: List<Message>): List<BaseViewModel<*>> = withContext(CommonPool) {
        val list = ArrayList<BaseViewModel<*>>(messages.size)

        messages.forEach {
            list.addAll(translate(it))
        }

        return@withContext list
    }

    private suspend fun translate(message: Message): List<BaseViewModel<*>> = withContext(CommonPool) {
        val list = ArrayList<BaseViewModel<*>>()

        message.urls?.forEach {
            val url = mapUrl(message, it)
            url?.let { list.add(url) }
        }

        message.attachments?.forEach {
            val attachment = mapAttachment(message, it)
            attachment?.let { list.add(attachment) }
        }

        mapMessage(message).let {
            list.add(it)
        }

        for (i in list.size - 1 downTo 0) {
            val next = if (i - 1 < 0) null else list[i - 1]
            list[i].nextDownStreamMessage = next
        }

        return@withContext list
    }

    private fun mapUrl(message: Message, url: Url): BaseViewModel<*>? {
        if (url.ignoreParse || url.meta == null) return null

        val hostname = url.parsedUrl?.hostname ?: ""
        val thumb = url.meta?.imageUrl
        val title = url.meta?.title
        val description = url.meta?.description

        return UrlPreviewViewModel(message, url, message.id, title, hostname, description, thumb,
                getReactions(message))
    }

    private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? {
        return when (attachment) {
            is FileAttachment -> mapFileAttachment(message, attachment)
            is MessageAttachment -> mapMessageAttachment(message, attachment)
            else -> null
        }
    }

    private fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentViewModel {
        val attachmentAuthor = attachment.author!!
        val time = getTime(attachment.timestamp!!)
        val attachmentText = when (attachment.attachments.orEmpty().firstOrNull()) {
            is ImageAttachment -> context.getString(R.string.msg_quote_photo)
            is VideoAttachment -> context.getString(R.string.msg_quote_video)
            is AudioAttachment -> context.getString(R.string.msg_quote_audio)
            else -> attachment.text ?: ""
        }

        return MessageAttachmentViewModel(message = getMessageWithoutQuoteMarkdown(message), rawData = message,
                messageId = message.id, time = time, senderName = attachmentAuthor,
                content = attachmentText, isPinned = message.pinned, reactions = getReactions(message))
    }

    private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? {
        val attachmentUrl = attachmentUrl(attachment)
        val attachmentTitle = attachmentTitle(attachment)
        val id = attachmentId(message, attachment)
        return when (attachment) {
            is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
                    attachmentUrl, attachmentTitle, id, getReactions(message))
            is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
                    attachmentUrl, attachmentTitle, id, getReactions(message))
            is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
                    attachmentUrl, attachmentTitle, id, getReactions(message))
            else -> null
        }
    }

    private fun attachmentId(message: Message, attachment: FileAttachment): Long {
        return "${message.id}_${attachment.url}".hashCode().toLong()
    }

    private fun attachmentTitle(attachment: FileAttachment): CharSequence {
        return with(attachment) {
            title?.let { return@with it }

            val fileUrl = HttpUrl.parse(url)
            fileUrl?.let {
                return@with it.pathSegments().last()
            }

            return@with ""
        }
    }

    private fun attachmentUrl(attachment: FileAttachment): String {
        return with(attachment) {
            if (url.startsWith("http")) return@with url

            val fullUrl = "$baseUrl$url"
            val httpUrl = HttpUrl.parse(fullUrl)
            httpUrl?.let {
                return@with it.newBuilder().apply {
                    addQueryParameter("rc_uid", token?.userId)
                    addQueryParameter("rc_token", token?.authToken)
                }.build().toString()
            }

            // Fallback to baseUrl + url
            return@with fullUrl
        }
    }

    private suspend fun mapMessage(message: Message): MessageViewModel = withContext(CommonPool) {
        val sender = getSenderName(message)
        val time = getTime(message.timestamp)
        val avatar = getUserAvatar(message)

        val content = getContent(context, getMessageWithoutQuoteMarkdown(message))
        MessageViewModel(message = getMessageWithoutQuoteMarkdown(message), rawData = message,
                messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
                content = content, isPinned = message.pinned, reactions = getReactions(message),
                isFirstUnread = false)
    }

    private fun getReactions(message: Message): List<ReactionViewModel> {
        val reactions = message.reactions?.let {
            val list = mutableListOf<ReactionViewModel>()
            it.getShortNames().forEach { shortname ->
                val usernames = it.getUsernames(shortname) ?: emptyList()
                val count = usernames.size
                list.add(
                        ReactionViewModel(messageId = message.id,
                                shortname = shortname,
                                unicode = EmojiParser.parse(shortname),
                                count = count,
                                usernames = usernames)
                )
            }
            list
        }
        return reactions ?: emptyList()
    }

    private fun getMessageWithoutQuoteMarkdown(message: Message): Message {
        val baseUrl = settings.baseUrl()
        return message.copy(
                message = message.message.replace("\\[\\s\\]\\($baseUrl.*\\)".toRegex(), "").trim()
        )
    }

    private fun getSenderName(message: Message): CharSequence {
        if (!message.senderAlias.isNullOrEmpty()) {
            return message.senderAlias!!
        }

        val username = message.sender?.username
        val realName = message.sender?.name
        val senderName = if (settings.useRealName()) realName else username
        return senderName ?: username.toString()
    }

    private fun getUserAvatar(message: Message): String? {
        message.avatar?.let {
            return it // Always give preference for overridden avatar from message
        }

        val username = message.sender?.username ?: "?"
        return baseUrl?.let {
            UrlHelper.getAvatarUrl(baseUrl, username)
        }
    }

    private fun getTime(timestamp: Long) = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(timestamp))

    private suspend fun getContent(context: Context, message: Message): CharSequence {
        return when (message.isSystemMessage()) {
            true -> getSystemMessage(message, context)
            false -> parser.renderMarkdown(message, currentUsername)
        }
    }

    private fun getSystemMessage(message: Message, context: Context): CharSequence {
        val content = when (message.type) {
        //TODO: Add implementation for Welcome type.
            is MessageType.MessageRemoved -> context.getString(R.string.message_removed)
            is MessageType.UserJoined -> context.getString(R.string.message_user_joined_channel)
            is MessageType.UserLeft -> context.getString(R.string.message_user_left)
            is MessageType.UserAdded -> context.getString(R.string.message_user_added_by, message.message, message.sender?.username)
            is MessageType.RoomNameChanged -> context.getString(R.string.message_room_name_changed, message.message, message.sender?.username)
            is MessageType.UserRemoved -> context.getString(R.string.message_user_removed_by, message.message, message.sender?.username)
            is MessageType.MessagePinned -> context.getString(R.string.message_pinned)
            else -> {
                throw InvalidParameterException("Invalid message type: ${message.type}")
            }
        }
        val spannableMsg = SpannableStringBuilder(content)
        spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
                0)
        spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length,
                0)

        return spannableMsg
    }
}