"""Module for generating QR codes to configure a study for the CARWatch app."""
import sys
from pathlib import Path
from typing import Optional, Sequence, Union

import qrcode
from qrcode.image.pil import PilImage

from carwatch.utils.study import Study
from carwatch.utils.utils import assert_is_dir, sanitize_str_for_qr


class QrCodeGenerator:
    """Class that is used to generate a QR-Code including all relevant study information for the CARWatch app.

    The QR-Code can be scanned by the CARWatch app to configure the app for a specific study.

    """

    def __init__(
        self,
        study: Study,
        saliva_distances: Union[int, Sequence[int]],
        contact_email: str,
        check_duplicates: bool = False,
        enable_manual_scan: bool = False,
    ):
        """Generate QR-Code encoding all relevant study information.

        Parameters
        ----------
        study: :obj:`~carwatch.utils.Study`
           Study object for which QR-Code will be created
        saliva_distances: int or list
            Time distances between morning samples in minutes ordered chronologically.
            For s morning saliva samples, s-1 saliva distances need to be specified.
            If the distances are constant between all morning samples,
        contact_email: str
            E-mail address that App log data will be shared with
        check_duplicates: bool
            If set to ``True``, the CAR Watch app will check and alert if a barcode has been checked twice
        enable_manual_scan: bool
            If set to ``True``, the CAR Watch app will allow manual barcode scanning apart from timed alarms

        """
        self.study = study
        self.saliva_distances = self._sanitize_distances(saliva_distances)
        self.contact = contact_email
        self.check_duplicates = check_duplicates
        self.enable_manual_scan = enable_manual_scan
        self.output_dir = None
        self.output_name = f"qr_code_{self.study.study_name}"

    def generate(self, output_dir: str = ".", output_name: Optional[str] = None):  # pragma: no cover
        """Generate a `*.png` file with QR code according to the properties of the created ``QrCodeGenerator``.

        Parameters
        ----------
        output_dir: str
            Path to the directory where the generated QR-Code will be stored
        output_name: str, optional
            Filename of the generated QR-Code.

        """
        output_dir = Path(output_dir)
        output_name = Path(output_name)
        try:
            assert_is_dir(output_dir)
        except ValueError as e:
            print(e)
            sys.exit(1)
        self.output_dir = output_dir
        if output_name:
            if output_name.suffix == ".png":
                # store without file ending
                output_name = output_name.stem
            self.output_name = output_name
        qr_img = self._generate_qr_code()
        self._save_qr_img(qr_img)

    def _generate_qr_code(self) -> PilImage:
        """Generate a QR-Code with the study information.

        This method translates the study data into a QR-Code. The information is encoded in the following format:

        ``CARWATCH;N:<study_name>;D:<num_days>;S:<subject_list>;T:<saliva_distances>;
        E:<has_evening_sample>;M:<contact_email>;F:<check_duplicates>;``

        Returns
        -------
        :obj:`qrcode.image.pil.PilImage`
            QR-Code as PIL image

        """
        # sanitize inputs to prevent decoding issues
        forbidden = [";", ":", ","]
        study_name = sanitize_str_for_qr(self.study.study_name, forbidden)
        subject_names = []
        for name in self.study.subject_names:
            sanitized_name = sanitize_str_for_qr(name, forbidden)
            subject_names.append(sanitized_name)

        app_id = "CARWATCH"
        name_string = ",".join(str(dist) for dist in subject_names)
        start_sample = 0 if self.study.start_sample_from_zero else 1
        start_sample = f"{self.study.sample_prefix}{start_sample}"
        distance_string = ",".join(str(dist) for dist in self.saliva_distances)

        # create encoding
        data = (
            f"{app_id};"
            f"N:{study_name};"
            f"D:{self.study.num_days};"
            f"S:{name_string};"
            f"SS:{start_sample};"
            f"T:{distance_string};"
            f"E:{int(self.study.has_evening_sample)};"
            f"M:{self.contact};"
            f"FD:{int(self.check_duplicates)};"
            f"FM:{int(self.enable_manual_scan)}"
        )
        # create qr code
        img = qrcode.make(data)
        return img

    def _save_qr_img(self, qr_img):
        img_location = self.output_dir.joinpath(f"{self.output_name}.png")
        qr_img.save(img_location)

    def _sanitize_distances(self, saliva_distances: Union[int, Sequence[int]]) -> Sequence[int]:
        if isinstance(saliva_distances, int):
            return [saliva_distances] * (self.study.num_samples - 1)
        if isinstance(saliva_distances, list):
            # all elements are ints
            if all(isinstance(dist, int) for dist in saliva_distances):
                # length is correct
                if len(saliva_distances) == self.study.num_samples - 1:
                    return saliva_distances
                raise ValueError(
                    f"Incorrect number of saliva distances provided! "
                    f"Needs to be {self.study.num_samples - 1}, as "
                    f"{self.study.num_samples} samples will be taken throughout the day."
                )
            raise ValueError("Invalid data detected in saliva distances! All values need to be integers!")
        raise ValueError("Saliva distances data type is invalid! Needs to be int or list of ints.")
