// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later

import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Window 2.12
import Clipboard 0.1
import "../../.."
import "../../../Base"
import "../../../PythonBridge"
import "../../../ShortcutBundles"

Rectangle {

    readonly property alias eventList: eventList
    readonly property alias contextMenu: contextMenu

    color: theme.chat.eventList.background

    HShortcut {
        sequences: window.settings.Keys.Messages.unfocus_or_deselect
        onActivated: {
            eventList.selectedCount ?
            eventList.checked = {} :
            eventList.currentIndex = -1
        }
    }

    HShortcut {
        sequences: window.settings.Keys.Messages.previous
        onActivated: eventList.focusPreviousMessage()
    }

    HShortcut {
        sequences: window.settings.Keys.Messages.next
        onActivated: eventList.focusNextMessage()
    }

    HShortcut {
        active: eventList.currentItem
        sequences: window.settings.Keys.Messages.select
        onActivated: eventList.toggleCheck(eventList.currentIndex)
    }

    HShortcut {
        active: eventList.currentItem
        sequences: window.settings.Keys.Messages.select_until_here
        onActivated:
            eventList.checkFromLastToHere(eventList.currentIndex)
    }

    HShortcut {
        sequences: window.settings.Keys.Messages.seen_tooltips
        onActivated:
            eventList.showFocusedSeenTooltips =
            ! eventList.showFocusedSeenTooltips
    }

    HShortcut {
        readonly property var events:
            eventList.selectedCount ?
            eventList.redactableCheckedEvents :

            eventList.currentItem &&
            eventList.canRedact(eventList.currentItem.currentModel) ?
            [eventList.currentItem.currentModel] :

            eventList.currentItem ?
            [] :
            null

        function findLastRemovableDelegate() {
            for (let i = 0; i < eventList.model.count && i <= 1000; i++) {
                const event = eventList.model.get(i)
                if (eventList.canRedact(event) &&
                    mainUI.accountIds.includes(event.sender_id)) return [event]
            }
            return []
        }

        enabled: (events && events.length > 0) || events === null
        sequences: window.settings.Keys.Messages.remove
        onActivated: window.makePopup(
            "Popups/RedactPopup.qml",
            {
                preferUserId: chat.userId,
                roomId: chat.roomId,

                eventSenderAndIds:
                    (events || findLastRemovableDelegate()).map(
                        ev => [ev.sender_id, ev.id],
                    ),

                isLast: ! events,

                onlyOwnMessageWarning:
                    ! chat.roomInfo.can_redact_all &&
                    events &&
                    events.length < eventList.selectedCount
            }
        )
    }

    HShortcut {
        sequences: window.settings.Keys.Messages.reply
        onActivated: {
            let event = eventList.model.get(0)

            if (eventList.currentIndex !== -1) {
                event = eventList.model.get(eventList.currentIndex)
            } else if (eventList.selectedCount) {
                event = eventList.getSortedChecked.slice(-1)[0]
            } else {
                // Find most recent event that wasn't sent by us
                for (let i = 0; i < eventList.model.count && i <= 1000; i++) {
                    const potentialEvent = eventList.model.get(i)

                    if (potentialEvent.sender_id !== chat.userId) {
                        event = potentialEvent
                        break
                    }
                }
            }

            if (! event) return

            if (event.id === chat.replyToEventId) {
                chat.clearReplyTo()
                return
            }

            chat.replyToEventId     = event.id
            chat.replyToUserId      = event.sender_id
            chat.replyToDisplayName = event.sender_name
        }
    }

    HShortcut {
        sequences: window.settings.Keys.Messages.open_links_files
        onActivated: {
            const indice =
                eventList.getFocusedOrSelectedOrLastMediaEvents(true)

            for (const i of Array.from(indice).sort().reverse()) {
                const event = eventList.model.get(i)

                if (event.media_url || event.thumbnail_url) {
                    eventList.getMediaType(event) === Utils.Media.Image ?
                    eventList.openImageViewer(event) :
                    eventList.openMediaExternally(event)

                    continue
                }

                for (const url of JSON.parse(event.links)) {
                    utils.getLinkType(url) === Utils.Media.Image ?
                    eventList.openImageViewer(event, url) :
                    Qt.openUrlExternally(url)
                }
            }
        }
    }

    HShortcut {
        sequences: window.settings.Keys.Messages.open_links_files_externally
        onActivated: {
            const indice =
                eventList.getFocusedOrSelectedOrLastMediaEvents(true)

            for (const i of Array.from(indice).sort().reverse()) {
                const event = eventList.model.get(i)

                if (event.media_url) {
                    eventList.openMediaExternally(event)
                    continue
                }

                for (const url of JSON.parse(event.links))
                    Qt.openUrlExternally(url)
            }
        }
    }

    HShortcut {
        sequences: window.settings.Keys.Messages.copy_files_path
        onActivated: {
            const paths  = []
            const indice =
                eventList.getFocusedOrSelectedOrLastMediaEvents(false)

            for (const i of Array.from(indice).sort().reverse()) {
                const event = eventList.model.get(i)

                if (event.media_local_path)
                    paths.push(
                        event.media_local_path.replace(/^file:\/\//, ""),
                    )
            }

            if (paths.length > 0) Clipboard.text = paths.join("\n")
        }
    }

    HShortcut {
        active: eventList.currentItem
        sequences: window.settings.Keys.Messages.debug
        onActivated: mainUI.debugConsole.toggle(
            eventList.currentItem, ".j t.dict()",
        )
    }

    HShortcut {
        sequences: window.settings.Keys.Messages.clear_all
        onActivated: window.makePopup(
            "Popups/ClearMessagesPopup.qml",
            {
                userId: window.uiState.pageProperties.userRoomId[0],
                roomId: window.uiState.pageProperties.userRoomId[1],
                preClearCallback: eventList.uncheckAll,
            }
        )
    }

    FlickShortcuts {
        active: chat.composerHasFocus
        flickable: eventList
    }

    HListView {
        id: eventList

        property string updateMarkerFutureId: ""
        property string loadPastEventsFutureId: ""
        property bool moreToLoad: true

        property bool ownEventsOnLeft:
            window.settings.Chat.own_messages_on_left_above < 0 ?
            false :
            width >
            window.settings.Chat.own_messages_on_left_above * theme.uiScale

        property string delegateWithSelectedText: ""
        property string selectedText: ""

        property bool showFocusedSeenTooltips: false

        property alias cursorShape: cursorShapeArea.cursorShape

        readonly property bool shouldLoadPastEvents:
            ! chat.roomInfo.inviter_id &&
            ! chat.roomInfo.left &&
            moreToLoad &&
            visibleArea.yPosition < 0.1

        readonly property var thumbnailCachedPaths: ({})  // {event.id: path}

        readonly property var redactableCheckedEvents:
            getSortedChecked().filter(ev => eventList.canRedact(ev))

        readonly property alias contextMenu: contextMenu

        function focusCenterMessage() {
            const previous     = highlightRangeMode
            highlightRangeMode = HListView.NoHighlightRange
            currentIndex       = indexAt(0, contentY + height / 2)
            highlightRangeMode = previous
        }

        function focusPreviousVisibleMessage() {
            incrementCurrentIndex()
            let lastIndex = -1
            while ( currentIndex != lastIndex && model.get(currentIndex).hidden ) {
                lastIndex = currentIndex
                incrementCurrentIndex()
            }
        }

        function focusPreviousMessage() {
            currentIndex === -1 && visibleEnd.y < contentHeight - height / 4 ?
            focusCenterMessage() :
            focusPreviousVisibleMessage()
        }

        function focusNextVisibleMessage() {
            decrementCurrentIndex()
            while ( currentIndex > -1 && model.get(currentIndex).hidden ) {
                if ( currentIndex === 0 )
                    currentIndex = -1;
                decrementCurrentIndex()
            }
        }

        function focusNextMessage() {
            currentIndex === -1 && visibleEnd.y < contentHeight - height / 4 ?
            focusCenterMessage() :

            eventList.currentIndex === 0 ?
            eventList.currentIndex = -1 :

            focusNextVisibleMessage()
        }

        function copySelectedDelegates() {
            if (eventList.selectedText) {
                Clipboard.text = eventList.selectedText
                return
            }

            if (! eventList.selectedCount && eventList.currentIndex !== -1) {
                const model  = eventList.model.get(eventList.currentIndex)
                const source = JSON.parse(model.source)

                Clipboard.text =
                    model.media_http_url &&
                    utils.isEmptyObject(JSON.parse(model.media_crypt_dict)) ?
                    model.media_http_url :

                    "body" in source ?
                    source.body :

                    utils.stripHtmlTags(utils.processedEventText(model))

                return
            }

            const contents = []

            for (const model of eventList.getSortedChecked()) {
                const source = JSON.parse(model.source)

                contents.push(
                    model.media_http_url &&
                    utils.isEmptyObject(JSON.parse(model.media_crypt_dict)) ?
                    model.media_http_url :

                    "body" in source ?
                    source.body :

                    utils.stripHtmlTags(utils.processedEventText(model))
                )
            }

            Clipboard.text = contents.join("\n\n")
        }

        function canRedact(eventModel) {
            return eventModel.event_type !== "RedactedEvent" &&
                   (chat.roomInfo.can_redact_all ||
                    mainUI.accountIds.includes(eventModel.sender_id))
        }

        function canCombine(item, itemAfter) {
            if (! item || ! itemAfter || item.hidden) return false

            return Boolean(
                ! canTalkBreak(item, itemAfter) &&
                ! canDayBreak(item, itemAfter) &&
                item.sender_id === itemAfter.sender_id &&
                utils.minutesBetween(item.date, itemAfter.date) <= 5
            )
        }

        function canTalkBreak(item, itemAfter) {
            if (! item || ! itemAfter) return false

            return Boolean(
                ! canDayBreak(item, itemAfter) &&
                utils.minutesBetween(item.date, itemAfter.date) >= 20
            )
        }

        function canDayBreak(item, itemAfter) {
            if (itemAfter && itemAfter.event_type === "RoomCreateEvent")
                return true

            if (! item || ! itemAfter || ! item.date || ! itemAfter.date)
                return false

            return item.date.getDate() !== itemAfter.date.getDate()
        }

        function loadPastEvents() {
            loadPastEventsFutureId = py.callClientCoro(
                chat.userId,
                "load_past_events",
                [chat.roomId],
                more => {
                    moreToLoad             = more
                    loadPastEventsFutureId = ""
                }
            )
        }

        function getFocusedOrSelectedOrLastMediaEvents(acceptLinks=false) {
            if (eventList.selectedCount) return eventList.checkedIndice
            if (eventList.currentIndex !== -1) return [eventList.currentIndex]

            // Find most recent event that's a media or contains links
            for (let i = 0; i < eventList.model.count && i <= 1000; i++) {
                const ev    = eventList.model.get(i)
                const links = JSON.parse(ev.links)

                if (ev.media_url || (acceptLinks && links.length)) return [i]
            }
        }

        function getMediaType(event) {
            if (event.event_type === "RoomAvatarEvent")
                return Utils.Media.Image

            const mainType   = event.media_mime.split("/")[0].toLowerCase()
            const fileEvents = ["RoomMessageFile", "RoomEncryptedFile"]

            return (
                mainType === "image" ? Utils.Media.Image :
                mainType === "video" ? Utils.Media.Video :
                mainType === "audio" ? Utils.Media.Audio :
                fileEvents.includes(event.event_type) ? Utils.Media.File :
                null
            )
        }

        function isAnimated(event, forLink="") {
            const link = forLink || event.media_url

            return (
                event.media_mime === "image/gif" ||
                utils.urlExtension(link).toLowerCase() === "gif"
            )
        }

        function getThumbnailTitle(event) {
            return event.media_title.replace(
                /\.[^\.]+$/,
                event.thumbnail_mime === "image/jpeg"    ? ".jpg" :
                event.thumbnail_mime === "image/png"     ? ".png" :
                event.thumbnail_mime === "image/gif"     ? ".gif" :
                event.thumbnail_mime === "image/tiff"    ? ".tiff" :
                event.thumbnail_mime === "image/svg+xml" ? ".svg" :
                event.thumbnail_mime === "image/webp"    ? ".webp" :
                event.thumbnail_mime === "image/bmp"     ? ".bmp" :
                ".thumbnail"
            ) || utils.urlFileName(event.media_url)
        }

        function openImageViewer(event, forLink="", callback=null) {
            // if forLink is empty, this must be a media event

            const title =
                event.media_title ||
                utils.urlFileName(forLink || event.media_url)

            // The thumbnail/cached path will be the full GIF
            const fullMxc =
                forLink || (isAnimated(event, forLink) ? "" : event.media_url)

            window.makePopup(
                "Popups/ImageViewerPopup/ImageViewerPopup.qml",
                {
                    clientUserId: chat.userId,
                    thumbnailTitle: getThumbnailTitle(event),
                    thumbnailMxc: event.thumbnail_url,
                    thumbnailPath: eventList.thumbnailCachedPaths[event.id],
                    thumbnailCryptDict: JSON.parse(event.thumbnail_crypt_dict),

                    fullTitle: title,
                    fullMxc: fullMxc,
                    fullCryptDict: JSON.parse(event.media_crypt_dict),
                    fullFileSize: event.media_size,

                    overallSize: Qt.size(
                        event.media_width ||
                        event.thumbnail_width ||
                        implicitWidth || // XXX
                        800,

                        event.media_height ||
                        event.thumbnail_height ||
                        implicitHeight || // XXX
                        600,
                    )
                },
                obj => {
                    obj.openExternallyRequested.connect(() => {
                        forLink ?
                        Qt.openUrlExternally(forLink) :
                        eventList.openMediaExternally(event)
                    })

                    if (callback) callback(obj)
                },
            )
        }

        function getLocalOrDownloadMedia(event, callback) {
            if (event.media_local_path) {
                callback(event.media_local_path)
                return
            }

            const args = [
                chat.userId,
                event.media_url,
                event.media_title,
                chat.roomId,
                event.media_size,
                JSON.parse(event.media_crypt_dict),
            ]

            py.callCoro("media_cache.get_media", args, callback)
        }

        function openMediaExternally(event) {
            eventList.getLocalOrDownloadMedia(event, Qt.openUrlExternally)
        }

        anchors.fill: parent
        enabled: ! window.anyPopup
        clip: true
        keyNavigationWraps: false
        leftMargin: theme.spacing
        rightMargin: theme.spacing
        topMargin: theme.spacing
        bottomMargin: theme.spacing
        verticalLayoutDirection: ListView.BottomToTop

        model: ModelStore.get(chat.userRoomId[0], chat.userRoomId[1], "events")
        delegate: EventDelegate {}

        highlight: Rectangle {
            color: theme.chat.message.focusedHighlight
            opacity: theme.chat.message.focusedHighlightOpacity
        }

        // Since the list is BottomToTop, this is actually a header
        footer: Item {
            width: eventList.width
            height: (button.height + theme.spacing * 2) * opacity
            opacity: eventList.loadPastEventsFutureId ? 1 : 0
            visible: opacity > 0

            HButton {
                readonly property bool offline:
                    chat.userInfo.presence === "offline"

                id: button
                width: Math.min(
                    parent.width,implicitWidth + leftPadding + rightPadding,
                )
                anchors.centerIn: parent

                loading: parent.visible && ! offline
                icon.name: offline ? "feature-unavailable-offline" : ""
                icon.color:
                    offline ?
                    theme.colors.negativeBackground :
                    theme.icons.colorize
                text:
                    offline ?
                    qsTr("Cannot load history offline") :
                    qsTr("Loading previous messages...")

                enableRadius: true
                iconItem.small: true
            }
        }

        Timer {
            id: increaseBufferTimer
            interval: 1000
            running: true

            // Keep x scroll pages cached, to limit the amount of images having
            // to be reloaded from network. We delay increasing this to reduce
            // the lag when switching rooms and loading all the delegates.
            onTriggered:
                eventList.cacheBuffer = Screen.desktopAvailableHeight * 2
        }

        Timer {
            interval: 200
            running:
                eventList.shouldLoadPastEvents &&
                ! eventList.loadPastEventsFutureId
            triggeredOnStart: true
            onTriggered: eventList.loadPastEvents()

        }

        Component.onDestruction: {
            if (loadPastEventsFutureId) py.cancelCoro(loadPastEventsFutureId)
        }

        MouseArea {
            id: cursorShapeArea
            anchors.fill: parent
            acceptedButtons: Qt.NoButton
        }

        EventContextMenu {
            id: contextMenu
            eventList: eventList
        }

        Connections {
            target: pageLoader
            onRecycled: eventList.moreToLoad = true
        }
    }

    Timer {
        interval: Math.max(100, window.settings.Chat.mark_read_delay * 1000)

        running:
            ! eventList.updateMarkerFutureId &&
            (
                chat.roomInfo.unreads ||
                chat.roomInfo.highlights ||
                chat.roomInfo.local_unreads
            ) &&
            Qt.application.state === Qt.ApplicationActive &&
            eventList.visibleEnd.y > eventList.contentHeight - 100

        onTriggered: {
            for (let i = 0; i < eventList.model.count; i++) {
                const item = eventList.model.get(i)

                if (item.sender !== chat.userId) {
                    eventList.updateMarkerFutureId = py.callCoro(
                        "update_room_read_marker",
                        [chat.roomId, item.event_id],
                        () => { eventList.updateMarkerFutureId = "" },
                        () => { eventList.updateMarkerFutureId = "" },
                    )
                    return
                }
            }
        }
    }

    HNoticePage {
        text: qsTr("No messages to show yet")

        visible: eventList.model.count < 1
        anchors.fill: parent
    }
}
