# Copyright © 2024 Noah Vogt <noah@noahvogt.com>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import sys
from os import path, listdir
from re import match
from dataclasses import dataclass

from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
    QApplication,
    QDialog,
    QVBoxLayout,
    QHBoxLayout,
    QStyle,
    QLabel,
    QCheckBox,
    QPushButton,
    QMessageBox,
    QScrollArea,
    QWidget,
)
from PyQt5.QtMultimedia import (  # pylint: disable=no-name-in-module
    QMediaPlayer,
    QMediaContent,
)
from PyQt5.QtCore import (  # pylint: disable=no-name-in-module
    QSize,
    QUrl,
    QTimer,
)

from audio import AudioSourceFileType, ChosenAudio, get_wave_duration_in_secs
from utils import CustomException, log, RadioButtonDialog
import config as const


@dataclass
class LabelConstruct:
    rel_path: str
    label: QLabel


@dataclass
class CheckBoxConstruct:
    rel_sheet_path: str
    abs_wave_path: str
    check_box: QCheckBox


class WaveAndSheetPreviewChooserGUI(
    QDialog
):  # pylint: disable=too-few-public-methods
    def __init__(
        self,
        base_dir: str,
        options: list[str],
        window_title: str,
        source_type: AudioSourceFileType,
    ) -> None:
        super().__init__()
        self.base_dir = base_dir
        self.setWindowTitle(window_title)

        self.master_layout = QVBoxLayout(self)

        scroll_area_layout = self.get_scroll_area_layout()

        self.check_buttons = []

        self.player = QMediaPlayer()

        self.chosen_audios = []
        self.position_labels = []
        for num, item in enumerate(options):
            if source_type is AudioSourceFileType.WAVE:
                rel_wave_path = item
            else:
                rel_wave_path = self.get_wav_relative_path_from_cue_sheet(item)
            abs_wave_path = path.join(base_dir, rel_wave_path)

            if source_type is AudioSourceFileType.WAVE:
                check_box = QCheckBox(
                    f"Segment {(num+1):0{const.CD_RECORD_FILENAME_ZFILL}}"
                )
            else:
                cd_numbering = "CD #" + item[
                    item.find("-") + 1 : item.find(".")
                ].lstrip("0")
                check_box = QCheckBox(cd_numbering)
            if num == 0 and source_type is AudioSourceFileType.CUESHEET:
                check_box.setChecked(True)
            button_layout = QHBoxLayout()
            check_box_construct = CheckBoxConstruct(
                item, abs_wave_path, check_box
            )
            self.check_buttons.append(check_box_construct)
            button_layout.addWidget(check_box)

            play_button = self.get_player_button("SP_MediaPlay")
            play_button.setToolTip("Play CD Preview")
            play_button.clicked.connect(
                lambda _, x=rel_wave_path: self.play_audio(x)
            )

            pause_button = self.get_player_button("SP_MediaPause")
            pause_button.setToolTip("Pause CD Preview")
            pause_button.clicked.connect(
                lambda _, x=rel_wave_path: self.pause_player(x)
            )

            stop_button = self.get_player_button("SP_MediaStop")
            stop_button.setToolTip("Stop CD Preview")
            stop_button.clicked.connect(
                lambda _, x=rel_wave_path: self.stop_player(x)
            )

            seek_bwd_button = self.get_player_button("SP_MediaSeekBackward")
            seek_bwd_button.setToolTip("Seek 10 seconds backwards")
            seek_bwd_button.clicked.connect(
                lambda _, x=rel_wave_path: self.seek_bwd_10secs(x)
            )

            seek_fwd_button = self.get_player_button("SP_MediaSeekForward")
            seek_fwd_button.setToolTip("Seek 10 seconds forwards")
            seek_fwd_button.clicked.connect(
                lambda _, x=rel_wave_path: self.seek_fwd_10secs(x)
            )

            button_layout.addWidget(play_button)
            button_layout.addWidget(pause_button)
            button_layout.addWidget(stop_button)
            button_layout.addWidget(seek_bwd_button)
            button_layout.addWidget(seek_fwd_button)

            time_label = self.get_initial_time_label(abs_wave_path)
            label_construct = LabelConstruct(rel_wave_path, time_label)
            self.position_labels.append(label_construct)
            button_layout.addWidget(time_label)
            timer = QTimer(self)
            timer.timeout.connect(self.update_position)
            timer.start(1000)

            scroll_area_layout.addLayout(button_layout)

        ok_button = QPushButton("OK")
        ok_button.clicked.connect(self.accept)
        self.master_layout.addWidget(ok_button)

    def get_initial_time_label(self, abs_wave_path):
        secs = get_wave_duration_in_secs(abs_wave_path)
        mins = secs // 60
        secs %= 60
        time_label = QLabel(f"00:00 / {mins:02}:{secs:02}")
        time_label.setMinimumSize(QSize(90, 10))
        return time_label

    def play_audio(self, rel_path: str) -> None:
        if self.player.state() == QMediaPlayer.PausedState:  # pyright: ignore
            media = self.player.media()
            if media.isNull():
                return
            if path.split(media.canonicalUrl().toString())[1] == rel_path:
                self.player.play()
        else:
            url = QUrl.fromLocalFile(path.join(self.base_dir, rel_path))
            content = QMediaContent(url)
            self.player.setMedia(content)
            self.player.play()

    def update_position(self) -> None:
        media = self.player.media()
        if media.isNull():
            return
        playing_path = path.split(media.canonicalUrl().toString())[1]
        for label_construct in self.position_labels:
            if label_construct.rel_path == playing_path:
                old_text = label_construct.label.text()
                old_text = old_text[old_text.find(" / ") :]
                secs = self.player.position() // 1000
                mins = secs // 60
                secs %= 60
                label_construct.label.setText(f"{mins:02}:{secs:02}{old_text}")

    def stop_player(self, rel_path: str) -> None:
        media = self.player.media()
        if media.isNull():
            return
        if path.split(media.canonicalUrl().toString())[1] == rel_path:
            self.player.stop()
            self.update_position()

    def seek_by(self, rel_path: str, seek_by_milis) -> None:
        media = self.player.media()
        if media.isNull():
            return
        if path.split(media.canonicalUrl().toString())[1] == rel_path:
            position = self.player.position()
            self.player.setPosition(position + seek_by_milis)
            self.update_position()

    def seek_fwd_10secs(self, rel_path: str) -> None:
        self.seek_by(rel_path, 10000)

    def seek_bwd_10secs(self, rel_path: str) -> None:
        self.seek_by(rel_path, -10000)

    def pause_player(self, rel_path: str) -> None:
        media = self.player.media()
        if media.isNull():
            return
        if path.split(media.canonicalUrl().toString())[1] == rel_path:
            self.player.pause()

    def get_scroll_area_layout(self):
        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        self.master_layout.addWidget(scroll_area)

        scroll_content = QWidget()
        scroll_area.setWidget(scroll_content)
        scroll_area_layout = QVBoxLayout(scroll_content)
        return scroll_area_layout

    def get_player_button(self, icon_name: str) -> QPushButton:
        stop_button = QPushButton("")
        stop_button.setMinimumSize(QSize(40, 40))
        stop_button.setMaximumSize(QSize(40, 40))
        pixmapi = getattr(QStyle, icon_name)
        icon = self.style().standardIcon(pixmapi)  # pyright: ignore
        stop_button.setIcon(icon)
        return stop_button

    def accept(self) -> None:
        for check_box_construct in self.check_buttons:
            if check_box_construct.check_box.isChecked():
                self.chosen_audios.append(
                    ChosenAudio(
                        check_box_construct.rel_sheet_path,
                        check_box_construct.abs_wave_path,
                    )
                )
        super().accept()

    def get_wav_relative_path_from_cue_sheet(
        self, sheet_relative_path: str
    ) -> str:
        full_path = path.join(self.base_dir, sheet_relative_path)

        try:
            with open(
                full_path,
                mode="r",
                encoding="utf-8-sig",
            ) as cachefile_reader:
                cachefile_content = cachefile_reader.readlines()
            first_line = cachefile_content[0].strip()
            if not match(r"^FILE \".+\" WAVE$", first_line):
                raise CustomException("invalid first cue sheet line")
            full_path_found = first_line[first_line.find('"') + 1 :]
            full_path_found = full_path_found[: full_path_found.rfind('"')]
            return path.split(full_path_found)[1]
        except (
            FileNotFoundError,
            PermissionError,
            IOError,
            IndexError,
            CustomException,
        ) as error:
            QMessageBox.critical(
                self,
                "Error",
                f"Could not parse cue sheet: '{full_path}', Reason: {error}",
            )
            sys.exit(1)


@dataclass
class ArchiveTypeStrings:
    archive_type_plural: str
    action_to_choose: str
    action_ing_form: str


def choose_cd_day() -> list[str]:
    strings = ArchiveTypeStrings("CD's", "CD day to Burn", "Burning CD for day")
    return choose_archive_day(strings)


def choose_sermon_day() -> list[str]:
    strings = ArchiveTypeStrings(
        "Sermons", "Sermon day to upload", "Uploading Sermon for day"
    )
    return choose_archive_day(strings)


def choose_archive_day(strings: ArchiveTypeStrings) -> list[str]:
    # pylint: disable=unused-variable
    app = QApplication([])
    try:
        dirs = sorted(listdir(const.CD_RECORD_OUTPUT_BASEDIR))
        dirs.reverse()

        if not dirs:
            return [
                f"Did not find any {strings.archive_type_plural} in: "
                + f"{const.CD_RECORD_OUTPUT_BASEDIR}.",
                "",
            ]

        dialog = RadioButtonDialog(
            dirs, "Choose a " + f"{strings.action_to_choose}"
        )
        if dialog.exec_() == QDialog.Accepted:
            log(f"{strings.action_ing_form} for day: {dialog.chosen}")
            return ["", dialog.chosen]
        return ["ignore", ""]
    except (FileNotFoundError, PermissionError, IOError):
        pass

    return [
        f"Failed to access directory: {const.CD_RECORD_OUTPUT_BASEDIR}.",
        "",
    ]
