# Validate option parsed from CommandParser in command.py
import copy
import os
import sys
import yaml
import builtins
import datetime
import platform
import re
from funid.src.logics import isvalidcolor
from funid.src.tool import check_avx


# FunID Option class definition
class Option:

    # Method option
    class Method_Option:
        def __init__(self):
            self.search = "mmseqs"
            self.alignment = "mafft"
            self.trim = "trimal"
            self.modeltest = "none"
            self.tree = "fasttree"

    # Visualize option
    class Visualize_Option:
        def __init__(self):
            self.bscutoff = 70
            self.fullgenus = False
            self.highlight = "#aa0000"
            self.heightmultiplier = 6
            self.maxwordlength = 48
            self.backgroundcolor = ["#f4f4f4", "#c6c6c6"]
            self.outgroupcolor = "#999999"
            self.ftype = "Arial"
            self.fsize = 10
            self.fsize_bootstrap = 9

    # Cluster option
    class Cluster_Option:
        def __init__(self):
            self.cutoff = 0.95
            self.evalue = 0.00001
            self.wordsize = 7

    # MAFFT option
    class MAFFT_Option:
        def __init__(self):
            self.algorithm = "auto"
            self.op = 1.3
            self.ep = 0.1

    # TrimAl option
    class TrimAl_Option:
        def __init__(self):
            self.algorithm = "gt"
            self.gt = 0.2  # Should be found

    # Option will be generated by file, and iteratively
    def __init__(self):

        # Running options
        self.query = []
        self.db = []
        self.gene = []
        self.email = ""
        self.api = ""
        self.test = None
        self.thread = "auto"
        self.outdir = None
        self.runname = None
        self.mode = "identification"
        self.continue_from_previous = False
        self.criterion = "BIC"
        self.step = ""
        self.level = "genus"
        self.queryonly = False
        self.confident = True
        self.verbose = 2
        self.maxoutgroup = 3
        self.collapsedistcutoff = 0.01
        self.collapsebscutoff = 101
        self.bootstrap = 100
        self.solveflat = True
        self.regex = None
        self.avx = True
        self.cachedb = True
        self.usecache = True
        self.matrixformat = "csv"
        self.nosearchmatrix = False
        self.nosearchresult = False

        # Method options
        self.method = self.Method_Option()

        # Visualization options
        self.visualize = self.Visualize_Option()

        # Cluster options
        self.cluster = self.Cluster_Option()

        # MAFFT options
        self.mafft = self.MAFFT_Option()

        # TrimAl options
        self.trimal = self.TrimAl_Option()

    # update values from given preset
    def update_from_preset(self, preset_file):

        # Try to parser preset file
        with open(preset_file) as f:
            try:
                parser_dict = yaml.safe_load(f)
            except:
                print(f"{preset_file} is not a valid preset yaml file")
                raise Exception

        # Update loaded preset
        for key in parser_dict:
            # Basic options
            if key.lower() in ("query"):
                self.query = parser_dict[key]
            elif key.lower() in ("db"):
                self.db = parser_dict[key]
            elif key.lower() in ("gene"):
                self.gene = parser_dict[key]
            elif key.lower() in ("email"):
                self.email = parser_dict[key]
            elif key.lower() in ("api"):
                self.api = parser_dict[key]
            elif key.lower() in ("thread"):
                self.thread = parser_dict[key]
            elif key.lower() in ("outdir"):
                self.outdir = parser_dict[key]
            elif key.lower() in ("runname"):
                self.runname = parser_dict[key]
            elif key.lower() in ("mode"):
                self.mode = parser_dict[key]
            elif key.lower() in ("continue"):
                self.continue_from_previous = parser_dict[key]
            elif key.lower() in ("step"):
                self.step = parser_dict[key]
            elif key.lower() in ("level"):
                self.level = parser_dict[key]
            elif key.lower() in ("queryonly"):
                self.queryonly = parser_dict[key]
            elif key.lower() in ("verbose"):
                self.verbose = parser_dict[key]
            elif key.lower() in ("maxoutgroup"):
                self.maxoutgroup = parser_dict[key]
            elif key.lower() in ("collapsedistcutff"):
                self.collapsedistcutoff = parser_dict[key]
            elif key.lower() in ("collapsebscutoff"):
                self.collapsebscutoff = parser_dict[key]
            elif key.lower() in ("bootstrap"):
                self.bootstrap = parser_dict[key]
            elif key.lower() in ("solveflat"):
                self.solveflat = parser_dict[key]
            elif key.lower() in ("regex"):
                self.regex = parser_dict[key]
            elif key.lower() in ("avx"):
                self.avx = parser_dict[key]
            elif key.lower() in ("criterion"):
                self.criterion = parser_dict[key]
            elif key.lower() in ("cachedb"):
                self.cachedb = parser_dict[key]
            elif key.lower() in ("usecache"):
                self.usecache = parser_dict[key]
            elif key.lower() in ("matrixformat"):
                self.matrixformat = parser_dict[key]
            elif key.lower() in ("nosearchmatrix"):
                self.nosearchmatrix = parser_dict[key]
            elif key.lower() in ("nosearchresult"):
                self.nosearchresult = parser_dict[key]

            # Method options
            elif key.lower() in ("search"):
                self.method.search = parser_dict[key]
            elif key.lower() in ("alignment"):
                self.method.alignment = parser_dict[key]
            elif key.lower() in ("trim"):
                self.method.trim = parser_dict[key]
            elif key.lower() in ("modeltest"):
                self.method.modeltest = parser_dict[key]
            elif key.lower() in ("tree"):
                self.method.tree = parser_dict[key]

            # Visualize options
            elif key.lower() in ("bscutoff"):
                self.visualize.bscutoff = parser_dict[key]
            elif key.lower() in ("bootstrapcutoff"):
                self.visualize.bscutoff = parser_dict[key]
            elif key.lower() in ("fullgenus"):
                self.visualize.fullgenus = parser_dict[key]
            elif key.lower() in ("highlight"):
                self.visualize.highlight = parser_dict[key]
            elif key.lower() in ("heightmultiplier"):
                self.visualize.heightmultiplier = parser_dict[key]
            elif key.lower() in ("maxwordlength"):
                self.visualize.maxwordlength = parser_dict[key]
            elif key.lower() in ("backgroundcolor"):
                self.visualize.backgroundcolor = parser_dict[key]
            elif key.lower() in ("outgroupcolor"):
                self.visualize.outgroupcolor = parser_dict[key]
            elif key.lower() in ("ftype"):
                self.visualize.ftype = parser_dict[key]
            elif key.lower() in ("fsize"):
                self.visualize.fsize = parser_dict[key]
            elif key.lower() in ("fsize_bootstrap"):
                self.visualize.fsize_bootstrap = parser_dict[key]

            # Cluster options
            elif key.lower() in ("evalue"):
                self.cluster.evalue = parser_dict[key]
            elif key.lower() in ("wordsize"):
                self.cluster.wordsize = parser_dict[key]

            # MAFFT options
            elif key.lower() in ("mafft-algorithm"):
                self.mafft.algorithm = parser_dict[key]
            elif key.lower() in ("mafft-op"):
                self.mafft.op = parser_dict[key]
            elif key.lower() in ("mafft-ep"):
                self.mafft.ep = parser_dict[key]

            # TrimAl options
            elif key.lower() in ("trimal-algorithm"):
                self.trimal.algorithm = parser_dict[key]
            elif key.lower() in ("trimal-gt"):
                self.trimal.gt = parser_dict[key]
            else:
                print(f"cannot recoginze {key} in preset file as valid variable")

    # update values from parser object
    def update_from_parser(self, parser):

        # Parsed terms will have higher priority from preset file
        # test should not be parsed here (should be parsed in initialize option)

        try:
            if not parser.query is None:
                self.query = parser.query
        except:
            pass

        try:
            if not parser.db is None:
                self.db = parser.db
        except:
            pass

        try:
            if not parser.gene is None:
                self.gene = parser.gene
        except:
            pass

        try:
            if not parser.email is None:
                self.email = parser.email
        except:
            pass

        try:
            if not parser.api is None:
                self.api = parser.api
        except:
            pass

        try:
            if not parser.thread is None:
                self.thread = parser.thread
        except:
            pass

        try:
            if not parser.runname is None:
                self.runname = parser.runname
        except:
            pass

        # Create runname directory in outdir location
        try:
            if not parser.outdir is None:
                self.outdir = parser.outdir
        except:
            pass

        try:
            if not parser.mode is None:
                self.mode = parser.mode
        except:
            pass

        try:
            if not parser.continue_from_previous is None:
                self.continue_from_previous = parser.continue_from_previous
        except:
            pass

        try:
            if not parser.step is None:
                self.step = parser.step
        except:
            pass

        try:
            if not parser.level is None:
                self.level = parser.level
        except:
            pass

        try:
            if parser.queryonly is True:
                self.queryonly = parser.queryonly
        except:
            pass

        try:
            if not parser.confident is None:
                self.confident = parser.confident
        except:
            pass

        try:
            if not parser.search is None:
                self.method.search = parser.search
        except:
            pass

        try:
            if not parser.alignment is None:
                self.method.alignment = parser.alignment
        except:
            pass

        try:
            if not parser.trim is None:
                self.method.trim = parser.trim
        except:
            pass

        try:
            if not parser.modeltest is None:
                self.method.modeltest = parser.modeltest
        except:
            pass

        try:
            if not parser.tree is None:
                self.method.tree = parser.tree
        except:
            pass

        try:
            if not parser.visualize.bscutoff is None:
                self.visualize.bscutoff = parser.bscutoff
        except:
            pass

        try:
            if parser.visualize.fullgenus is True:
                self.visualize.fullgenus = parser.fullgenus
        except:
            pass

        try:
            if not parser.highlight is None:
                self.visualize.highlight = parser.highlight
        except:
            pass

        try:
            if not parser.heightmultiplier is None:
                self.visualize.heightmultiplier = parser.heightmultiplier
        except:
            pass

        try:
            if not parser.maxwordlength is None:
                self.visualize.maxwordlength = parser.maxwordlength
        except:
            pass

        try:
            if not parser.backgroundcolor is None:
                self.visualize.backgroundcolor = parser.backgroundcolor
        except:
            pass

        try:
            if not parser.outgroupcolor is None:
                self.visualize.outgroupcolor = parser.outgroupcolor
        except:
            pass

        try:
            if not parser.ftype is None:
                self.visualize.ftype = parser.ftype
        except:
            pass

        try:
            if not parser.fsize is None:
                self.visualize.fsize = parser.fsize
        except:
            pass

        try:
            if not parser.fsize_bootstrap is None:
                self.visualize.fsize_bootstrap = parser.fsize_bootstrap
        except:
            pass

        try:
            if not parser.verbose is None:
                self.verbose = parser.verbose
        except:
            pass

        try:
            if not parser.maxoutgroup is None:
                self.maxoutgroup = parser.maxoutgroup
        except:
            pass

        try:
            if not parser.collapsedistcutoff is None:
                self.collapsedistcutoff = parser.collapseddistcutoff
        except:
            pass

        try:
            if not parser.collapsebscutoff is None:
                self.collapsebscutoff = parser.collapsebscutoff
        except:
            pass

        try:
            if not parser.bootstrap is None:
                self.bootstrap = parser.bootstrap
        except:
            pass

        try:
            if parser.solveflat is True:
                self.solveflat = parser.solveflat
        except:
            pass

        try:
            if not parser.regex is None:
                self.regex = parser.regex
        except:
            pass

        try:
            if not parser.cluster_cutoff is None:
                self.cluster.cutoff = parser.cluster_cutoff
        except:
            pass

        try:
            if not parser.cluster_evalue is None:
                self.cluster.evalue = parser.cluster_evalue
        except:
            pass

        try:
            if not parser.cluster_wordsize is None:
                self.cluster.wordsize = parser.cluster_wordsize
        except:
            pass

        try:
            if not parser.mafft_algorithm is None:
                self.mafft.algorithm = parser.mafft_algorithm
        except:
            pass

        try:
            if not parser.mafft_op is None:
                self.mafft.op = parser.mafft_op
        except:
            pass

        try:
            if not parser.mafft_ep is None:
                self.mafft.ep = parser.mafft_ep
        except:
            pass

        try:
            if not parser.trimal_algorithm is None:
                self.trimal.algorithm = parser.trimal_algorithm
        except:
            pass

        try:
            if not parser.trimal_gt is None:
                self.trimal.gt = parser.trimal_gt
        except:
            pass

        try:
            if parser.noavx is True:
                self.avx = False
        except:
            pass

        try:
            if not parser.criterion is None:
                self.criterion = parser.criterion
        except:
            pass

        try:
            if parser.cachedb is True:
                self.cachedb = parser.cachedb
        except:
            pass

        try:
            if parser.usecache is True:
                self.usecache = parser.usecache
        except:
            pass

        try:
            if not parser.matrixformat is None:
                self.matrixformat = parser.matrixformat
        except:
            pass

        try:
            if parser.nosearchmatrix is True:
                self.nosearchmatrix = parser.nosearchmatrix
        except:
            pass

        try:
            if parser.nosearchresult is True:
                self.nosearchresult = parser.nosearchresult
        except:
            pass

    # Valiate current status preset
    def validate(self):

        list_info = []
        list_error = []
        list_warning = []

        # query
        # Check if query files are in valid directories
        if not (type(self.query) is list):
            list_error.append(f"Type for query should be list format")
        else:
            flag_query = 0
            for query in self.query:
                if not (type(query) is str):
                    list_error.append(f"query {query} is not a valid string format")
                    flag_query = 1

            if flag_query == 0:
                # Adjust query location if query is in test dataset
                if not (self.test is None):
                    path_query = os.path.abspath(
                        f"{os.path.dirname(__file__)}/../test_dataset/{self.test}/Query"
                    )

                    self.query = [f"{path_query}/{query}" for query in self.query]

                for query in self.query:
                    # Check if query location is valid
                    try:
                        if not (os.path.exists(f"{query}")):
                            list_error.append(f"{query} is not a valid query path")
                    except:
                        pass

        # db
        # Check if db files are in valid
        if not (type(self.db) is list):
            list_error.append(f"Type for db should be list format")
        else:
            flag_db = 0
            for db in self.db:
                if not (type(db) is str):
                    list_error.append(f"db {db} is not a valid string format")
                    flag_db = 1

            if flag_db == 0:
                # Adjust db location if db is in test dataset
                if not (self.test is None):
                    path_db = os.path.abspath(
                        f"{os.path.dirname(__file__)}/../test_dataset/{self.test}/DB"
                    )
                    self.db = [f"{path_db}/{db}" for db in self.db]

                for db in self.db:
                    # Check if DB location is valid
                    try:
                        if not (os.path.exists(f"{db}")):
                            list_error.append(f"{db} is not a valid query path")
                    except:
                        pass

        # Change all db path into linux style

        # gene
        # Check if gene names are valid strings (comparing with db will be performed in parsing)
        if not (type(self.gene) is list):
            list_error.append(f"Type for gene should be list format")
        elif len(self.gene) < 1:
            list_error.append(f"At least one gene should be designated")
        else:
            for gene in self.gene:
                if not (type(gene) is str):
                    list_error.append(f"gene {gene} is not a valid string format")

        # email
        # Check if email is in valid format
        if self.email is None:
            pass
        else:
            if not (type(self.email) is str):
                list_error.append(f"Type for email should be string")
            email_pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
            if not (re.match(email_pattern, self.email)):
                list_error.append(f"Email {self.email} is not a valid email")

        # api
        # Check if api number is in valid format
        # Connect to entrez once to check api is valid
        if self.api is None:
            pass
        else:
            if not (type(self.api) is str):
                list_error.append(f"Type for api should be string")

        # Check either email or api exists
        if self.email is None and self.api is None:
            list_warning.append(
                f"Email and API both were not given. May cause error when downloading sequence"
            )

        # thread
        # Check if thread is valid int
        # If thread is over system thread, 0, or negative, adjust it to maximum
        if not (type(self.thread) is int):
            if not (type(self.thread) is str):
                list_warning.append(
                    f"Type for thread should be int but {self.thread} was given. Using {os.cpu_count()} for default"
                )
            else:
                if not (self.thread.lower() == "auto"):
                    list_warning.append(
                        f"Type for thread should be int but {self.thread} was given. Using {os.cpu_count()} for default"
                    )

            self.thread = os.cpu_count()
        elif self.thread <= 0:
            list_info.append(f"thread adjusted to {os.cpu_count()}")
            self.thread = os.cpu_count()
        elif self.thread >= os.cpu_count():
            list_info.append(
                f"thread exceeded system maximum. Adjusting to {os.cpu_count()}"
            )
            self.thread = os.cpu_count()

        # outdir
        # Check if outdirectory is valid path
        # If outdir does not exists, try making directory
        if self.outdir:
            if not (os.path.exists(self.outdir)):
                list_warning.append(
                    f"Out directory location {self.outdir} does not exists try making it"
                )
                try:
                    os.makedirs(self.outdir, mode=0o755, exist_ok=True)
                except:
                    list_error.append(
                        f"Failed making outdir location {self.outdir}. Try checking directory or permissions"
                    )

                # Change outdir to absolute path
                self.outdir = str(os.path.abspath(self.outdir))

        # continue - continue should be validated before runname designated
        # If continue is not None, give True, else, give False
        # If continue is true, check if runname directory is valid
        # If outdir is not distinctively selected, warn that it will overwrite previous run
        if self.continue_from_previous is False or self.continue_from_previous is None:
            self.continue_from_previous = False
        else:
            self.continue_from_previous = True

        # runname
        # Check if runname is valid string
        # Check for existing runname
        invalid_char = r'[\\/:*?"<>|]|\.|\s$'

        # Use current time stamp if no runname given
        if self.runname is None:
            now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
            self.runname = now

        if not (type(self.runname) is str):
            list_error.append(f"runname should be string")
        elif re.search(invalid_char, self.runname.strip()):
            list_error.append(f"invalid characters in runname")
        else:  # if valid runname
            if self.continue_from_previous is True:
                # Check if continue available
                if not (os.path.exists(f"{self.outdir}/{self.runname}")):
                    list_error.append(
                        f"continue option selected, but previous run directory {self.runname} not found in {self.outdir}"
                    )
            else:
                # If already previous path exists
                if os.path.exists(f"{self.outdir}/{self.runname}"):
                    # memo user designated runname
                    ori_runname = self.runname
                    # if same name exists, try to add numbers at the end to discriminate
                    i = 1
                    while 1:
                        if os.path.exists(f"{self.outdir}/{ori_runname}_{i}"):
                            i += 1
                        else:
                            self.runname = f"{ori_runname}_{i}"
                            break

        # step
        # Check if continue is True. If False, warn that step will be ignored
        # Check if step is valid :
        step = [
            "setup",
            "search",
            "cluster",
            "align",
            "trim",
            "concatenate",
            "modeltest",
            "tree",
            "visualize",
            "report",
        ]

        if self.continue_from_previous is True:
            if not (type(self.step) is str):
                list_error.append(f"step should be string")
            elif not (self.step in step):
                list_error.append(f"step should be one of {str(step)}")
        else:
            if not self.step is None:
                list_warning.append(
                    f"As --continue is not designated, step will be ignored"
                )

        # level
        """
        # Check for valid level : subseries, series, subsection, section, subtribe, 
        tribe, subfamily, family, suborder, order, subclass, class, subdivision, 
        division, subphylum, phylum, subkingdom, kingdom
        """
        level = [
            "subseries",
            "series",
            "subsection",
            "section",
            "genus",
            "subtribe",
            "tribe",
            "subfamily",
            "family",
            "suborder",
            "order",
            "subclass",
            "class",
            "subdivision",
            "division",
            "subphylum",
            "phylum",
            "subkingdom",
            "kingdom",
        ]

        if not (type(self.level) is str):
            list_error.append(f"--level should be string")
        elif not (self.level in level):
            list_error.append(
                f"--level should be one of {str(level)}, not {self.level}"
            )

        # mode
        # Check for valid mode : identificaion or validation
        mode = ["identification", "validation"]
        if not (type(self.mode) is str):
            list_error.append(f"--mode should be string")
        elif not (self.mode in mode):
            list_error.append(f"--mode should be one of {str(mode)}")

        # queryonly
        # If queryonly is not None, give True, else, give False
        # If no query is empty, raise Exception
        if not (self.queryonly is None or self.queryonly is False):
            self.queryonly = True
        else:
            self.queryonly = False

        if self.query:
            if len(self.query) == 0:
                list_warning.append(f"No query detected, ignoring --queryonly")
                self.queryonly = False
        else:
            list_warning.append(f"No query detected, ignoring --queryonly")
            self.queryonly = False

        # confident
        # If confident is not None, give True, else, give False
        # "confident" option can be only used when queryonly is True
        if not (self.confident is None or self.confident is False):
            if self.queryonly is True:
                self.confident = True
            else:
                list_warning.append(
                    f"Option --confident can be only used when --queryonly is True. Ignoring it"
                )
                self.confident = False
        else:
            self.confident = False

        # search
        # Check if search method is one of default, blast, mmseqs
        # Adjust misspellings
        # If given value is default, change it to mmseqs
        # If does not met to any of above, raise Exception
        search = ["blast", "mmseqs"]
        search_adjust = {
            "default": "blast",
            "blast": "blast",
            "blastn": "blast",
            "mmseq": "mmseqs",
            "mmseqs": "mmseqs",
            "mmseq2": "mmseqs",
            "mmseqs2": "mmseqs",
        }
        if not (type(self.method.search) is str):
            list_error.append(f"search method should be string")
        else:
            self.method.search = search_adjust[self.method.search.lower()]
            if not (self.method.search in search):
                list_error.append(f"search method should be one of {str(search)}")

        # alignment
        # Check if alignment method is one of default, mafft
        # Adjust misspellings
        # If given value is default, change it to mafft
        # If does not met to any of above, raise Exception
        alignment = ["mafft"]
        alignment_adjust = {
            "default": "mafft",
            "mafft": "mafft",
        }
        if not (type(self.method.alignment) is str):
            list_error.append(f"align method should be string")
        else:
            self.method.alignment = alignment_adjust[self.method.alignment.lower()]
            if not (self.method.alignment in alignment):
                list_error.append(f"align method should be one of {str(alignment)}")

        # trim
        # Check if trimming method is one of default, none, trimal, gblocks
        # Adjust misspellings
        # If given value is default, change it to none
        # If does not met to any of above, raise Exception
        trim = ["none", "trimal", "gblocks"]
        trim_adjust = {
            "default": "none",
            "trimal": "trimal",
            "trimai": "trimal",
            "gblocks": "gblocks",
            "gblock": "gblocks",
            "none": "none",
        }

        if self.method.trim is None:
            self.method.trim = "none"

        if not (type(self.method.trim) is str):
            list_error.append(f"trim method should be string")
        else:
            self.method.trim = trim_adjust[self.method.trim.lower()]
            if not (self.method.trim in trim):
                list_error.append(
                    f"trim method should be one of {str(trim_adjust.keys())}"
                )

        # model
        # Check if search method is one of default, none, modeltest-ng, iqtree
        # Adjust misspellings
        # If given value is default, change it to none
        # If does not met to any of above, raise Exception
        modeltest = ["none", "modeltest-ng", "iqtree"]
        modeltest_adjust = {
            "default": "none",
            "modeltest": "modeltest-ng",
            "modeltestng": "modeltest-ng",
            "modeltest-ng": "modeltest-ng",
            "jmodeltest": "modeltest-ng",
            "iqtree": "iqtree",
            "none": "none",
        }

        if self.method.modeltest is None:
            self.method.modeltest = "none"

        if not (type(self.method.modeltest) is str):
            list_error.append(f"modeltest method should be string")

        else:
            if self.method.modeltest.lower() == "jmodeltest":
                list_warning.append(
                    f"option jmodeltest will be substituted to modeltest-ng"
                )
            self.method.modeltest = modeltest_adjust[self.method.modeltest.lower()]
            if not (self.method.modeltest in modeltest):
                list_error.append(f"modeltest method should be one of {str(modeltest)}")
            if self.method.modeltest == "modeltest-ng" and sys.platform == "win32":
                list_error.append(
                    f"Modeltest-ng is currently not available in windows platform. Please select other modeltest methods or use linux platform"
                )

        # tree
        # Check if search method is one of default, fasttree, raxml, iqtree
        # Adjust misspellings
        # If given value is default, change it to fasttree
        # If does not met to any of above, raise Exception
        tree = ["fasttree", "iqtree", "raxml"]
        tree_adjust = {
            "default": "fasttree",
            "fasttree": "fasttree",
            "iqtree": "iqtree",
            "raxml": "raxml",
        }

        if not (type(self.method.tree) is str):
            list_error.append(f"tree method should be string")
        else:
            self.method.tree = tree_adjust[self.method.tree.lower()]
            if not (self.method.tree in tree):
                list_error.append(f"tree method should be one of {str(tree)}")

        # bscutoff
        # Check if bootstrap cutoff is in optimal range 0~
        # If lower than 0, set to 0
        # If upper than 100, warn that bootstrap will not be shown
        # If between 0 and 1, multiply 100 and warn
        if not (type(self.visualize.bscutoff) is int):
            try:
                if self.visualize.bscutoff > 0 and self.visualize.bscutoff < 1:
                    list_warning.append(
                        f"bscutoff should be in integer range, but given one seems to between 0 and 1. Automatically multiplying 100"
                    )
                    self.visualize.bscutoff = int(self.visualize.bscutoff * 100)
                    # If failed solving
                    if self.visualize.bscutoff > 0 and self.visualize.bscutoff < 1:
                        list_error.append(f"Failed to solve bscutoff range")

                else:
                    self.visualize.bscutoff = int(self.visualize.bscutoff)

                if self.visualize.bscutoff < 0:
                    self.visualize.bscutoff = 0
                if self.visualize.bscutoff > 100:
                    list_warning.append(
                        f"bscutoff is over 100, all bootstrap will not seen"
                    )

            except:
                list_error.append(f"--bscutoff should be integer")

        # fullgenus
        # If fullgenus is not None, give True, else, give False
        if self.visualize.fullgenus:
            if self.visualize.fullgenus is True:
                self.visualize.fullgenus = True
            else:
                self.visualize.fullgenus = False
        else:
            self.visualize.fullgenus = False

        # highlight
        # highlight should be availabe svg colors or unicode
        if not (type(self.visualize.highlight) is str):
            list_error.append(f"--highlight should be string")
        else:
            if not (isvalidcolor(self.visualize.highlight)):
                list_error.append(
                    f"in --highlight, color {color} does not seems to be valid svg color nor hex code"
                )
            else:
                self.visualize.highlight = self.visualize.highlight.lower()

        # heightmultiplier
        # heightmultiplier should be positive float
        try:
            self.visualize.heightmultiplier = float(self.visualize.heightmultiplier)
            if self.visualize.heightmultiplier <= 0:
                list_warning.append(
                    "--heightmultiplier should be positive, setting to default value, 6"
                )
                self.visualize.heightmultiplier = 6
        except:
            list_error.append(
                "--heightmultiplier should be positive floating point number"
            )

        # maxwordlength
        # maxwordlength should be positive int
        try:
            self.visualize.maxwordlength = int(self.visualize.maxwordlength)
            if self.visualize.maxwordlength <= 0:
                list_warning.append(
                    "--maxwordlength should be positive int, setting to default value, 48"
                )
                self.visualize.maxwordlength = 48
        except:
            list_error.append(
                "--maxwordlength should be positive floating point number"
            )

        # backgroundcolor
        # backgroundcolor should be list of colors
        flag_backgroundcolor = 0
        if not (type(self.visualize.backgroundcolor) is list):
            list_error.append(f"--backgroundcolor should be list")
            flag_backgroundcolor += 1
        elif len(self.visualize.backgroundcolor) < 1:
            list_error.append(
                f"At least one color should be designated for --backgroundcolor"
            )
            flag_backgroundcolor += 1
        else:
            for color in self.visualize.backgroundcolor:
                if not isvalidcolor(color):
                    list_error.append(
                        f"color {color} does not seems to be valid svg color nor hex code"
                    )
                    flag_backgroundcolor += 1
        if flag_backgroundcolor == 0:
            self.visualize.backgroundcolor = [
                x.lower() for x in self.visualize.backgroundcolor
            ]

        # outgroupcolor
        # outgroupcolor should be availabe svg colors or unicode
        if not (type(self.visualize.outgroupcolor) is str):
            list_error.append(f"--outgroupcolor should be string")
        else:
            if not (isvalidcolor(self.visualize.outgroupcolor)):
                list_error.append(
                    f"in --outgroupcolor, color {color} does not seems to be valid svg color nor hex code"
                )
            else:
                self.visualize.outgroupcolor = self.visualize.outgroupcolor.lower()

        # ftype
        if not (type(self.visualize.ftype) is str):
            list_error.append(f"--ftype should be valid font name (string)")
        else:
            # Fix this when enough data has been collected
            pass

        # fsize
        try:
            float(self.visualize.fsize)
            if self.visualize.fsize < 0:
                list_warning.append(
                    f"--fsize should be positive float. Setting to default, 10"
                )
                self.visualize.fsize = 10
        except:

            list_error.append(f"--fsize should be positive float")

        # fsize_bootstrap
        try:
            float(self.visualize.fsize_bootstrap)
            if self.visualize.fsize_bootstrap < 0:
                list_warning.append(
                    f"--fsize_bootstrap should be positive float. Setting to default, 10"
                )
                self.visualize.fsize_bootstrap = 9
        except:
            list_error.append(f"--fsize_bootstrap should be positive float")

        # verbose
        # If not 0,1,2,3 raise error with warning
        # 0: quiet, 1: info, 2: warning, 3: debug, default : 2
        try:
            self.verbose = int(self.verbose)
            if not self.verbose in (0, 1, 2, 3):
                list_error.append(
                    f"verbose should be one of 0,1,2,3 - 0: only error, 1: warning, 2: info, 3: debug"
                )
        except:
            list_error.append(
                f"verbose should be one of 0,1,2,3 - 0: only error, 1: warning, 2: info, 3: debug"
            )

        # maxoutgroup
        # Check if maxoutgroup is in optimal range 0~
        # if lower than 1, change into 1 and warn
        try:
            self.maxoutgroup = int(self.maxoutgroup)
            if self.maxoutgroup < 1:
                list_warning.append(f"invalid maxoutgroup, automatically selecting 1")
        except:
            list_warning.append(f"invalid maxoutgroup, automatically selecting 1")
            self.maxoutgroup = 1

        # collapsedistcutoff
        # Check if collapse distance cutoff is in optimal range 0~
        # if lower than 0, change into 0 and warn
        try:
            self.collapsedistcutoff = float(self.collapsedistcutoff)
        except:
            list_warning.append(f"invalid collapsedistcutoff, change into 0")
            self.collapsedistcutoff = 0

        # collapsebscutoff
        # Check if collapse bootstrap cutoff is in optimal range 0~
        # If higher than 101, change into 100 and warn
        # If lower than 0, change into 0 and warn
        if not (type(self.collapsebscutoff) is int):
            try:
                if self.collapsebscutoff > 0 and self.collapsebscutoff < 1:
                    list_warning.append(
                        f"collapsebscutoff should be in integer range, but given one seems to between 0 and 1. Automatically multiplying 100"
                    )
                    self.collapsebscutoff = int(self.collapsebscutoff * 100)
                    # If failed solving
                    if self.collapsebscutoff > 0 and self.collapsebscutoff < 1:
                        list_error.append(f"Failed to solve collapse bscutoff range")

                else:
                    self.collapsebscutoff = int(self.collapsebscutoff)

                if self.collapsebscutoff < 0:
                    self.collapsebscutoff = 0
                if self.collapsebscutoff > 100:
                    list_warning.append(
                        f"collapsebscutoff is over 100, all bootstrap will not seen"
                    )

            except:
                list_error.append(f"bscutoff should be integer")

        # bootstrap
        # bootstrap number should be int
        # If IQTREE selected and bootstrap number under 1000, change to 1000
        if self.method.tree == "fasttree":
            list_warning.append(
                f"Fasttree does not supports bootstrap, but bootstrap option selected. Will be ignored"
            )
            self.bootstrap = None
        else:
            try:
                self.bootstrap = int(self.bootstrap)
                if self.method.tree == "iqtree" and self.bootstrap < 1000:
                    list_warning.append(
                        "iqtree requires at least 1000 bootstrap. adjusting to 1000"
                    )
                    self.bootstrap = 1000
                elif self.method.tree == "raxml" and self.bootstrap < 0:
                    list_warning.append(
                        "bootstrap should not be negative. adjusting to 1"
                    )
                    self.bootstrap = 1

            except:
                list_error.append(
                    f"bootstrap should be integer when iqtree or raxml are selected"
                )

        # solveflat
        # If solveflat is not None, give True, else, give False
        if self.solveflat is False or self.solveflat is None:
            self.solveflat = False
        else:
            self.solveflat = True

        # regex
        # Regex list for queries
        # Validate if regex are valid regex
        if self.regex is None:
            pass
        elif not (type(self.regex)) is list:
            list_error.append("regex should be given in list of pattern")
        else:
            for regex in self.regex:
                try:
                    re.compile(regex)
                except:
                    list_error.append(f"regex {regex} is not a valid python regex")

        # cluster-evalue
        # E-value cutoff for clustering - should be positive
        try:
            self.cluster.evalue = float(self.cluster.evalue)
            if self.cluster.evalue < 0:
                list_warning.append("evalue should be positive, setting to 1")
                self.cluster.evalue = 1
        except:
            list_error.append("evalue should be positive floating point number")

        # cluster-wordsize
        # Wordsize option for clustering - should be int and not less than 7
        try:
            self.cluster.wordsize = int(self.cluster.wordsize)
            if self.cluster.wordsize < 7:
                list_warning.append(
                    "wordsize should be int not less than 7. Changing to 7"
                )
                self.cluster.wordsize = 7
        except:
            list_error.append("wordsize should be int not less than 7. Changing to 7")

        # mafft-algorithm
        # mafft-algorithm - auto, l-ins-i
        # mafft algorithm commands should be revised
        if self.method.alignment == "mafft":
            try:
                self.mafft.algorithm = str(self.mafft.algorithm)
                if not (
                    self.mafft.algorithm.lower()
                    in ("auto", "l-ins-i", "linsi", "localpair")
                ):
                    list_error.append(
                        f"Invalid mafft algorithm {self.mafft.algorithm}. Currently available algorithms are auto and l-ins-i"
                    )
                elif self.mafft.algorithm.lower() in ("l-ins-i", "linsi", "localpair"):
                    self.mafft.algorithm = "localpair"

            except:
                list_error.append(
                    f"Invalid mafft algorithm {self.mafft.algorithm}. Currently available algorithms are auto and l-ins-i"
                )

        # mafft-op
        # mafft gap opening penalty should be 0 or positive
        if self.method.alignment == "mafft":
            try:
                self.mafft.op = float(self.mafft.op)
                if self.mafft.op < 0:
                    list_warning.append(
                        "mafft op value should be positive, setting to 1.2"
                    )
                    self.mafft.op = 1.2
            except:
                list_error.append("mafft op value should be 0 or positive")

        # mafft-ep
        # mafft gap extension penalty should be 0 or positive
        if self.method.alignment == "mafft":
            try:
                self.mafft.ep = float(self.mafft.ep)
                if self.mafft.ep < 0:
                    list_warning.append(
                        "mafft ep value should be positive, setting to 1.2"
                    )
                    self.mafft.ep = 0.1
            except:
                list_error.append("mafft ep value should be 0 or positive")

        # trimal-algorithm
        # trimal algorithm, should be either gt
        # if auto, set gt
        if self.method.trim.lower() == "trimal":
            try:
                self.trimal.algorithm = str(self.trimal.algorithm)
                if not (self.trimal.algorithm.lower() in ("auto", "gt")):
                    list_warning(
                        f"Invalid trimal algorithm {self.trimal.algorithm}. Chaniging to gt"
                    )
                    self.trimal.algorithm = "gt"
            except:
                list_error.append(
                    f"Invalid trimal algorithm {self.trimal.algorithm}. Currently avilable algorithms are auto and gt"
                )

        # trimal-gt
        # trimal gt value, should be between 0 and 1
        # If trimming method is not trimal or trimal-algorithm is not 0, warn
        # Change to default value
        if self.method.trim.lower() == "trimal":
            try:
                self.trimal.gt = float(self.trimal.gt)
                if self.trimal.gt < 0 and self.trimal.gt >= 1:
                    list_warning.append(
                        f"trimal gt value should be between 1 and 0, setting to 0.2"
                    )
                    self.trimal.gt = 0.2
            except:
                list_error.append(
                    f"Invalid trimal gt value {self.trimal.gt}. gt should be between 1 and 0"
                )

        # noavx
        # default : False
        # if used, set True
        # if avx is not available command, set True
        # Negative boolean and save as avx
        if check_avx() is False and self.avx is True:
            list_warning.append(f"AVX is not available. Changing --noavx to True")
            self.avx = False

        # criterion
        # default : BIC
        # Should be one of AIC, AICc, BIC
        try:
            self.criterion = str(self.criterion)
            if not (self.criterion.lower() in ("aic", "aicc", "bic")):
                list_error.append(
                    "Modeltest criterion should be one of AIC, AICc and BIC"
                )
            else:
                # For prevent capital errors
                if self.criterion.lower() == "aic":
                    self.criterion = "AIC"
                elif self.criterion.lower() == "aicc":
                    self.criterion = "AICc"
                elif self.criterion.lower() == "bic":
                    self.criterion = "BIC"
                else:
                    list_error.append(
                        f"Somewhat error in parsing criterion, {self.criterion}"
                    )

        except:
            list_error.append("Modeltest criterion should be one of AIC, AICc and BIC")

        # cachedb
        # If cachedb is not None, give True, else, give False
        if self.cachedb is False or self.cachedb is None:
            self.cachedb = False
        else:
            self.cachedb = True

        # usecache
        # If usecache is not None, give True, else, give False
        if self.usecache is False or self.usecache is None:
            self.usecache = False
        else:
            self.usecache = True

        # matrixformat
        # should be either [csv, xlsx, parquet, feather]
        # if ftr, change it to feather
        # if excel, change it to xlsx
        try:
            self.matrixformat = str(self.matrixformat)
            if not (
                self.matrixformat.lower()
                in ("csv", "tsv", "xlsx", "parquet", "ftr", "feather")
            ):
                list_error.append(
                    "matrixformat should be one of csv, tsv, xlsx, parquet, ftr, or feather"
                )

        except:
            list_error.append(
                "matrixformat should be one of csv, tsv, xlsx, parquet, ftr, or feather"
            )

        # nosearchmatrix
        # If nosearchmatrix is not None, give True, else, give False
        if self.nosearchmatrix is True:
            self.nosearchmatrix = True
        else:
            self.nosearchmatrix = False

        # nosearchresult
        # If nosearchresult is not None, give True, else, give False
        if self.nosearchresult is True:
            self.nosearchresult = True
        else:
            self.nosearchresult = False

        # Printing logs while parsing validate options
        # Written in print functions, because logging can be loaded after option parsing
        print("--INFO--")
        for info in list_info:
            print(f"[INFO] {info}")

        print("\n")
        print("--WARNING--")
        for warning in list_warning:
            print(f"[WARNING] {warning}")

        print("\n")
        print("--ERROR--")
        for error in list_error:
            print(f"[ERROR] {error}")

        if len(list_error) > 0:
            raise Exception

        # Returning option parsing logs
        return list_info, list_warning, list_error


### Main function in validate_option.py
def initialize_option(parser, path_run):

    #### Before start
    # Backup original parser
    ori_parser = copy.deepcopy(parser)

    ### Initialize option
    opt = Option()

    ### test
    # 1. Check if test name is valid (Avaliable list : Penicillium)
    # 2. Change preset to test

    path_test = f"{os.path.dirname(__file__)}/../test_dataset"
    path_preset = f"{os.path.dirname(__file__)}/../preset"

    # *1 - if test option selected
    if not (parser.test is None):
        # *1 - if test value is valid dataset
        if parser.test.lower() in os.listdir(path_test):
            if os.path.exists(f"{path_test}/{parser.test.lower()}/preset.yaml"):
                parser.preset = f"{path_test}/{parser.test.lower()}/preset.yaml"
                print(f"test dataset {parser.preset} selected")
            else:
                print("Something wrong with test dataset option")
                raise Exception

            # Set runname to current time
            now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")

            # Give default locations if no outdir and runname given
            if parser.outdir is None:
                opt.outdir = f"{path_run}"
            if parser.runname is None:
                f"{parser.test.lower()}_{now}"

            opt.test = parser.test.lower()

        else:
            print("Invalid test dataset. Please --test option")
            raise Exception

    ### preset
    # 1. Check if lower case is fast or accurate
    # 2. Else, check if preset is parsable YAML file
    # 3. If parsable json file, parse it and update parser
    if not (parser.preset is None):
        if str(parser.preset).lower() == "fast":  # *1 - fast mode
            parser.preset = f"{path_preset}/fast.yaml"  # - connect to fast.yaml
            print("Using fast preset as default option")
            opt.update_from_preset(parser.preset)
        elif str(parser.preset).lower() == "accurate":  # *1 - accurate mode
            parser.preset = f"{path_preset}/accurate.yaml"  # - connect to accurate.yaml
            print("Using accurate preset as default option")
            opt.update_from_preset(parser.preset)
        else:
            if os.path.exists(parser.preset):
                print(f"[DEBUG] {parser.preset}")
                opt.update_from_preset(parser.preset)
            else:
                print(f"Cannot find preset file : {parser.preset}")
                raise Exception

    ### Then, update other options in parser
    opt.update_from_parser(ori_parser)

    ### validate
    list_info, list_warning, list_error = opt.validate()

    ### stdout input options
    print("[OPTIONS INPUT]")

    ## Print and log output options
    for attr, value in opt.__dict__.items():
        if isinstance(value, (str, float, bool, int, list, type(None))):
            list_info.append(f"Option {attr} : {value}")
            print(f"Option {attr} : {value}")
        else:
            for attr_, value_ in value.__dict__.items():
                list_info.append(f"Option {attr}-{attr_}: {value_}")
                print(f"Option {attr}-{attr_}: {value_}")

    print("------------------------------------")

    return opt, list_info, list_warning, list_error
