class BaseMapper(object):

    def __init__(self, *inargs, **inkwargs):
        import os
        from time import time
        import pkg_resources
        from ..module.local import get_module_conf

        self.cmd_parser = None
        self.input_path = None
        self.input_dir = None
        self.reader = None
        self.output_dir = None
        self.output_base_fname = None
        self.crx_path = None
        self.crg_path = None
        self.crx_writer = None
        self.crg_writer = None
        self.input_fname = None
        self.slavemode = False
        self.confs = None
        self.postfix = None
        self.primary_transcript_paths = None
        self.args = None
        self.logger = None
        self.error_logger = None
        self.unique_excs = None
        self.written_primary_transc = None
        self._define_main_cmd_args()
        self._define_additional_cmd_args()
        self._parse_cmd_args(inargs, inkwargs)
        if self.args is None:
            return
        self.live = self.args["live"]
        self.t = time()
        self.status_writer = self.args["status_writer"]
        main_fpath = self.args["script_path"]
        main_basename = os.path.basename(main_fpath)
        if "." in main_basename:
            self.module_name = ".".join(main_basename.split(".")[:-1])
        else:
            self.module_name = main_basename
        self.module_dir = os.path.dirname(main_fpath)
        self.mapper_dir = os.path.dirname(main_fpath)
        self.gene_sources = []
        # self.primary_gene_source = None
        self.gene_info = {}
        # self.written_primary_transc = set([])
        self._setup_logger()
        self.conf = get_module_conf(self.module_name, module_type="mapper")
        self.cravat_version = pkg_resources.get_distribution("oakvar").version

    def _define_main_cmd_args(self):
        import argparse

        self.cmd_parser = argparse.ArgumentParser()
        self.cmd_parser.add_argument("input_file", help="Input crv file")
        self.cmd_parser.add_argument(
            "-n", dest="name", help="Name of job. " + "Default is input file name."
        )
        self.cmd_parser.add_argument(
            "-d",
            dest="output_dir",
            help="Output directory. " + "Default is input file directory.",
        )
        self.cmd_parser.add_argument(
            "--confs", dest="confs", default="{}", help="Configuration string"
        )
        self.cmd_parser.add_argument(
            "--seekpos", dest="seekpos", default=None, help=argparse.SUPPRESS
        )
        self.cmd_parser.add_argument(
            "--chunksize", dest="chunksize", default=None, help=argparse.SUPPRESS
        )
        self.cmd_parser.add_argument(
            "--slavemode",
            dest="slavemode",
            action="store_true",
            default=False,
            help=argparse.SUPPRESS,
        )
        self.cmd_parser.add_argument(
            "--postfix", dest="postfix", default="", help=argparse.SUPPRESS
        )
        self.cmd_parser.add_argument(
            "--primary-transcript",
            dest="primary_transcript",
            # nargs="*",
            default=["mane"],
            help='"mane" for MANE transcripts as primary transcripts, or a path to a file of primary transcripts. MANE is default.',
        )
        self.cmd_parser.add_argument(
            "--live", action="store_true", default=False, help=argparse.SUPPRESS
        )
        self.cmd_parser.add_argument(
            "--status_writer", default=None, help=argparse.SUPPRESS
        )

    def _define_additional_cmd_args(self):
        """This method allows sub-classes to override and provide addittional command line args"""
        pass

    def _parse_cmd_args(self, inargs, inkwargs):
        from os.path import abspath, split, exists
        from os import makedirs
        from json import loads
        from ..util.util import get_args

        args = get_args(self.cmd_parser, inargs, inkwargs)
        self.input_path = abspath(args["input_file"]) if args["input_file"] else None
        if self.input_path:
            self.input_dir, self.input_fname = split(self.input_path)
            if args["output_dir"]:
                self.output_dir = args["output_dir"]
            else:
                self.output_dir = self.input_dir
            if not (exists(self.output_dir)):
                makedirs(self.output_dir)
        if hasattr(args, "run_name"):
            self.output_base_fname = args["run_name"]
        else:
            self.output_base_fname = self.input_fname
        self.confs = None
        if args["confs"] is not None:
            confs = args["confs"].lstrip("'").rstrip("'").replace("'", '"')
            self.confs = loads(confs)
        self.slavemode = args["slavemode"]
        self.postfix = args["postfix"]
        self.primary_transcript_paths = [v for v in args["primary_transcript"] if v]
        self.args = args

    def base_setup(self):
        self.setup()
        if self.live == False:
            self._setup_io()

    def setup(self):
        raise NotImplementedError("Mapper must have a setup() method.")

    def end(self):
        pass

    def _setup_logger(self):
        import logging

        self.logger = logging.getLogger("oakvar.mapper")
        if self.input_path:
            self.logger.info("input file: %s" % self.input_path)
        self.error_logger = logging.getLogger("err." + self.module_name)
        self.unique_excs = []

    def _setup_io(self):
        import os
        from ..util.inout import FileReader
        from ..util.inout import FileWriter
        from ..consts import crx_def, crx_idx, crg_def, crg_idx

        if (
            self.output_base_fname is None
            or self.postfix is None
            or self.conf is None
            or self.output_dir is None
        ):
            from ..exceptions import SetupError

            raise SetupError()
        # Reader
        if (
            self.args is not None
            and self.args["seekpos"] is not None
            and self.args["chunksize"] is not None
        ):
            self.reader = FileReader(
                self.input_path,
                seekpos=int(self.args["seekpos"]),
                chunksize=int(self.args["chunksize"]),
            )
        else:
            self.reader = FileReader(self.input_path)
        # Various output files
        output_toks = self.output_base_fname.split(".")
        if output_toks[-1] == "crv":
            output_toks = output_toks[:-1]
        # .crx
        crx_fname = ".".join(output_toks) + ".crx"
        self.crx_path = os.path.join(self.output_dir, crx_fname)
        if self.slavemode:
            self.crx_path += self.postfix
        self.crx_writer = FileWriter(self.crx_path)
        self.crx_writer.add_columns(crx_def)
        self.crx_writer.write_definition(self.conf)
        for index_columns in crx_idx:
            self.crx_writer.add_index(index_columns)
        self.crx_writer.write_meta_line("title", self.conf["title"])
        self.crx_writer.write_meta_line("version", self.conf["version"])
        self.crx_writer.write_meta_line("modulename", self.module_name)
        if not self.primary_transcript_paths:
            self.crx_writer.write_meta_line("primary_transcript_paths", "")
        else:
            self.crx_writer.write_meta_line(
                "primary_transcript_paths", ",".join(self.primary_transcript_paths)
            )
        # .crg
        crg_fname = ".".join(output_toks) + ".crg"
        self.crg_path = os.path.join(self.output_dir, crg_fname)
        if self.slavemode:
            self.crg_path += self.postfix
        self.crg_writer = FileWriter(self.crg_path)
        self.crg_writer.add_columns(crg_def)
        self.crg_writer.write_definition(self.conf)
        for index_columns in crg_idx:
            self.crg_writer.add_index(index_columns)

    def run(self):
        """
        Read crv file and use map() function to convert to crx dict. Write the
        crx dict to the crx file and add information in crx dict to gene_info
        """
        from time import time, asctime, localtime

        self.base_setup()
        start_time = time()
        if (
            self.logger is None
            or self.conf is None
            or self.reader is None
            or not hasattr(self, "map")
        ):
            from ..exceptions import SetupError

            raise SetupError()
        self.logger.info("started: %s" % asctime(localtime(start_time)))
        if self.status_writer is not None:
            self.status_writer.queue_status_update(
                "status", "Started {} ({})".format(self.conf["title"], self.module_name)
            )
        count = 0
        last_status_update_time = time()
        crx_data = None
        output = {}
        for ln, line, crv_data in self.reader.loop_data():
            crx_data = None
            try:
                count += 1
                cur_time = time()
                if self.status_writer is not None:
                    if count % 10000 == 0 or cur_time - last_status_update_time > 3:
                        self.status_writer.queue_status_update(
                            "status", "Running gene mapper: line {}".format(count)
                        )
                        last_status_update_time = cur_time
                if crv_data["alt_base"] == "*":
                    crx_data = crv_data
                    crx_data["all_mappings"] = "{}"
                else:
                    crx_data = self.map(crv_data)  # type: ignore
                # Skip cases where there was no change. Can result if ref_base not in original input
                if crx_data["ref_base"] == crx_data["alt_base"]:
                    continue
            except Exception as e:
                self._log_runtime_error(ln, line, e, fn=self.reader.path)
                continue
            if crx_data is not None:
                self.crx_writer.write_data(crx_data)  # type: ignore
                self._add_crx_to_gene_info(crx_data)
        self._write_crg()
        stop_time = time()
        self.logger.info("finished: %s" % asctime(localtime(stop_time)))
        runtime = stop_time - start_time
        self.logger.info("runtime: %6.3f" % runtime)
        if self.status_writer is not None:
            self.status_writer.queue_status_update("status", "Finished gene mapper")
        self.end()
        return output

    def run_as_slave(self, __pos_no__):
        """
        Read crv file and use map() function to convert to crx dict. Write the
        crx dict to the crx file and add information in crx dict to gene_info
        """
        from time import time, asctime, localtime

        self.base_setup()
        if (
            self.args is None
            or self.logger is None
            or self.conf is None
            or self.reader is None
            or self.crx_writer is None
        ):
            from ..exceptions import SetupError

            raise SetupError()
        start_time = time()
        tstamp = asctime(localtime(start_time))
        self.logger.info(f"started: {tstamp} | {self.args['seekpos']}")
        if self.status_writer is not None:
            self.status_writer.queue_status_update(
                "status", "Started {} ({})".format(self.conf["title"], self.module_name)
            )
        count = 0
        last_status_update_time = time()
        crx_data = None
        for ln, line, crv_data in self.reader.loop_data():
            try:
                count += 1
                cur_time = time()
                if self.status_writer is not None:
                    if count % 10000 == 0 or cur_time - last_status_update_time > 3:
                        self.status_writer.queue_status_update(
                            "status", "Running gene mapper: line {}".format(count)
                        )
                        last_status_update_time = cur_time
                if crv_data["alt_base"] == "*":
                    crx_data = crv_data
                    crx_data["all_mappings"] = "{}"
                else:
                    crx_data = self.map(crv_data)  # type: ignore
                if crx_data is None:
                    continue
            except Exception as e:
                self._log_runtime_error(ln, line, e, fn=self.reader.path)
            if crx_data is not None:
                self.crx_writer.write_data(crx_data)
                self._add_crx_to_gene_info(crx_data)
        self._write_crg()
        stop_time = time()
        tstamp = asctime(localtime(stop_time))
        self.logger.info(f"finished: {tstamp} | {self.args['seekpos']}")
        runtime = stop_time - start_time
        self.logger.info("runtime: %6.3f" % runtime)
        self.end()

    def _add_crx_to_gene_info(self, crx_data):
        """
        Add information in a crx dict to persistent gene_info dict
        """
        from ..util.inout import AllMappingsParser

        tmap_json = crx_data["all_mappings"]
        # Return if no tmap
        if tmap_json == "":
            return
        tmap_parser = AllMappingsParser(tmap_json)
        for hugo in tmap_parser.get_genes():
            self.gene_info[hugo] = True

    def _write_crg(self):
        """
        Convert gene_info to crg dict and write to crg file
        """
        if self.crg_writer is None:
            return
        from ..consts import crg_def

        sorted_hugos = list(self.gene_info.keys())
        sorted_hugos.sort()
        for hugo in sorted_hugos:
            crg_data = {x["name"]: "" for x in crg_def}
            crg_data["hugo"] = hugo
            self.crg_writer.write_data(crg_data)

    def _log_runtime_error(self, ln, line, e, fn=None):
        import traceback

        _ = line
        err_str = traceback.format_exc().rstrip()
        if (
            self.logger is not None
            and self.unique_excs is not None
            and err_str not in self.unique_excs
        ):
            self.unique_excs.append(err_str)
            self.logger.error(err_str)
        if self.error_logger is not None:
            self.error_logger.error(f"{fn}:{ln}\t{str(e)}")
            # self.error_logger.error(
            #    "\nLINE:{:d}\nINPUT:{}\nERROR:{}\n#".format(ln, line[:-1], str(e))
            # )

    async def get_gene_summary_data(self, cf):
        from ..consts import crx_def
        from ..consts import result_viewer_num_var_limit_for_gene_summary_key
        from ..consts import DEFAULT_RESULT_VIEWER_NUM_VAR_LIMIT_FOR_GENE_SUMMARY
        from ..system import get_system_conf

        hugos = await cf.exec_db(cf.get_filtered_hugo_list)
        # Below is to fix opening oc 1.8.0 jobs with oc 1.8.1.
        # TODO: Remove it after a while and add 1.8.0 to the db update chain in cmd_util.
        cols = [
            "base__" + coldef["name"]
            for coldef in crx_def
            if coldef["name"] != "cchange"
        ]
        cols.extend(["tagsampler__numsample"])
        data = {}
        rows = await cf.exec_db(cf.get_variant_data_for_cols, cols)
        # TODO: gene level summary only for <100k variants is a temporary fix.
        # Remove this when async gene level summary is implemented.
        sys_conf = get_system_conf()
        result_viewer_num_var_limit_for_gene_summary = sys_conf.get(result_viewer_num_var_limit_for_gene_summary_key, DEFAULT_RESULT_VIEWER_NUM_VAR_LIMIT_FOR_GENE_SUMMARY)
        if len(rows) > result_viewer_num_var_limit_for_gene_summary:
            return {}
        rows_by_hugo = {}
        for row in rows:
            hugo = row[-1]
            if hugo not in rows_by_hugo:
                rows_by_hugo[hugo] = []
            rows_by_hugo[hugo].append(row)
        for hugo in hugos:
            rows = rows_by_hugo[hugo]
            input_data = {}
            for i in range(len(cols)):
                input_data[cols[i].split("__")[1]] = [row[i] for row in rows]
            if hasattr(self, "summarize_by_gene"):
                out = self.summarize_by_gene(hugo, input_data)  # type: ignore
                data[hugo] = out
        return data

    def live_report_substitute(self, d):
        import re

        if self.conf is None or "report_substitution" not in self.conf:
            return
        rs_dic = self.conf["report_substitution"]
        rs_dic_keys = list(rs_dic.keys())
        for colname in d.keys():
            if colname in rs_dic_keys:
                value = d[colname]
                if colname in ["all_mappings", "all_so"]:
                    for target in list(rs_dic[colname].keys()):
                        value = re.sub(
                            "\\b" + target + "\\b", rs_dic[colname][target], value
                        )
                else:
                    if value in rs_dic[colname]:
                        value = rs_dic[colname][value]
                d[colname] = value
        return d
