# vim: fileencoding=utf-8
#
from ..template_engine import ParametersContainer, Variables
from ...variables.datavars import NamespaceNode, VariableNotFoundError
from ...variables.loader import Datavars
from ...utils.images import ImageMagick
from .base_format import Format, FormatError

from typing import Union, List, Tuple, NoReturn
import hashlib
import re
import os


class BackgroundsFormat(Format):
    FORMAT = 'backgrounds'
    EXECUTABLE = True
    FORMAT_PARAMETERS = {'convert', 'stretch'}

    def __init__(self, template_text: str,
                 template_path: str,
                 parameters: ParametersContainer,
                 datavars: Union[Datavars, NamespaceNode, Variables],
                 chroot_path: str = "/"):
        self._lines: List[str] = [line for line
                                  in template_text.strip().split('\n') if line]
        self._datavars: Union[Datavars, NamespaceNode, Variables] = datavars

        if parameters.source:
            self._source = parameters.source
        else:
            raise FormatError("source parameter is not set for template with"
                              "with 'backgrounds' format.")
        self._mirror: bool = parameters.mirror

        self._stretch: bool = parameters.stretch
        self._convert: Union[bool, str] = parameters.convert
        self._empty_name: bool = (parameters.name == '')

        # Измененные файлы.
        self.changed_files: dict = dict()

        # Список предупреждений.
        self._warnings: list = []

        # Флаг для тестов.
        self._fake_chroot: bool = False
        try:
            self._fake_chroot = datavars.main.fake_chroot
        except Exception:
            pass

    def execute_format(self, target_path: str, chroot_path: str = '/') -> dict:
        '''Метод для запуска работы формата.'''
        if not self._check_source(self._source, target_path, self._mirror):
            return self.changed_files

        print(f"FAKE_CHROOT: {self._fake_chroot}")
        self._magician = ImageMagick(
                          chroot=chroot_path if not self._fake_chroot else "/")
        source_resolution = self._magician.get_image_resolution(self._source)

        resolutions_from_var = False
        resolutions = self._get_resolutions_from_lines(self._lines,
                                                       source_resolution)
        if not resolutions:
            resolutions = self._get_resolutions_from_var(self._datavars)
            resolutions_from_var = True

        action_md5sum = self._get_action_md5sum(self._source, resolutions)
        if not self._check_target_directory(target_path, self._mirror,
                                            resolutions, action_md5sum):
            return {}

        images_format = self._get_format_value(self._convert,
                                               self._datavars)
        if self._convert and self._convert == "GFXBOOT":
            converter = self._magician.convert_resize_gfxboot
        else:
            converter = self._magician.convert_resize_crop_center

        output_paths = self._make_output_paths(self._lines, images_format,
                                               target_path, resolutions,
                                               resolutions_from_var)

        # Выполняем преобразование изображений в требуемый размер и формат.
        for resolution, output_path in output_paths.items():
            if not self._check_stretch(source_resolution,
                                       resolution,
                                       self._stretch):
                continue
            width, height = resolution
            converter(self._source, output_path, height, width,
                      image_format=images_format)
            if output_path in self.changed_files:
                self.changed_files[output_path] = 'M'
            else:
                self.changed_files[output_path] = 'N'

        self._create_md5sum_file(target_path, action_md5sum)
        return self.changed_files

    def _get_format_value(self, convert: Union[bool, str],
                          datavars: Union[Datavars, NamespaceNode, Variables]
                          ) -> str:
        """Метод для получения значения формата."""
        if convert:
            if convert == "JPG" or convert == "GFXBOOT":
                images_format = "JPEG"
            else:
                images_format = convert
        else:
            images_format = self._magician.get_image_format(self._source)
        return images_format

    def _make_output_paths(self, lines: List[str],
                           images_format: str,
                           target_path: str,
                           resolutions: List[Tuple[int, int]],
                           resolutions_from_var: bool
                           ) -> List[str]:
        """Метод для получения списка путей, по которым будут создаваться
        изображения."""
        if not self._empty_name:
            path, name = os.path.split(target_path)
        else:
            path = os.path.dirname(target_path)
            name = ''
        paths = {}

        if not resolutions_from_var and len(resolutions) == 1:
            if self._empty_name:
                raise FormatError("'name' parameter is empty in 'backgrounds'"
                                  " format template with single file output.")
            paths = {next(iter(resolutions)): target_path}
            return paths

        for width, height in resolutions:
            paths[(width, height)] = (f"{path}/{name}{width}x{height}"
                                      f".{images_format.lower()}")
        return paths

    def _get_resolutions_from_lines(self, lines: List[str],
                                    source_resolution: Tuple[int, int]
                                    ) -> List[Tuple[int, int]]:
        """Метод для получения списка кортежей с разрешениями выходных
        изображений из текста шаблона."""
        resolutions = []
        if lines:
            for line in lines:
                if (line.strip() == "original"
                   and source_resolution not in resolutions):
                    resolutions.append(source_resolution)
                else:
                    try:
                        width, height = line.lower().strip().split("x")
                        resolution = (int(width), int(height))
                    except Exception:
                        raise FormatError("can not parse line from template"
                                          " with 'backgrounds' format:"
                                          f" '{line}'")
                    if resolution not in resolutions:
                        resolutions.append(resolution)
        return resolutions

    def _get_resolutions_from_var(self, datavars: Union[Datavars,
                                                        NamespaceNode,
                                                        Variables]
                                  ) -> List[Tuple[int, int]]:
        """Метод для получения списка кортежей с разрешениями выходных
        изображений из переменной."""
        try:
            resolutions = []
            for resolution in self._datavars.main.cl_resolutions:
                resolution = tuple(resolution.strip().split("x"))
                resolutions.append(resolution)
            return resolutions
        except VariableNotFoundError:
            raise FormatError("resolutions values was not found.")
        except Exception:
            raise FormatError("can not use resolution values from variable:"
                              " 'main.cl_resolutions'")

    def _check_target_directory(self, target_path: str, mirror: bool,
                                resolutions: List[Tuple[int, int]],
                                action_md5sum: str
                                ) -> Union[bool, str]:
        """Метод для проверки содержимого целевой директории, удаления
        изображений и md5-сумм, подлежащих удалению, а также сравнения
        имеющейся в директории md5-суммы с суммой, полученной для действия
        текущего шаблона."""
        if not self._empty_name:
            path, name = os.path.split(target_path)
        else:
            path = os.path.dirname(target_path)
            name = ''

        name_pattern = re.compile(rf"^{name}\d+x\d+")
        md5sum = None
        images = []

        # Проверяем содержимое целевой директории.
        for node in os.scandir(path):
            if node.is_file():
                if (node.name ==
                        f"{name.strip('-_.')}{'.' if name else ''}md5sum"):
                    md5sum = node.name
                elif (node.name == name
                      or (name_pattern.search(node.name) is not None)):
                    images.append(node.name)

        if not images and md5sum is None:
            # Если нет файла суммы и нет изображений -- продолжаем выполнение
            # шаблона.
            return True
        elif not images and md5sum is not None:
            # Если есть файл суммы, но нет изображений -- удаляем файл суммы.
            md5sum_path = os.path.join(path, md5sum)
            os.unlink(md5sum_path)
            self.changed_files[md5sum_path] = 'D'
            return True
        elif images and md5sum is None:
            # Если есть файлы изображений, но нет суммы -- удаляем файлы
            # изображений.
            for image in images:
                image_path = os.path.join(path, image)
                os.unlink(image_path)
                self.changed_files[image_path] = 'D'
            return True
        else:
            # Сравниваем суммы из md5sum и для текущего действия если они
            # сходятся, то делать ничего не надо, если нет -- удаляем
            # имеющиеся изображения и md5-сумму.
            with open(os.path.join(path, md5sum), "r") as md5sum_file:
                current_md5sum = md5sum_file.read().strip()
            if current_md5sum != action_md5sum:
                for image in images:
                    image_path = os.path.join(path, image)
                    os.unlink(image_path)
                    self.changed_files[image_path] = 'D'
                md5sum_path = os.path.join(path, md5sum)
                os.unlink(md5sum_path)
                self.changed_files[md5sum_path] = 'D'
                return True
            else:
                return False

    def _check_source(self, source: str, target_path: str, mirror: bool
                      ) -> bool:
        """Метод для проверки исходного изображения."""
        if not self._empty_name:
            path, name = os.path.split(target_path)
        else:
            path = os.path.dirname(target_path)
            name = ''

        name_pattern = re.compile(rf"^{name}\d+x\d+")

        if not os.path.exists(source):
            if mirror:
                for node in os.scandir(path):
                    if (node.name == name
                       or name_pattern.search(node.name) is not None
                       or node.name == (f"{node.name.strip('_-.')}"
                                        f"{ '.' if name else '' }md5sum")):
                        os.unlink(node.path)
                        self.changed_files[node.path] = "D"
                return False
            else:
                raise FormatError("image from 'source' parameter does not"
                                  f" exist: {source}.")
        return True

    def _get_action_md5sum(self, source: str,
                           resolutions: List[Tuple[int, int]]) -> bool:
        """Метод для получения md5-суммы текущего действия шаблона,
        рассчитываемой из последовательности байтов изображения и списка
        разрешений, в которые данный файл должен быть конвертирован."""
        print("RESOLUTIONS:", resolutions)
        with open(source, "rb") as source_file:
            md5_object = hashlib.md5(source_file.read())
            for width, height in resolutions:
                resolution = f"{width}x{height}"
                md5_object.update(resolution.encode())
        return md5_object.hexdigest()

    def _create_md5sum_file(self, target_path: str, action_md5sum: str
                            ) -> NoReturn:
        """Метод для создания файла с md5-суммой действия, выполненного
        данным шаблоном."""
        if not self._empty_name:
            path, name = os.path.split(target_path)
        else:
            path = os.path.dirname(target_path)
            name = ''

        md5sum_path = (f"{path}/{name.strip('_-.')}"
                       f"{ '.' if name else '' }md5sum")
        with open(md5sum_path, "w") as md5sum_file:
            md5sum_file.write(action_md5sum)
        if md5sum_path in self.changed_files:
            self.changed_files[md5sum_path] = 'M'
        else:
            self.changed_files[md5sum_path] = 'N'

    def _check_stretch(self, source_resolution: Tuple[int, int],
                       resolution: Tuple[int, int],
                       stretch: bool) -> bool:
        """Метод определяющий необходимость растягивания исходного изображения
        и делающий вывод о том, возможно ли создание изображения заданного
        разрешения исходя из значения параметра stretch."""
        return (stretch
                or (source_resolution[0] >= resolution[0]
                    and source_resolution[1] >= resolution[1]))

    @property
    def warnings(self):
        return self._warnings
