# Copyright (C) 2025 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from functools import cache

from PySide6.QtMultimedia import (QAudioBufferOutput, QAudioDevice, QAudioOutput, QMediaDevices,
                                  QMediaFormat, QMediaMetaData, QMediaPlayer)
from PySide6.QtWidgets import (QApplication, QComboBox, QDialog, QFileDialog, QGridLayout,
                               QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton,
                               QSizePolicy, QSlider, QVBoxLayout, QWidget)
from PySide6.QtGui import QCursor, QPixmap
from PySide6.QtCore import QDir, QLocale, QStandardPaths, QTime, Qt, Signal, Slot

from audiolevelmeter import AudioLevelMeter
from playercontrols import PlayerControls
from videowidget import VideoWidget


MP4 = 'video/mp4'


@cache
def getSupportedMimeTypes():
    result = []
    for f in QMediaFormat().supportedFileFormats(QMediaFormat.ConversionMode.Decode):
        mime_type = QMediaFormat(f).mimeType()
        result.append(mime_type.name())
    if MP4 not in result:
        result.append(MP4)  # Should always be there when using FFMPEG
    return result


class Player(QWidget):

    fullScreenChanged = Signal(bool)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_statusInfo = ""
        self.m_mediaDevices = QMediaDevices()
        self.m_player = QMediaPlayer(self)
        self.m_audioOutput = QAudioOutput(self)
        self.m_player.setAudioOutput(self.m_audioOutput)
        self.m_player.durationChanged.connect(self.durationChanged)
        self.m_player.positionChanged.connect(self.positionChanged)
        self.m_player.metaDataChanged.connect(self.metaDataChanged)
        self.m_player.mediaStatusChanged.connect(self.statusChanged)
        self.m_player.bufferProgressChanged.connect(self.bufferingProgress)
        self.m_player.hasVideoChanged.connect(self.videoAvailableChanged)
        self.m_player.errorChanged.connect(self.displayErrorMessage)
        self.m_player.tracksChanged.connect(self.tracksChanged)

        self.m_videoWidget = VideoWidget(self)
        available_geometry = self.screen().availableGeometry()
        self.m_videoWidget.setMinimumSize(available_geometry.width() / 2,
                                          available_geometry.height() / 3)
        self.m_player.setVideoOutput(self.m_videoWidget)

        # audio level meter
        self.m_audioBufferOutput = QAudioBufferOutput(self)
        self.m_player.setAudioBufferOutput(self.m_audioBufferOutput)
        self.m_audioLevelMeter = AudioLevelMeter(self)
        self.m_audioBufferOutput.audioBufferReceived.connect(self.m_audioLevelMeter.onAudioBufferReceived)  # noqa: E501
        self.m_player.playingChanged.connect(self.m_audioLevelMeter.deactivate)

        # player layout
        layout = QVBoxLayout(self)

        # display
        displayLayout = QHBoxLayout()
        displayLayout.addWidget(self.m_videoWidget, 2)
        displayLayout.addWidget(self.m_audioLevelMeter, 3)
        layout.addLayout(displayLayout)

        # duration slider and label
        hLayout = QHBoxLayout()

        self.m_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.m_slider.setRange(0, self.m_player.duration())
        self.m_slider.sliderMoved.connect(self.seek)
        hLayout.addWidget(self.m_slider)

        self.m_labelDuration = QLabel()
        self.m_labelDuration.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
        hLayout.addWidget(self.m_labelDuration)
        layout.addLayout(hLayout)

        # controls
        controlLayout = QHBoxLayout()
        controlLayout.setContentsMargins(0, 0, 0, 0)

        openButton = QPushButton("Open", self)
        openButton.clicked.connect(self.open)
        controlLayout.addWidget(openButton)
        controlLayout.addStretch(1)

        controls = PlayerControls()
        controls.setState(self.m_player.playbackState())
        controls.setVolume(self.m_audioOutput.volume())
        controls.setMuted(controls.isMuted())

        controls.play.connect(self.m_player.play)
        controls.pause.connect(self.m_player.pause)
        controls.stop.connect(self.m_player.stop)
        controls.previous.connect(self.previousClicked)
        controls.changeVolume.connect(self.m_audioOutput.setVolume)
        controls.changeMuting.connect(self.m_audioOutput.setMuted)
        controls.changeRate.connect(self.m_player.setPlaybackRate)
        controls.stop.connect(self.m_videoWidget.update)

        self.m_player.playbackStateChanged.connect(controls.setState)
        self.m_audioOutput.volumeChanged.connect(controls.setVolume)
        self.m_audioOutput.mutedChanged.connect(controls.setMuted)

        controlLayout.addWidget(controls)
        controlLayout.addStretch(1)

        self.m_fullScreenButton = QPushButton("FullScreen", self)
        self.m_fullScreenButton.setCheckable(True)
        controlLayout.addWidget(self.m_fullScreenButton)

        self.m_pitchCompensationButton = QPushButton("Pitch compensation", self)
        self.m_pitchCompensationButton.setCheckable(True)
        toolTip = ""
        match self.m_player.pitchCompensationAvailability():
            case QMediaPlayer.PitchCompensationAvailability.AlwaysOn:
                self.m_pitchCompensationButton.setEnabled(False)
                self.m_pitchCompensationButton.setChecked(True)
                toolTip = "Pitch compensation always enabled on self backend"
            case QMediaPlayer.PitchCompensationAvailability.Unavailable:
                self.m_pitchCompensationButton.setEnabled(False)
                self.m_pitchCompensationButton.setChecked(False)
                toolTip = "Pitch compensation unavailable on self backend"
            case QMediaPlayer.PitchCompensationAvailability.Available:
                self.m_pitchCompensationButton.setEnabled(True)
                self.m_pitchCompensationButton.setChecked(self.m_player.pitchCompensation())
        self.m_pitchCompensationButton.setToolTip(toolTip)

        controlLayout.addWidget(self.m_pitchCompensationButton)
        self.m_player.pitchCompensationChanged.connect(self._updatePitchCompensation)
        self.m_pitchCompensationButton.setChecked(self.m_player.pitchCompensation())
        self.m_pitchCompensationButton.toggled.connect(self.m_player.setPitchCompensation)

        self.m_audioOutputCombo = QComboBox(self)
        controlLayout.addWidget(self.m_audioOutputCombo)

        self.updateAudioDevices()

        self.m_audioOutputCombo.activated.connect(self.audioOutputChanged)

        self.m_mediaDevices.audioOutputsChanged.connect(self.updateAudioDevices)

        layout.addLayout(controlLayout)

        # tracks
        tracksLayout = QGridLayout()

        self.m_audioTracks = QComboBox(self)
        self.m_audioTracks.activated.connect(self.selectAudioStream)
        tracksLayout.addWidget(QLabel("Audio Tracks:"), 0, 0)
        tracksLayout.addWidget(self.m_audioTracks, 0, 1)

        self.m_videoTracks = QComboBox(self)
        self.m_videoTracks.activated.connect(self.selectVideoStream)
        tracksLayout.addWidget(QLabel("Video Tracks:"), 1, 0)
        tracksLayout.addWidget(self.m_videoTracks, 1, 1)

        self.m_subtitleTracks = QComboBox(self)
        self.m_subtitleTracks.activated.connect(self.selectSubtitleStream)
        tracksLayout.addWidget(QLabel("Subtitle Tracks:"), 2, 0)
        tracksLayout.addWidget(self.m_subtitleTracks, 2, 1)

        layout.addLayout(tracksLayout)

        # metadata
        metaDataLabel = QLabel("Metadata for file:")
        layout.addWidget(metaDataLabel)

        metaDataLayout = QGridLayout()
        metaDataCount = QMediaMetaData.NumMetaData
        self.m_metaDataLabels = [None] * metaDataCount
        self.m_metaDataFields = [None] * metaDataCount
        key = QMediaMetaData.Key.Title.value
        for i in range(0, round((metaDataCount + 2) / 3)):
            for j in range(0, 6, 2):
                labelText = QMediaMetaData.metaDataKeyToString(QMediaMetaData.Key(key))
                self.m_metaDataLabels[key] = QLabel(labelText)
                if (key == QMediaMetaData.Key.ThumbnailImage
                        or key == QMediaMetaData.Key.CoverArtImage):
                    self.m_metaDataFields[key] = QLabel()
                else:
                    lineEdit = QLineEdit()
                    lineEdit.setReadOnly(True)
                    self.m_metaDataFields[key] = lineEdit

                self.m_metaDataLabels[key].setDisabled(True)
                self.m_metaDataFields[key].setDisabled(True)
                metaDataLayout.addWidget(self.m_metaDataLabels[key], i, j)
                metaDataLayout.addWidget(self.m_metaDataFields[key], i, j + 1)
                key += 1
                if key == QMediaMetaData.NumMetaData:
                    break

        layout.addLayout(metaDataLayout)

        if not self.isPlayerAvailable():
            QMessageBox.warning(self, "Service not available",
                                "The QMediaPlayer object does not have a valid service.\n"
                                "Please check the media service plugins are installed.")

            controls.setEnabled(False)
            openButton.setEnabled(False)
            self.m_fullScreenButton.setEnabled(False)
        self.metaDataChanged()

    def closeEvent(self, event):
        self.m_audioLevelMeter.closeRequest()
        event.accept()

    @Slot()
    def _updatePitchCompensation(self):
        self.m_pitchCompensationButton.setChecked(self.m_player.pitchCompensation())

    def isPlayerAvailable(self):
        return self.m_player.isAvailable()

    @Slot()
    def open(self):
        fileDialog = QFileDialog(self)
        fileDialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen)
        fileDialog.setWindowTitle("Open Files")
        fileDialog.setMimeTypeFilters(getSupportedMimeTypes())
        fileDialog.selectMimeTypeFilter(MP4)
        movieDirs = QStandardPaths.standardLocations(QStandardPaths.StandardLocation.MoviesLocation)
        fileDialog.setDirectory(movieDirs[0] if movieDirs else QDir.homePath())
        if fileDialog.exec() == QDialog.DialogCode.Accepted:
            self.openUrl(fileDialog.selectedUrls()[0])

    def openUrl(self, url):
        self.m_player.setSource(url)

    @Slot("qlonglong")
    def durationChanged(self, duration):
        self.m_duration = duration / 1000
        self.m_slider.setMaximum(duration)

    @Slot("qlonglong")
    def positionChanged(self, progress):
        if not self.m_slider.isSliderDown():
            self.m_slider.setValue(progress)
        self.updateDurationInfo(progress / 1000)

    @Slot()
    def metaDataChanged(self):
        metaData = self.m_player.metaData()
        artist = metaData.value(QMediaMetaData.Key.AlbumArtist)
        title = metaData.value(QMediaMetaData.Key.Title)
        trackInfo = QApplication.applicationName()
        if artist and title:
            trackInfo = f"{artist} - {title}"
        elif artist:
            trackInfo = artist
        elif title:
            trackInfo = title
        self.setTrackInfo(trackInfo)

        for i in range(0, QMediaMetaData.NumMetaData):
            field = self.m_metaDataFields[i]
            if isinstance(field, QLineEdit):
                field.clear()
            elif isinstance(field, QLabel):
                field.clear()
            self.m_metaDataFields[i].setDisabled(True)
            self.m_metaDataLabels[i].setDisabled(True)

        for key in metaData.keys():
            i = key.value
            field = self.m_metaDataFields[i]
            if key == QMediaMetaData.Key.CoverArtImage or key == QMediaMetaData.Key.ThumbnailImage:
                if isinstance(field, QLabel):
                    field.setPixmap(QPixmap.fromImage(metaData.value(key)))
            elif isinstance(field, QLineEdit):
                field.setText(metaData.stringValue(key))

            self.m_metaDataFields[i].setDisabled(False)
            self.m_metaDataLabels[i].setDisabled(False)

        tracks = self.m_player.videoTracks()
        currentVideoTrack = self.m_player.activeVideoTrack()
        if currentVideoTrack >= 0 and currentVideoTrack < len(tracks):
            track = tracks[currentVideoTrack]
            trackKeys = track.keys()
            for key in trackKeys:
                i = key.value
                field = self.m_metaDataFields[i]
                if isinstance(field, QLineEdit):
                    stringValue = track.stringValue(key)
                    field.setText(stringValue)
                self.m_metaDataFields[i].setDisabled(True)
                self.m_metaDataLabels[i].setDisabled(True)

    def trackName(self, metaData, index):
        name = ""
        title = metaData.stringValue(QMediaMetaData.Key.Title)
        lang = metaData.value(QMediaMetaData.Key.Language)
        if not title:
            if lang == QLocale.Language.AnyLanguage:
                name = f"Track {index + 1}"
            else:
                name = QLocale.languageToString(lang)
        else:
            if lang == QLocale.Language.AnyLanguage:
                name = title
            else:
                langName = QLocale.languageToString(lang)
                name = f"{title} - [{langName}]"
        return name

    @Slot()
    def tracksChanged(self):
        self.m_audioTracks.clear()
        self.m_videoTracks.clear()
        self.m_subtitleTracks.clear()

        audioTracks = self.m_player.audioTracks()
        self.m_audioTracks.addItem("No audio", -1)
        for i in range(0, len(audioTracks)):
            self.m_audioTracks.addItem(self.trackName(audioTracks[i], i), i)
        self.m_audioTracks.setCurrentIndex(self.m_player.activeAudioTrack() + 1)

        videoTracks = self.m_player.videoTracks()
        self.m_videoTracks.addItem("No video", -1)
        for i in range(0, len(videoTracks)):
            self.m_videoTracks.addItem(self.trackName(videoTracks[i], i), i)
        self.m_videoTracks.setCurrentIndex(self.m_player.activeVideoTrack() + 1)

        self.m_subtitleTracks.addItem("No subtitles", -1)
        subtitleTracks = self.m_player.subtitleTracks()
        for i in range(0, len(subtitleTracks)):
            self.m_subtitleTracks.addItem(self.trackName(subtitleTracks[i], i), i)
        self.m_subtitleTracks.setCurrentIndex(self.m_player.activeSubtitleTrack() + 1)

    @Slot()
    def previousClicked(self):
        self.m_player.setPosition(0)

    @Slot(int)
    def seek(self, mseconds):
        self.m_player.setPosition(mseconds)

    @Slot(QMediaPlayer.MediaStatus)
    def statusChanged(self, status):
        self.handleCursor(status)
        # handle status message
        match status:
            case QMediaPlayer.MediaStatus.NoMedia | QMediaPlayer.MediaStatus.LoadedMedia:
                self.setStatusInfo("")
            case QMediaPlayer.MediaStatus.LoadingMedia:
                self.setStatusInfo("Loading...")
            case QMediaPlayer.MediaStatus.BufferingMedia | QMediaPlayer.MediaStatus.BufferedMedia:
                progress = round(self.m_player.bufferProgress() * 100.0)
                self.setStatusInfo(f"Buffering {progress}%")
            case QMediaPlayer.MediaStatus.StalledMedia:
                progress = round(self.m_player.bufferProgress() * 100.0)
                self.setStatusInfo(f"Stalled {progress}%")
            case QMediaPlayer.MediaStatus.EndOfMedia:
                QApplication.alert(self)
            case QMediaPlayer.MediaStatus.InvalidMedia:
                self.displayErrorMessage()

    def handleCursor(self, status):
        if (status == QMediaPlayer.MediaStatus.LoadingMedia
                or status == QMediaPlayer.MediaStatus.BufferingMedia
                or status == QMediaPlayer.MediaStatus.StalledMedia):
            self.setCursor(QCursor(Qt.CursorShape.BusyCursor))
        else:
            self.unsetCursor()

    @Slot("float")
    def bufferingProgress(self, progressV):
        progress = round(progressV * 100.0)
        if self.m_player.mediaStatus() == QMediaPlayer.MediaStatus.StalledMedia:
            self.setStatusInfo(f"Stalled {progress}%")
        else:
            self.setStatusInfo(f"Buffering {progress}%")

    @Slot(bool)
    def videoAvailableChanged(self, available):
        if not available:
            self.m_fullScreenButton.clicked.disconnect(self.m_videoWidget.switchToFullScreen)
            self.m_videoWidget.fullScreenChanged.disconnect(self.m_fullScreenButton.setChecked)
            self.m_videoWidget.setFullScreen(False)
        else:
            self.m_fullScreenButton.clicked.connect(self.m_videoWidget.switchToFullScreen)
            self.m_videoWidget.fullScreenChanged.connect(self.m_fullScreenButton.setChecked)
            if self.m_fullScreenButton.isChecked():
                self.m_videoWidget.setFullScreen(True)

    @Slot()
    def selectAudioStream(self):
        stream = self.m_audioTracks.currentData()
        self.m_player.setActiveAudioTrack(stream)

    @Slot()
    def selectVideoStream(self):
        stream = self.m_videoTracks.currentData()
        self.m_player.setActiveVideoTrack(stream)

    @Slot()
    def selectSubtitleStream(self):
        stream = self.m_subtitleTracks.currentData()
        self.m_player.setActiveSubtitleTrack(stream)

    def setTrackInfo(self, info):
        self.m_trackInfo = info
        title = self.m_trackInfo
        if self.m_statusInfo:
            title += f" | {self.m_statusInfo}"
        self.setWindowTitle(title)

    def setStatusInfo(self, info):
        self.m_statusInfo = info
        title = self.m_trackInfo
        if self.m_statusInfo:
            title += f" | {self.m_statusInfo}"
        self.setWindowTitle(title)

    @Slot()
    def displayErrorMessage(self):
        if self.m_player.error() != QMediaPlayer.Error.NoError:
            self.setStatusInfo(self.m_player.errorString())

    def updateDurationInfo(self, currentInfo):
        tStr = ""
        if currentInfo or self.m_duration:
            currentTime = QTime((currentInfo / 3600) % 60, (currentInfo / 60) % 60,
                                currentInfo % 60, (currentInfo * 1000) % 1000)
            totalTime = QTime((self.m_duration / 3600) % 60, (self.m_duration / 60) % 60,
                              self.m_duration % 60, (self.m_duration * 1000) % 1000)
            format = "hh:mm:ss" if self.m_duration > 3600 else "mm:ss"
            tStr = currentTime.toString(format) + " / " + totalTime.toString(format)
        self.m_labelDuration.setText(tStr)

    @Slot()
    def updateAudioDevices(self):
        self.m_audioOutputCombo.clear()

        self.m_audioOutputCombo.addItem("Default", QAudioDevice())
        for deviceInfo in QMediaDevices.audioOutputs():
            self.m_audioOutputCombo.addItem(deviceInfo.description(), deviceInfo)

    @Slot(int)
    def audioOutputChanged(self, index):
        device = self.m_audioOutputCombo.itemData(index)
        self.m_player.audioOutput().setDevice(device)
