#!/usr/bin/python3
import datetime
import json
import os
import shutil
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
import zipfile
from math import ceil

from PyQt6 import QtCore
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl, QLockFile, QRegularExpression
from PyQt6.QtGui import QColor, QIcon, QCloseEvent, QRegularExpressionValidator
from PyQt6.QtMultimedia import QMediaPlayer
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QMainWindow, QPushButton, QHBoxLayout, QVBoxLayout, \
    QGroupBox, QTableWidget, QGridLayout, QComboBox, QFileDialog, QTableWidgetItem, QSlider, QLineEdit, \
    QAbstractItemView, QCheckBox
import logging

from school_interview_system_expertise_modules.config import files_list_file, experts_list_file, \
    extracted_archive_folder, \
    file_with_marks, config_folder, config_file, icon_file, version, log_file
from school_interview_system_expertise_modules.expert_classes import NewExpert
from school_interview_system_expertise_modules.system_functions import alert, question, enter_password, \
    reset_log_to_last_n_days


class MusicInterfaceRefresh(QThread):
    pos_signal = pyqtSignal(str)
    time_elapsed_signal = pyqtSignal(int)
    player = None

    def run(self):
        while True:
            pos = self.player.position() // 1000
            if pos < 0:
                pos = 0
            # print(pos)
            minutes, seconds = divmod(int(pos), 60)
            if self.player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
                self.pos_signal.emit(f'{minutes:02}:{seconds:02}')
                self.time_elapsed_signal.emit(pos)
            time.sleep(0.1)


class MyWindow(QMainWindow):

    def switch_current_answer_file(self):
        self.stop_music()
        if self.records_tablewidget.currentItem():
            self.current_answer_file = os.path.join(self.folder_with_extracted_expert_archive,
                                                    self.records_tablewidget.currentItem().text() + '.ogg')
        else:
            self.current_answer_file = os.path.join(self.folder_with_extracted_expert_archive,
                                                    self.records_tablewidget.item(0, 0).text() + '.ogg')
        self.current_code = os.path.basename(self.current_answer_file).replace('.ogg', '')
        if not os.path.isfile(self.current_answer_file):
            self.player_groupbox.setEnabled(False)
            self.clock_label.setText('по потоковой аудиозаписи')
            self.clock_label.setStyleSheet('font-size: 50px; color: gray')
        else:
            self.player_groupbox.setEnabled(True)
            self.clock_label.setStyleSheet('font-size: 50px; color: green')
            url = QUrl.fromLocalFile(self.current_answer_file)
            # content = QUrl.fromLocalFile(url)
            self.media_player.setSource(url)
            self.current_answer_length_in_seconds = ceil(float(subprocess.run(['soxi', '-D', self.current_answer_file],
                                                                              capture_output=True).stdout.decode()))
            current_answer_minutes, current_answer_seconds = divmod(self.current_answer_length_in_seconds, 60)
            self.current_answer_length = f'{current_answer_minutes:02}:{current_answer_seconds:02}'
            self.update_music_interface('00:00')
            self.audio_time_slider.setRange(0, self.current_answer_length_in_seconds)
            self.update_music_slider(0)

        for i in range(len(self.marks_inputs)):
            self.marks_inputs[i].setText('')
            self.marks_inputs[i].setStyleSheet('')

        if self.current_code in self.dict_with_marks.keys():
            self.impossible_to_evaluate_checkbox.setChecked(False)
            self.switch_marks_enable()
            if 'Variant' not in self.dict_with_marks[self.current_code].keys():
                self.variant_combobox.setCurrentText('')
            for key in self.dict_with_marks[self.current_code].keys():
                if key == 'Итого':
                    continue
                if key == 'impossible':
                    self.impossible_to_evaluate_checkbox.setChecked(True)
                    self.switch_marks_enable()
                    continue
                try:
                    mark_index = self.marks_headers.index(key)
                # может быть не оценка, а номер варианта
                except ValueError:
                    if key == 'Variant':
                        try:
                            self.variant_combobox.setCurrentText(self.dict_with_marks[self.current_code][key])
                        except Exception:
                            self.variant_combobox.setCurrentText('')
                    continue
                self.marks_inputs[mark_index].setText(self.dict_with_marks[self.current_code][key])
                if self.marks_inputs[mark_index].text().strip() == '':
                    self.marks_inputs[mark_index].setStyleSheet('')
                try:
                    if not 0 <= int(self.marks_inputs[mark_index].text()) <= self.max_task_points[mark_index]:
                        self.marks_inputs[mark_index].setStyleSheet('color: red; border: 2px solid red;')
                    else:
                        self.marks_inputs[mark_index].setStyleSheet('')
                except ValueError:
                    pass
        else:
            self.dict_with_marks[self.current_code] = {'Итого': "0"}
        self.update_itog_and_save()
        self.update_colors_in_table()

    def update_colors_in_table(self):
        for row in range(self.records_tablewidget.rowCount()):
            code = self.records_tablewidget.item(row, 0).text().strip()
            if self.check_if_marks_for_code_are_correct(code):
                self.records_tablewidget.item(row, 0).setBackground(QColor(0, 140, 0))
            elif code in self.dict_with_marks:
                for criteria in self.dict_with_marks[code]:
                    if criteria not in ('Итого', 'Variant', 'expert') and self.dict_with_marks[code][criteria] != '':
                        self.records_tablewidget.item(row, 0).setBackground(QColor(200, 140, 0))
                        break
                else:
                    self.records_tablewidget.item(row, 0).setData(QtCore.Qt.ItemDataRole.BackgroundRole, None)
            else:
                self.records_tablewidget.item(row, 0).setData(QtCore.Qt.ItemDataRole.BackgroundRole, None)

    def check_archive(self, file):
        try:
            with zipfile.ZipFile(file, 'r') as archive:
                file_list = [i.filename for i in archive.infolist()]
            if not any([i.endswith('.dat') for i in
                        file_list]) or files_list_file not in file_list or experts_list_file not in file_list:
                alert(message='В архиве отсутствуют необходимые файлы. Архив повреждён или создан другой программой.')
                return False
            if len([i for i in file_list if i.endswith('.dat')]) != 1:
                alert(message='В архиве найдено больше одного DAT-файла! Архив повреждён или создан другой программой.')
                return False

            return True
        except Exception:
            alert(message=f'Не удалось распаковать архив {file}.')
            return False

    def load_archive(self):
        self.stop_music()
        self.file_for_expert_from_management_station, _ = QFileDialog.getOpenFileName(
            parent=None, caption='Выберите файл', directory=os.getcwd(), filter='ZIP archives (*.zip)'
        )
        if self.file_for_expert_from_management_station:
            if not self.check_archive(self.file_for_expert_from_management_station):
                logging.warning(f'Архив {self.file_for_expert_from_management_station} '
                               f'повреждён или создан другой программой.')
                return
            self.folder_with_extracted_expert_archive = extracted_archive_folder
            subprocess.run(['mkdir', '-p', extracted_archive_folder])
            if len(os.listdir(extracted_archive_folder)) > 0:
                if question(message='На данном компьютере уже проводилась экспертиза! Удалить имеющиеся файлы? Отмена '
                                    '- продолжить предыдущую проверку'):
                    for file in os.listdir(extracted_archive_folder):
                        os.remove(os.path.join(extracted_archive_folder, file))
                    os.remove(os.path.join(config_folder, file_with_marks))
                    with zipfile.ZipFile(self.file_for_expert_from_management_station, 'r') as archive:
                        archive.extractall(path=self.folder_with_extracted_expert_archive)
            else:
                with zipfile.ZipFile(self.file_for_expert_from_management_station, 'r') as archive:
                    archive.extractall(path=self.folder_with_extracted_expert_archive)
            subprocess.run(['py-ini-config', 'set', config_file, '-n', 'current_expert',
                            os.path.basename(self.file_for_expert_from_management_station).replace('.zip', '')])
            self.load_examen()

    def load_examen(self):
        if len(os.listdir(self.folder_with_extracted_expert_archive)) == 0:
            return
        self.impossible_to_evaluate_checkbox.setChecked(False)
        self.existing_recordings_names = [i.replace('.ogg', '') for i in
                                          os.listdir(self.folder_with_extracted_expert_archive) if
                                          i.endswith('.ogg')]
        # Чтение списка файлов вместе с отсутствующими
        self.existing_and_missing_recordings_names = open(
            os.path.join(self.folder_with_extracted_expert_archive, files_list_file)).readlines()
        for i in range(len(self.existing_and_missing_recordings_names)):
            self.existing_and_missing_recordings_names[i] = self.existing_and_missing_recordings_names[i].strip()
        while '' in self.existing_and_missing_recordings_names:
            self.existing_and_missing_recordings_names.remove('')

        if len(self.existing_and_missing_recordings_names) == 0:
            alert(message='Все работы уже проверены. Необходимо экспортировать результаты проверки.')
            self.export_marks(check=False)
            return
        self.records_tablewidget.setRowCount(len(self.existing_and_missing_recordings_names))
        for i in range(len(self.existing_and_missing_recordings_names)):
            self.records_tablewidget.setItem(i, 0, QTableWidgetItem(self.existing_and_missing_recordings_names[i]))
            self.start_table_background = self.records_tablewidget.item(i, 0).background().color()
            if self.existing_and_missing_recordings_names[i] not in self.existing_recordings_names:
                self.records_tablewidget.item(i, 0).setForeground(QColor('darkred'))
        self.save_mark_button.setEnabled(True)

        # если нашёлся недозаполненный файл с теми же ключами, то продолжить.
        # иначе - удалить его
        if os.path.isfile(self.json_file_with_marks):
            try:
                temp_dict = json.load(open(self.json_file_with_marks))
                temp_keys = set(temp_dict.keys())
                if temp_keys.issubset(set(self.existing_and_missing_recordings_names)):
                    self.load_marks_from_file()
            except Exception:
                os.remove(self.json_file_with_marks)
                subprocess.run(['touch', self.json_file_with_marks])
                self.dict_with_marks = dict()
        else:
            subprocess.run(['touch', self.json_file_with_marks])
            self.dict_with_marks = dict()

        self.switch_current_answer_file()

        self.clock_label.setStyleSheet('font-size: 50px; color: green')
        self.start_play_button.setEnabled(True)
        self.audio_time_slider.setEnabled(True)

        self.interface_refresh_thread.player = self.media_player
        self.interface_refresh_thread.start()

        dat_file = os.path.join(self.folder_with_extracted_expert_archive,
                                [i for i in os.listdir(self.folder_with_extracted_expert_archive) if
                                 i.endswith('.dat')][0])
        decoded_variants_xml = subprocess.run(['base64', '-d', dat_file], capture_output=True).stdout.decode()
        try:
            tree = ET.fromstring(decoded_variants_xml)
            self.variant_combobox.addItem('')
            model = self.variant_combobox.model()
            model.item(0).setEnabled(False)
            for i in tree.iter('Variant'):
                if i.text and i.text not in [self.variant_combobox.itemText(row) for row in
                                             range(self.variant_combobox.count())]:
                    self.variant_combobox.addItem(i.text)
        except Exception as e:
            alert(message=f'DAT-файл {dat_file} повреждён. Загрузка вариантов невозможна. Ошибка: {e}')
            sys.exit(1)
        expert_from_config = subprocess.run(['py-ini-config', 'get', config_file, '-n', 'current_expert'],
                                            capture_output=True).stdout.decode().strip()
        if expert_from_config:
            self.expert_label.setText(expert_from_config)
        elif self.file_for_expert_from_management_station:
            self.expert_label.setText(
                os.path.basename(self.file_for_expert_from_management_station).replace('.zip', ''))
        if 'current_exam_info.json' in os.listdir(self.folder_with_extracted_expert_archive):
            current_exam_info = json.loads(
                open(os.path.join(self.folder_with_extracted_expert_archive, 'current_exam_info.json')).read())
            self.examen_date_label.setText(f"Дата экзамена: {current_exam_info['examen_date']}")
            self.school_number_label.setText(f"ОО: {current_exam_info['school_number']}")
        for item in self.marks_inputs:
            item.setEnabled(True)
        self.substitute_expert_button.setEnabled(True)

    def load_marks_from_file(self):
        self.dict_with_marks = json.load(open(self.json_file_with_marks))
        self.switch_current_answer_file()

    def update_itog_and_save(self):
        if self.current_answer_file:
            try:
                if type(self.sender()) is QLineEdit:
                    indx = self.marks_inputs.index(self.sender())
                    if not 0 <= int(self.sender().text()) <= self.max_task_points[indx]:
                        self.sender().setStyleSheet('color: red; border: 2px solid red;')
                    else:
                        self.sender().setStyleSheet('')
            except ValueError:
                pass

        try:
            if type(self.sender()) is QLineEdit:
                if self.current_code not in self.dict_with_marks.keys():
                    self.dict_with_marks[self.current_code] = dict()
                mark_index = self.marks_inputs.index(self.sender())
                mark_name = self.marks_headers[mark_index]
                if self.sender().text().strip():
                    self.dict_with_marks[self.current_code][mark_name] = self.sender().text()
                else:
                    if mark_name in self.dict_with_marks[self.current_code].keys():
                        self.dict_with_marks[self.current_code].pop(mark_name)
                self.dict_with_marks[self.current_code]['expert'] = self.expert_label.text().strip()
        except Exception as e:
            logging.error(f'Возникло исключение: {e}')
        itog = 0
        for i in range(len(self.marks_inputs)):
            line = self.marks_inputs[i]
            try:
                itog += int(line.text())
            # оценка не заполнена, можно пропустить
            except Exception:
                pass
        self.itog_lineedit.setText(f'{itog}')
        self.dict_with_marks[self.current_code]['Итого'] = f'{itog}'
        self.save_dict_with_marks_to_file()
        self.update_colors_in_table()

    def save_dict_with_marks_to_file(self):
        with open(self.json_file_with_marks, 'w') as out:
            json.dump(self.dict_with_marks, out, ensure_ascii=False, indent=4)

    def change_variant(self):
        if self.current_code not in self.dict_with_marks.keys():
            self.dict_with_marks[self.current_code] = {'Итого': '0'}
        if self.variant_combobox.currentText().strip() == '':
            self.variant_info_label.setStyleSheet('color: red;')

        else:
            self.variant_info_label.setStyleSheet('')
            self.dict_with_marks[self.current_code]["Variant"] = self.variant_combobox.currentText()
            self.save_dict_with_marks_to_file()
        self.switch_marks_enable()

    def play_music(self):
        if self.current_answer_file:
            if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
                self.stop_music()
            self.media_player.play()
            self.is_paused = False
            self.pause_play_button.setEnabled(True)
            self.stop_play_button.setEnabled(True)

    def pause_music(self):
        if not self.is_paused:
            self.media_player.pause()
            self.is_paused = True
            self.pause_play_button.setText('Возобновить')
        else:
            self.media_player.play()
            self.is_paused = False
            self.pause_play_button.setText('Пауза')

    def stop_music(self):
        self.media_player.stop()
        self.is_paused = False
        self.pause_play_button.setText('Пауза')
        self.pause_play_button.setEnabled(False)
        self.stop_play_button.setEnabled(False)
        # self.interface_refresh_thread.terminate()
        self.update_music_interface('00:00')
        self.update_music_slider(0)

    def update_music_interface(self, pos):
        self.clock_label.setText(f'{pos} / {self.current_answer_length}')

    def update_music_slider(self, time_elapsed):
        try:
            self.audio_time_slider.setValue(time_elapsed)
        except ZeroDivisionError:
            self.audio_time_slider.setValue(0)

    def set_audio_position_from_slider(self):
        time_set = self.audio_time_slider.value()
        self.stop_music()
        minutes, seconds = divmod(time_set, 60)
        self.update_music_interface(f'{minutes:02}:{seconds:02}')
        self.media_player.setPosition(time_set * 1000)
        self.play_music()
        # self.stop_music()

    def check_if_marks_for_code_are_correct(self, code):
        try:
            if 'impossible' in self.dict_with_marks[code] and self.dict_with_marks[code]['impossible'] == 'true':
                return True
            for key in self.marks_headers:
                if self.dict_with_marks[code][key] == '':
                    return False
            return True
        except Exception:
            return False

    def check_if_all_marks_are_correct(self):
        if not set(self.existing_recordings_names).issubset(set(self.dict_with_marks.keys())):
            alert(message='Оценки проставлены не всем ученикам')
            return False
        for key in self.dict_with_marks.keys():
            marks_given = set(self.dict_with_marks[key].keys())
            # Если не выставлены все оценки или "невозможно проверить"
            if not marks_given.issuperset(set(self.marks_headers)) and 'impossible' not in self.dict_with_marks[
                key].keys():
                alert(message=f'Для записи {key} проставлены не все оценки')
                return False
            for criteria in self.dict_with_marks[key].keys():
                if criteria in ('Variant', 'Итого', 'expert'):
                    continue
                try:
                    mark_index = self.marks_headers.index(criteria)
                except ValueError:
                    continue
                mark = int(self.dict_with_marks[key][criteria])
                if not 0 <= mark <= self.max_task_points[mark_index]:
                    alert(message=f'Оценка по критерию {criteria} для ученика {key} не соответствует ограничениям')
                    return False
        return True

    def export_marks(self, check=True):
        if check:
            if not self.check_if_all_marks_are_correct():
                return
            if not question(message='Вы действительно хотите завершить проверку и экспортировать результаты?'):
                return
        export_filename = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S-%f')[:-3] + '-Экспертиза.json'
        folder = QFileDialog.getExistingDirectory(
            parent=None, caption='Выберите папку для экспорта', directory=os.getcwd()
        )
        if not folder:
            if check:
                return
            else:
                if question(message='Все работы уже проверены. Необходимо экспортировать результаты проверки. '
                                    'Вы действительно хотите отменить экспорт оценок?'):
                    return
                self.export_marks(check=False)
        shutil.copyfile(self.json_file_with_marks, os.path.join(folder, export_filename))
        # os.remove(self.json_file_with_marks)
        alert(message=f'Экспорт завершён.\nСохранённый файл: {folder}/{export_filename}')

    def switch_marks_enable(self):
        if not self.current_code:
            return
        checked = self.impossible_to_evaluate_checkbox.isChecked()
        for item in self.marks_inputs:
            item.setEnabled(not checked and not self.variant_combobox.currentText().strip() == '')
        if type(self.sender()) == QCheckBox:
            if self.current_code:
                if checked:
                    if self.current_code not in self.dict_with_marks.keys():
                        self.dict_with_marks[self.current_code] = {'impossible': 'true'}
                    else:
                        self.dict_with_marks[self.current_code]['impossible'] = 'true'
                if not checked:
                    if self.current_code not in self.dict_with_marks.keys():
                        self.dict_with_marks[self.current_code] = dict()
                    elif 'impossible' in self.dict_with_marks[self.current_code].keys():
                        self.dict_with_marks[self.current_code].pop('impossible')
            self.dict_with_marks[self.current_code]['expert'] = self.expert_label.text().strip()
            self.save_dict_with_marks_to_file()
            self.update_colors_in_table()

    def new_expert_window_show(self):
        if not question(message=f'Вы действительно хотите заменить эксперта и сохранить все работы, '
                                f'полностью проверенные экспертом {self.expert_label.text().strip()}?'):
            return
        if not enter_password():
            return
        new_expert_window = NewExpert()
        new_expert_window.ok_button.clicked.connect(
            lambda: self.substitute_expert(new_expert_window.experts_combobox.currentText()))
        new_expert_window.show()

    def substitute_expert(self, new_expert):
        self.expert_label.setText(new_expert)
        subprocess.run(['py-ini-config', 'set', config_file, '-n', 'current_expert', new_expert])
        self.finished_codes = []
        for key in self.dict_with_marks.keys():
            # Если работа целиком проверена, другой эксперт её не получает
            if len(self.dict_with_marks[key].keys()) == len(self.marks_headers) + 2 or 'impossible' in \
                    self.dict_with_marks[key].keys():
                self.finished_codes.append(key)
        for key in self.finished_codes:
            if key in self.existing_and_missing_recordings_names:
                self.existing_and_missing_recordings_names.remove(key)
        with open(os.path.join(self.folder_with_extracted_expert_archive, files_list_file), 'w') as out:
            print(*self.existing_and_missing_recordings_names, sep='\n', file=out)
        self.load_examen()

    def closeEvent(self, a0: QCloseEvent):
        if not question(message='Вы действительно хотите выйти из программы?'):
            a0.ignore()
            return
        a0.accept()

    def __init__(self):
        if not enter_password():
            sys.exit(0)
        QMainWindow.__init__(self)
        subprocess.run(['mkdir', '-p', config_folder])
        subprocess.run(['mkdir', '-p', extracted_archive_folder])
        subprocess.run(['touch', config_file])
        self.existing_and_missing_recordings_names = None
        self.file_for_expert_from_management_station = None
        self.folder_with_extracted_expert_archive = extracted_archive_folder
        self.existing_recordings_names = None
        self.current_answer_file = ''
        self.current_code = ''
        self.json_file_with_marks = file_with_marks
        if os.path.isfile(self.json_file_with_marks):
            try:
                self.dict_with_marks = json.load(open(self.json_file_with_marks))
            except Exception:
                self.dict_with_marks = dict()
        else:
            self.dict_with_marks = dict()
        self.current_answer_length_in_seconds = 0
        self.current_answer_length = '00:00'
        self.is_paused = False

        self.media_player = QMediaPlayer()

        self.interface_refresh_thread = MusicInterfaceRefresh()
        self.interface_refresh_thread.setTerminationEnabled(True)
        self.interface_refresh_thread.pos_signal.connect(self.update_music_interface)
        self.interface_refresh_thread.time_elapsed_signal.connect(self.update_music_slider)
        self.start_table_background = QColor(255, 255, 255)
        self.initUI()

    def initUI(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout()
        central_widget.setLayout(layout)

        # кнопки "Загрузить ответ" и "Сохранить"
        upper_panel = QHBoxLayout()

        self.load_exam_button = QPushButton('Загрузить архив')
        self.load_exam_button.clicked.connect(self.load_archive)
        upper_panel.addWidget(self.load_exam_button)
        self.save_mark_button = QPushButton('Сохранить выставленные баллы')
        self.save_mark_button.clicked.connect(self.export_marks)
        self.save_mark_button.setEnabled(False)
        upper_panel.addWidget(self.save_mark_button)

        layout.addLayout(upper_panel)

        info_about_exam_groupbox = QGroupBox()
        info_about_exam_groupbox_layout = QGridLayout()
        info_about_exam_groupbox.setLayout(info_about_exam_groupbox_layout)
        info_about_exam_groupbox_layout.addWidget(QLabel('Регион: 77'), 0, 0, 1, 2)
        self.school_number_label = QLabel(f'ОО:')
        info_about_exam_groupbox_layout.addWidget(self.school_number_label, 0, 2, 1, 2)
        self.examen_date_label = QLabel(f'Дата экзамена:')
        info_about_exam_groupbox_layout.addWidget(self.examen_date_label, 0, 4, 1, 2)
        info_about_exam_groupbox_layout.addWidget(QLabel('Предмет: 20 - Итоговое собеседование по русскому языку'), 1, 0, 1, 3)
        info_about_exam_groupbox_layout.addWidget(QLabel('Эксперт:'), 2, 0)
        self.expert_label = QLabel()
        info_about_exam_groupbox_layout.addWidget(self.expert_label, 2, 1, 1, 5)
        layout.addWidget(info_about_exam_groupbox)

        left_and_right_panels_layout = QHBoxLayout()
        self.records_tablewidget = QTableWidget()
        self.records_tablewidget.setRowCount(0)
        self.records_tablewidget.setColumnCount(1)
        self.records_tablewidget.setHorizontalHeaderLabels(['Записи'])
        self.records_tablewidget.horizontalHeader().setStretchLastSection(True)
        self.records_tablewidget.setFixedWidth(300)
        self.records_tablewidget.cellClicked.connect(self.switch_current_answer_file)
        self.records_tablewidget.currentItemChanged.connect(self.switch_current_answer_file)
        self.records_tablewidget.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
        self.records_tablewidget.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        left_and_right_panels_layout.addWidget(self.records_tablewidget)

        right_panel_layout = QVBoxLayout()

        self.player_groupbox = QGroupBox()
        player_groupbox_layout = QVBoxLayout()
        self.player_groupbox.setLayout(player_groupbox_layout)

        self.clock_label = QLabel('00:00 / 00:00')
        self.clock_label.setStyleSheet('font-size: 50px; color: gray')
        self.clock_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        player_groupbox_layout.addWidget(self.clock_label)
        self.audio_time_slider = QSlider()
        self.audio_time_slider.setOrientation(Qt.Orientation.Horizontal)
        self.audio_time_slider.setEnabled(False)
        self.audio_time_slider.setRange(0, 1)
        self.audio_time_slider.sliderReleased.connect(self.set_audio_position_from_slider)
        player_groupbox_layout.addWidget(self.audio_time_slider)
        buttons_layout = QHBoxLayout()
        self.start_play_button = QPushButton('Воспроизвести')
        self.start_play_button.clicked.connect(self.play_music)
        buttons_layout.addWidget(self.start_play_button)
        self.pause_play_button = QPushButton('Пауза')
        self.pause_play_button.clicked.connect(self.pause_music)
        buttons_layout.addWidget(self.pause_play_button)
        self.stop_play_button = QPushButton('Остановить')
        self.stop_play_button.clicked.connect(self.stop_music)
        buttons_layout.addWidget(self.stop_play_button)
        self.pause_play_button.setEnabled(False)
        self.start_play_button.setEnabled(False)
        self.stop_play_button.setEnabled(False)
        player_groupbox_layout.addLayout(buttons_layout)

        right_panel_layout.addWidget(self.player_groupbox)
        criteria_label = QLabel('Критерии оценивания')
        right_panel_layout.addWidget(criteria_label)
        variant_layout = QHBoxLayout()
        self.variant_info_label = QLabel('Вариант')
        self.variant_info_label.setStyleSheet('color: red;')
        variant_layout.addWidget(self.variant_info_label)
        self.variant_combobox = QComboBox()
        self.variant_combobox.currentIndexChanged.connect(self.change_variant)
        variant_layout.addWidget(self.variant_combobox)
        variant_layout.addStretch()
        right_panel_layout.addLayout(variant_layout)

        marks_table_layout = QGridLayout()
        # https://fipi.ru/oge/demoversii-specifikacii-kodifikatory
        self.marks_headers = (
            'Ч1', 'Ч2', 'Ч3', 'П1', 'П2', 'П3', 'М1', 'М2', 'Д1', 'Р1', 'Р2', 'Р3', 'Р4', 'Итого')
        self.max_task_points = (
            1, 1, 1,
            2, 1, 1,
            2, 1,
            3,
            2, 2, 2, 1
        )
        self.marks_inputs = []
        for i in range(len(self.marks_headers) - 1):
            self.marks_inputs.append(QLineEdit())

        for i in range(len(self.marks_inputs)):
            self.marks_inputs[i].setValidator(
                QRegularExpressionValidator(QRegularExpression(f'[0-{self.max_task_points[i]}]')))
            self.marks_inputs[i].setToolTip(f'0-{self.max_task_points[i]}')
            self.marks_inputs[i].textEdited.connect(self.update_itog_and_save)
            self.marks_inputs[i].setEnabled(False)

        row_len = len(self.marks_headers) // 2 + len(self.marks_headers) % 2
        for i in range(len(self.marks_headers) - 1):
            marks_table_layout.addWidget(QLabel(self.marks_headers[i]), i // row_len * 2, i % row_len)
            marks_table_layout.addWidget(self.marks_inputs[i], i // row_len * 2 + 1, i % row_len)

        marks_table_layout.addWidget(QLabel(self.marks_headers[-1]), 2, row_len - 1 - len(self.marks_headers) % 2, 1, 1 + len(self.marks_headers) % 2)
        self.itog_lineedit = QLineEdit('0')
        self.itog_lineedit.setDisabled(True)
        marks_table_layout.addWidget(self.itog_lineedit, 3, row_len - 1 - len(self.marks_headers) % 2, 1, 1 + len(self.marks_headers) % 2)

        right_panel_layout.addLayout(marks_table_layout)

        self.impossible_to_evaluate_checkbox = QCheckBox('Невозможно оценить')
        self.impossible_to_evaluate_checkbox.clicked.connect(self.switch_marks_enable)
        right_panel_layout.addWidget(self.impossible_to_evaluate_checkbox)

        right_panel_layout.addStretch()

        substitute_expert_layout = QHBoxLayout()
        self.substitute_expert_button = QPushButton('Заменить эксперта')
        self.substitute_expert_button.clicked.connect(self.new_expert_window_show)
        self.substitute_expert_button.setEnabled(False)
        substitute_expert_layout.addStretch()
        substitute_expert_layout.addWidget(self.substitute_expert_button)
        right_panel_layout.addLayout(substitute_expert_layout)
        left_and_right_panels_layout.addLayout(right_panel_layout)

        layout.addLayout(left_and_right_panels_layout)

        if len(os.listdir(self.folder_with_extracted_expert_archive)) > 0 or os.path.isfile(
                os.path.join(config_folder, file_with_marks)):
            if question(message='На данном компьютере уже производилась проверка! Продолжить? '
                                'Ответ Отмена удалит существующую проверку.'):
                self.load_examen()
            else:
                shutil.rmtree(self.folder_with_extracted_expert_archive)
                if os.path.isfile(os.path.join(config_folder, file_with_marks)):
                    os.remove(os.path.join(config_folder, file_with_marks))

        self.setGeometry(100, 100, 1200, 800)
        # Если программа уже установлена
        if os.path.isfile(f'/usr/share/icons/hicolor/scalable/apps/{icon_file}'):
            self.setWindowIcon(QIcon.fromTheme(icon_file.replace('.svg', '')))
        # Если ещё тестируется
        else:
            self.setWindowIcon(QIcon(icon_file))
        self.setWindowTitle(f"Станция экспертизы {version}")
        self.setStyleSheet('font-size: 20pt;')
        self.show()
        logging.info('Программа запущена.')


if __name__ == '__main__':
    root = logging.getLogger()
    if root.handlers:
        for handler in root.handlers:
            root.removeHandler(handler)
    filename = log_file
    logging.basicConfig(
        filename=filename,
        format=u'%(asctime)s %(filename)s line:%(lineno)d %(funcName)s() %(message)s',
        level=logging.DEBUG)
    reset_log_to_last_n_days(log_file=filename, n=7)
    try:
        app = QApplication(sys.argv)
        if os.path.isfile(f'/usr/share/icons/hicolor/scalable/apps/{icon_file}'):
            app.setWindowIcon(QIcon.fromTheme(icon_file.replace('.svg', '')))
        lockfile = QLockFile('/tmp/school-interview-system-expertise.lock')
        if not lockfile.tryLock(100):
            alert(message='Программа уже запущена. Запуск второго экземпляра не допускается.')
            sys.exit(0)
        window = MyWindow()
        sys.exit(app.exec())
    except Exception as e:
        logging.error(f'Во время выполнения программы возникла ошибка: {e}')
