class Driver:
    def __init__(self, argv: list, argc: int):
        """
		argv: ./__main__.py [SOURCE] [ALGORITHM] [MODE] [COMPRESSION] [OVERFLOW] [TARGET]
		argc: number of command line arguments

		Methods:
			validImageFile
			validExtension
			validCompression
			getValidInput
			saveLog
			run
		"""
        self.name = argv[0]
        self.validExt = (".jpg", ".jpeg", ".png", ".tif")
        self.initialized = False

        if argc == 2:
            print(self.usage() + "\n")
        elif argc == 1 or argc > 8:
            print(self.usage())
            return

        print("Initializing...")

        self.imgName = None
        self.source = None
        self.path = None
        self.algorithm = None
        self.mode = None
        self.compression = None
        self.overflow = False
        self.shouldLog = False
        self.target = None

        self.resourcePath = os.path.join(os.getcwd(), "input")
        self.outputPath = os.path.join(os.getcwd(), "output")

        self.variances = {
            "low": 90.,
            "medium": 95.,
            "high": 99.99,
        }

        # parse cmd line arguments and get user input
        if argc >= 2:
            imgName = argv[1]
            self.source = argv[1]  # will get changed to full path later
            if argc >= 3:
                self.algorithm = argv[2]
                if argc >= 4:
                    self.mode = argv[3]
                    if argc >= 5:
                        self.compression = argv[4]
                        if argc >= 6:
                            try:
                                self.overflow = bool(int(argv[5]))
                            except:
                                self.overflow = not bool(
                                    self.getValidInput(
                                        "\nAllow overflow?\n\t1) True\n\t2) False",
                                        int,
                                        valid={1, 2}) - 1)
                            if argc >= 7:
                                try:
                                    self.shouldLog = bool(int(argv[6]))
                                except:
                                    self.shouldLog = not bool(
                                        self.getValidInput(
                                            "\nLog data?\n\t1) True\n\t2) False: ",
                                            int,
                                            valid={1, 2}) - 1)
                                if argc == 8:
                                    self.target = argv[7]
                                    if not self.validExtension(self.target):
                                        self.target = self.getValidInput(
                                            "\nInput valid target file name: ",
                                            str,
                                            isValid=self.validExtension)

        if self.source is None or not self.validImageFile(
                self.source):  # get valid image file name
            available = os.listdir(self.resourcePath)

            if len(available) == 0:
                print(
                    "No images in \"images\" folder. Add more and try again..."
                )
                return

            options = ""

            for imageName in available:
                options += "\n\t- " + imageName

            self.source = self.getValidInput(
                "\nSelect valid file from \"images\" folder." + options,
                str,
                isValid=self.validImageFile)

        self.path = os.path.join(self.resourcePath, self.source)

        self.imgName = ""

        for l in imgName:
            if l != ".":
                self.imgName += l
            else:
                break

        self.image = Image(self.imgName, self.path)

        if self.algorithm is None or self.algorithm not in {
                "pca", "svd"
        }:  # check which algorithm user wants to use
            choice = self.getValidInput(
                "\nWhich algorithm would you like to use?\n\t1) PCA\n\t2) SVD",
                int,
                valid={1, 2})
            if choice == 1:
                self.algorithm = "pca"
            else:  # choice == 2
                self.algorithm = "svd"

        if self.mode is None or self.mode not in {
                "v", "c", "q"
        }:  # check which mode user wants to use
            if self.algorithm == "pca":
                choice = self.getValidInput(
                    "\nHow would you like to determine compression?\n\tv) Variance\n\tc) Component\n\tq) Qualitative",
                    str,
                    valid={"v", "c", "q"})
            else:  # self.algorithm == "svd"
                choice = "c"
            self.mode = choice

        if self.algorithm == "svd" and not self.mode == "c":
            print("Invalid mode for SVD: " + str(self.mode) +
                  "\n\t- setting mode to \"c\", \"components\"")
            self.mode = "c"
            self.compression = None

        if self.compression is None or not self.validCompression(
                self.mode, self.compression):  # get value for compression
            if self.mode == "v":  # mode is variance
                choice = self.getValidInput(
                    "\nHow much variance would you like to retain? (0, 100): ",
                    float,
                    lower=0,
                    upper=100)

            elif self.mode == "c":  # mode is components
                choice = self.getValidInput(
                    "\nHow many components would you like to use? (0, " +
                    str(self.image.data.shape[0]) + "): ",
                    int,
                    lower=0,
                    upper=self.image.data.shape[0])

            else:  # mode is qualitative
                quality = {
                    1: "high",
                    2: "medium",
                    3: "low",
                }

                choice = quality[self.getValidInput(
                    "\nHow much quality would you like?\n\t1) High\n\t2) Medium\n\t3) Low",
                    int,
                    valid={1, 2, 3})]

            self.compression = choice

        elif self.mode == "v":  # compression passed through command line
            self.compression = float(self.compression)

        elif self.mode == "c":  # compression passed through command line
            self.compression = int(self.compression)

        if self.mode == "q":
            self.compression = self.variances[self.compression]
            self.mode = "v"

        self.initialized = True

    def usage(self) -> str:
        return f"""\
{"o"+"="*85+"o"}
{"IMAGE COMPRESSOR!!!"}

Runs lossy image compression on images with choice parameters.

Usage:
	{self.name} [SOURCE] [ALGORITHM] [MODE] [COMPRESSION] [OVERFLOW] [TARGET]
	{self.name} [SOURCE] [ALGORITHM] [MODE] [COMPRESSION] [OVERFLOW]
	{self.name} [SOURCE] [ALGORITHM] [MODE] [COMPRESSION]
	{self.name} [SOURCE] [ALGORITHM] [MODE]
	{self.name} [SOURCE]

Note: If the former is used, user will complete parameters through terminal

Run image compression on image with valid extension: {self.validExt}

Algorithms:
	pca: Principal Component Analysis
	svd: Singular Value Decomposition

Modes:
	v: Select a percentage of variance to keep. Only valid if mode=pca.    
	c: Select a number of components to keep.
	q: Select compression level from predefined settings. Only valid if mode=pca.  

Compression ranges:
	Given mode...
		v -> float between 0 and 100
		c -> int
		q -> Either low, medium, or high quality

Overflow:
	1: True, values of pixels on one or more channels may overflow, resulting in cool noise
	0: False, prevent cool noise but retain maximum image quality
	Optional

Log:
	1: True, log data to logs/
	0: False do not log data to logs/
	Optional

Target:
	file name: name with valid extension which will be saved to "output" folder
	Optional

Examples:
	{self.name} tiger.jpg pca 1 95 1 0 tiger_pca_v_95.tif
	{self.name} flower.jpg svd 3 min 1
	{self.name} knight.png

{"o"+"="*85+"o"}"""

    def validImageFile(self, fileName: str) -> bool:
        """
		Determine whether image file is in "input" folder and whether it has a proper extension

		fileName: name of file w/o full path

		Returns: True/ False
		"""
        availableImages = os.listdir(self.resourcePath)
        exists = fileName in availableImages
        extValid = self.validExtension(fileName)
        return exists and extValid

    def validExtension(self, fileName: str) -> bool:
        """
		Determine whether image file has proper extension

		fileName: name of file w/o full path

		Returns: True/ False
		"""
        extValid = False

        for ext in self.validExt:
            if ext in fileName:
                valid = True
                for i in range(1, len(ext) + 1):
                    if fileName[-1 * i] != ext[-1 * i]:
                        valid = False
                if valid:
                    return True
        return False

    def validCompression(self, mode: str, compression: int or float
                         or str) -> bool:
        """
		Determines whether "compression" value is valid given "mode" parameter

		mode: mode to use, either "v", "v", or "q"
		compression: degree of compression associated with mode

		Returns: True/ False
		"""
        if mode == "v":
            try:
                return (0 <= float(compression) <= 100)
            except ValueError:
                return False

        elif mode == "c":
            try:
                return (0 <= int(compression) <= self.image.data.shape[0])
            except ValueError:
                return False
        else:  # mode == q
            return compression in {"low", "medium", "high"}

    def getValidInput(self,
                      msg: str,
                      dtype: any,
                      lower: float = None,
                      upper: float = None,
                      valid: set = None,
                      isValid: callable = None) -> any:
        """
		Gets input from user contrained by parameters
		
		msg: message to print out to user requesting input
		dtype: type that input will get converted to
		lower: numerical lower bound
		upper: numerical upper bound
		valid: set of possible valid inputs
		isValid: function returning bool to determine if input is valid

		Returns: valid input
		"""
        print(msg)
        while True:
            try:
                choice = dtype(input("\nChoice: "))
            except ValueError:
                continue

            if (lower is not None and choice < lower) or \
             (upper is not None and choice > upper) or \
             (valid is not None and choice not in valid) or \
             (isValid is not None and not isValid(choice)):
                continue

            return choice

    def saveLog(self, name: str, data: list):
        """
		Save log data to text file in "logs" folder
		
		name: name of log
		data: data to write to log file
		"""
        path = "logs/" + name + ".txt"

        f = open(path, "w+")
        print("Writing log files to " + path)
        for pt in data:
            f.write(str(pt[0]) + " " + str(pt[1]) + "\n")
        f.close()

    def run(self):
        """
		Run compression, handle saving image and logging data.
		"""
        if not self.initialized:
            return

        logs = self.image.compress(self.algorithm,
                                   self.mode,
                                   self.compression,
                                   overflow=self.overflow)

        print()

        if self.shouldLog:
            for channel, log in logs.items():
                name = self.image.name + "_" + self.algorithm + "_" + self.mode + "_" + str(
                    self.compression).replace(".",
                                              "-") + "_" + channel + "_" + str(
                                                  int(self.overflow))
                self.saveLog(name, log)

        if self.target is not None:
            path = os.path.join(self.outputPath, self.target)
            self.image.save(path)
        else:
            print("\nShowing image...")
            self.image.show()