STDIN = "stdin"


class VTracker:
    """This helper class is used to identify the unique variants from the input
    so the crv file will not contain multiple copies of the same variant.
    """

    def __init__(self, deduplicate=True):
        from collections import defaultdict
        self.var_by_chrom = defaultdict(dict)
        self.current_UID = 1
        self.deduplicate = deduplicate

    # Add a variant - Returns true if the variant is a new unique variant, false
    # if it is a duplicate.  Also returns the UID.
    def addVar(self, chrom, pos, ref, alt):
        if not self.deduplicate:
            self.current_UID += 1
            return True, self.current_UID - 1

        change = ref + ":" + alt

        chr_dict = self.var_by_chrom[chrom]
        if pos not in chr_dict:
            # we have not seen this position before, add the position and change
            chr_dict[pos] = {}
            chr_dict[pos][change] = self.current_UID
            self.current_UID += 1
            return True, chr_dict[pos][change]
        else:
            variants = chr_dict[pos]
            if change not in variants:
                # we have the position but not this base change, add it.
                chr_dict[pos][change] = self.current_UID
                self.current_UID = self.current_UID + 1
                return True, chr_dict[pos][change]
            else:
                # this variant has been seen before.
                return False, chr_dict[pos][change]


class MasterCravatConverter(object):
    """Convert a file of ambiguous format to .crv format.

    Reads in CravatConverter classes in the same directory, selects the
    correct converter, and writes a crv file.
    """

    ALREADYCRV = 2

    def __init__(self, *inargs, **inkwargs):
        from oakvar.constants import crs_def
        self.logger = None
        self.crv_writer = None
        self.crs_writer = None
        self.crm_writer = None
        self.crl_writer = None
        self.primary_converter = None
        self.converters = {}
        self.possible_formats = []
        self.ready_to_convert = False
        self.input_format = None
        self.pipeinput = False
        self.input_paths = None
        self.input_dir = None
        self.input_path_dict = None
        self.input_path_dict2 = None
        self.output_dir = None
        self.output_base_fname = None
        self.input_assembly = None
        self.do_liftover = False
        self.lifter = None
        self.status_fpath = None
        self.conf = None
        self.unique_variants = None
        self.status_writer = None
        self.error_logger = None
        self.unique_excs = []
        self.wpath = None
        self.err_path = None
        self.crm_path = None
        self.crs_path = None
        self.crl_path = None
        self.error_lines = 0
        self.chromdict = {
            "chrx": "chrX",
            "chry": "chrY",
            "chrMT": "chrM",
            "chrMt": "chrM",
            "chr23": "chrX",
            "chr24": "chrY",
        }
        self._parse_cmd_args(inargs, inkwargs)
        self._setup_logger()
        self.vtracker = VTracker(deduplicate=not (self.unique_variants))
        from oakvar import get_wgs_reader
        self.wgsreader = get_wgs_reader(assembly="hg38")
        self.crs_def = crs_def.copy()

    def _parse_cmd_args(self, inargs, inkwargs):
        """Parse the arguments in sys.argv"""
        import sys
        from json import loads
        from argparse import ArgumentParser, SUPPRESS
        from os.path import abspath, dirname, exists, basename, join
        from os import makedirs
        from oakvar.admin_util import get_liftover_chain_paths
        from oakvar.exceptions import ExpectedException
        from pyliftover import LiftOver
        from oakvar.util import get_args
        parser = ArgumentParser()
        parser.add_argument("path",
                            help="Path to this converter's python module")
        parser.add_argument("inputs",
                            nargs="*",
                            default=None,
                            help="Files to be converted to .crv")
        parser.add_argument("-f",
                            dest="format",
                            default=None,
                            help="Specify an input format")
        parser.add_argument("-n",
                            "--name",
                            dest="name",
                            help="Name of job. Default is input file name.")
        parser.add_argument(
            "-d",
            "--output-dir",
            dest="output_dir",
            help="Output directory. Default is input file directory.",
        )
        parser.add_argument(
            "-l",
            "--genome",
            dest="genome",
            default="hg38",
            help="Input gene assembly. Will be lifted over to hg38",
        )
        parser.add_argument("--confs",
                            dest="confs",
                            default="{}",
                            help="Configuration string")
        parser.add_argument(
            "--unique-variants",
            dest="unique_variants",
            default=False,
            action="store_true",
            help=SUPPRESS,
        )
        if len(sys.argv) > 1 and len(inargs) == 0:
            inargs = [sys.argv]
        parsed_args = get_args(parser, inargs, inkwargs)
        self.input_format = None
        if parsed_args["format"]:
            self.input_format = parsed_args["format"]
        if parsed_args["inputs"] is None:
            raise ExpectedException("Input files are not given.")
        self.pipeinput = False
        if (parsed_args["inputs"] is not None
                and len(parsed_args["inputs"]) == 1
                and parsed_args["inputs"][0] == "-"):
            self.pipeinput = True
        self.input_paths = []
        if self.pipeinput == False:
            self.input_paths = [
                abspath(x) for x in parsed_args["inputs"] if x != "-"
            ]
        else:
            self.input_paths = [f"./{STDIN}"]
        self.input_dir = dirname(self.input_paths[0])
        self.input_path_dict = {}
        self.input_path_dict2 = {}
        if self.pipeinput == False:
            for i in range(len(self.input_paths)):
                self.input_path_dict[i] = self.input_paths[i]
                self.input_path_dict2[self.input_paths[i]] = i
        else:
            self.input_path_dict[0] = self.input_paths[0]
            self.input_path_dict2[STDIN] = 0
        self.output_dir = None
        if parsed_args["output_dir"]:
            self.output_dir = parsed_args["output_dir"]
        else:
            self.output_dir = self.input_dir
        if self.output_dir is None:
            from .exceptions import SetupError
            raise SetupError("output directory")
        if not (exists(self.output_dir)):
            makedirs(self.output_dir)
        self.output_base_fname = None
        if parsed_args["name"]:
            self.output_base_fname = parsed_args["name"]
        else:
            self.output_base_fname = basename(self.input_paths[0])
        self.input_assembly = parsed_args["genome"]
        self.do_liftover = self.input_assembly != "hg38"
        liftover_chain_paths = get_liftover_chain_paths()
        if self.do_liftover:
            if self.input_assembly not in liftover_chain_paths:
                from oakvar.exceptions import InvalidGenomeAssembly
                raise InvalidGenomeAssembly(self.input_assembly)
            else:
                self.lifter = LiftOver(
                    liftover_chain_paths[self.input_assembly])
        else:
            self.lifter = None
        self.status_fpath = join(self.output_dir,
                                 self.output_base_fname + ".status.json")
        self.conf = {}
        if parsed_args["confs"] is not None:
            confs = parsed_args["confs"].lstrip("'").rstrip("'").replace(
                "'", '"')
            self.conf = loads(confs)
        if "conf" in parsed_args:
            self.conf.update(parsed_args["conf"])
        self.unique_variants = parsed_args["unique_variants"]
        if "status_writer" in parsed_args:
            self.status_writer = parsed_args["status_writer"]

    def open_input_file(self, input_path):
        import gzip
        from oakvar.util import detect_encoding
        encoding = detect_encoding(input_path)
        if input_path.endswith(".gz"):
            f = gzip.open(input_path, mode="rt", encoding=encoding)
        else:
            f = open(input_path, encoding=encoding)
        return f

    def first_input_file(self):
        import gzip
        from sys import stdin
        from oakvar.util import detect_encoding
        if self.input_paths is None:
            from oakvar.exceptions import NoInputException
            raise NoInputException()
        if self.pipeinput == False:
            input_path = self.input_paths[0]
            encoding = detect_encoding(input_path)
            if input_path.endswith(".gz"):
                f = gzip.open(input_path, mode="rt", encoding=encoding)
            else:
                f = open(input_path, encoding=encoding)
        else:
            f = stdin
        return f

    def setup(self):
        """Do necesarry pre-run tasks"""
        if self.ready_to_convert:
            return
        # Read in the available converters
        self._initialize_converters()
        # Select the converter that matches the input format
        self._select_primary_converter()
        # Open the output files
        self._open_output_files()
        self.ready_to_convert = True

    def _setup_logger(self):
        """Open a log file and set up log handler"""
        from logging import getLogger
        from time import asctime
        self.logger = getLogger("oakvar.converter")
        self.logger.info("started: %s" % asctime())
        if self.pipeinput == False and self.input_paths:
            self.logger.info("Input file(s): %s" % ", ".join(self.input_paths))
        else:
            self.logger.info(f"Input file(s): {STDIN}")
        if self.do_liftover:
            self.logger.info("liftover from %s" % self.input_assembly)
        self.error_logger = getLogger("error.converter")
        self.unique_excs = []

    def _initialize_converters(self):
        """Reads in available converters.

        Loads any python files in same directory that start with _ as
        python modules. Initializes the CravatConverter class from that
        module and places them in a dict keyed by their input format
        """
        from importlib.util import spec_from_file_location, module_from_spec
        from oakvar.exceptions import ExpectedException
        from oakvar.admin_util import get_local_module_infos_of_type
        for module_info in get_local_module_infos_of_type(
                "converter").values():
            # path based import from https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
            spec = spec_from_file_location(module_info.name,
                                           module_info.script_path)
            if spec is None or spec.loader is None:
                from oakvar.exceptions import ModuleLoadingError
                raise ModuleLoadingError(module_info.name)
            module = module_from_spec(spec)
            spec.loader.exec_module(module)
            converter = module.CravatConverter()
            if not hasattr(converter, "format_name"):
                from .exceptions import InvalidModule
                raise InvalidModule(module_info.name)
            converter.module_name = module_info.name
            if converter.format_name not in self.converters:
                self.converters[converter.format_name] = converter
            else:
                err_msg = ("Cannot load two converters for format %s" %
                           converter.format_name)
                raise ExpectedException(err_msg)
        self.possible_formats = list(self.converters.keys())

    def _select_primary_converter(self):
        """Choose the converter which matches the input format.

        If a input format was not specified in the cmd args, uses the
        check_format() method of the CravatConverters to identify a
        converter which can parse the input file.
        """
        from oakvar.exceptions import InvalidInputFormat
        if self.logger is None:
            from oakvar.exceptions import LoggerError
            raise LoggerError()
        if self.input_format is not None:
            if self.input_format not in self.possible_formats:
                raise InvalidInputFormat(self.input_format)
        else:
            if self.pipeinput == False:
                valid_formats = []
                first_file = self.first_input_file()
                first_file.seek(0)
                for converter_name, converter in self.converters.items():
                    try:
                        check_success = converter.check_format(first_file)
                    except:
                        check_success = False
                    first_file.seek(0)
                    if check_success:
                        valid_formats.append(converter_name)
                if len(valid_formats) == 0:
                    raise InvalidInputFormat()
                elif len(valid_formats) > 1:
                    raise InvalidInputFormat(
                        f"ambiguous {','.join(valid_formats)}")
                else:
                    self.input_format = valid_formats[0]
            else:
                if self.input_format is None:
                    raise InvalidInputFormat()
        self.primary_converter = self.converters[self.input_format]
        self._set_converter_properties(self.primary_converter)
        if self.input_paths is None:
            from oakvar.exceptions import NoInputException
            raise NoInputException()
        if self.pipeinput == False:
            if len(self.input_paths) > 1:
                for fn in self.input_paths[1:]:
                    f = self.open_input_file(fn)
                    if not self.primary_converter.check_format(f):
                        raise InvalidInputFormat("inconsistent input formats")
        self.logger.info("input format: %s" % self.input_format)

    def _set_converter_properties(self, converter):
        if self.primary_converter is None or self.conf is None:
            from oakvar.exceptions import SetupError
            raise SetupError()
        converter.output_dir = self.output_dir
        converter.run_name = self.output_base_fname
        converter.input_assembly = self.input_assembly
        module_name = self.primary_converter.format_name + "-converter"
        if module_name in self.conf:
            if hasattr(converter, "conf") == False:
                converter.conf = {}
            converter.conf.update(self.conf[module_name])

    def _open_output_files(self):
        """Open .crv .crs and .crm output files, plus .err file.

        .crv .crs and .crm files are opened using a CravatWriter.
        .err file will contain all errors which occur during conversion.
        .map file contains two columns showing which lines in input
        correspond to which lines in output.
        """
        if self.output_base_fname is None or self.primary_converter is None or self.output_dir is None:
            from oakvar.exceptions import SetupError
            raise SetupError()
        from oakvar.constants import (
            crv_def,
            crv_idx,
            crm_def,
            crm_idx,
            crs_idx,
            crl_def,
        )
        from oakvar.inout import CravatWriter
        from os.path import join
        # Setup CravatWriter
        self.wpath = join(self.output_dir, self.output_base_fname + ".crv")
        self.crv_writer = CravatWriter(self.wpath)
        self.crv_writer.add_columns(crv_def)
        self.crv_writer.write_definition()
        for index_columns in crv_idx:
            self.crv_writer.add_index(index_columns)
        self.crv_writer.wf.write("#input_format={}\n".format(
            self.primary_converter.format_name))
        # Setup err file
        self.err_path = join(self.output_dir,
                             self.output_base_fname + ".converter.err")
        # Setup crm line mappings file
        self.crm_path = join(self.output_dir, self.output_base_fname + ".crm")
        self.crm_writer = CravatWriter(self.crm_path)
        self.crm_writer.add_columns(crm_def)
        self.crm_writer.write_definition()
        for index_columns in crm_idx:
            self.crm_writer.add_index(index_columns)
        self.crm_writer.write_input_paths(self.input_path_dict)
        # Setup crs sample file
        self.crs_path = join(self.output_dir, self.output_base_fname + ".crs")
        self.crs_writer = CravatWriter(self.crs_path)
        self.crs_writer.add_columns(self.crs_def)
        if hasattr(self.primary_converter, "addl_cols"):
            self.crs_writer.add_columns(self.primary_converter.addl_cols,
                                        append=True)
            self.crs_def.extend(self.primary_converter.addl_cols)
        self.crs_writer.write_definition()
        for index_columns in crs_idx:
            self.crs_writer.add_index(index_columns)
        # Setup liftover var file
        # if self.do_liftover or self.primary_converter.format_name == "vcf":
        self.crl_path = join(
            self.output_dir,
            ".".join([self.output_base_fname, "original_input", "var"]),
        )
        self.crl_writer = CravatWriter(self.crl_path)
        self.crl_writer.add_columns(crl_def)
        self.crl_writer.write_definition()
        self.crl_writer.write_names("original_input", "Original Input", "")

    def run(self):
        """Convert input file to a .crv file using the primary converter."""
        from sys import stdin
        from os.path import basename
        from time import time, asctime, localtime
        from copy import copy
        from re import compile
        from oakvar.exceptions import BadFormatError, ExpectedException, NoVariantError
        from oakvar.base_converter import BaseConverter
        from oakvar.exceptions import SetupError
        self.setup()
        if self.wgsreader is None or self.crv_writer is None or self.crl_writer is None or self.crm_writer is None \
                or self.crs_writer is None or self.input_path_dict2 is None or self.primary_converter is None:
            raise SetupError()
        if self.logger is None:
            from .exceptions import LoggerError
            raise LoggerError()
        if not hasattr(self.primary_converter, "format_name"):
            from .exceptions import InvalidModule
            if hasattr(self.primary_converter, "module_name"):
                raise InvalidModule(self.primary_converter.module_name)
            else:
                raise InvalidModule("unknown module")
        start_time = time()
        if self.status_writer is not None:
            self.status_writer.queue_status_update(
                "status",
                "Started {} ({})".format(
                    "Converter",
                    self.primary_converter.format_name),  # type: ignore
            )
        last_status_update_time = time()
        if self.input_paths is None:
            raise SetupError()
        multiple_files = len(self.input_paths) > 1
        fileno = 0
        total_lnum = 0
        base_re = compile("^[ATGC]+|[-]+$")
        write_lnum = 0
        for fn in self.input_paths:
            if self.pipeinput:
                f = stdin
            else:
                f = self.open_input_file(fn)
            if self.pipeinput == True:
                fname = STDIN
            else:
                fname = f.name
            fileno += 1
            converter = self.primary_converter.__class__()
            if converter is None:
                raise SetupError()
            self._set_converter_properties(converter)
            converter.setup(f)  # type: ignore
            if self.pipeinput == False:
                f.seek(0)
            if self.pipeinput:
                cur_fname = STDIN
            else:
                cur_fname = basename(f.name)
            last_read_lnum = None
            for read_lnum, l, all_wdicts in converter.convert_file(  # type: ignore
                    f, exc_handler=self._log_conversion_error):
                samp_prefix = cur_fname
                last_read_lnum = read_lnum
                try:
                    # all_wdicts is a list, since one input line can become
                    # multiple output lines. False is returned if converter
                    # decides line is not an input line.
                    if all_wdicts is BaseConverter.IGNORE:
                        continue
                    total_lnum += 1
                    if all_wdicts:
                        UIDMap = []
                        no_unique_var = 0
                        for wdict_no in range(len(all_wdicts)):
                            wdict = all_wdicts[wdict_no]
                            chrom = wdict["chrom"]
                            pos = wdict["pos"]
                            if chrom is not None:
                                if not chrom.startswith("chr"):
                                    chrom = "chr" + chrom
                                wdict["chrom"] = self.chromdict.get(
                                    chrom, chrom)
                                if multiple_files:
                                    if wdict["sample_id"]:
                                        wdict["sample_id"] = "__".join(
                                            [samp_prefix, wdict["sample_id"]])
                                    else:
                                        wdict["sample_id"] = samp_prefix
                                if "ref_base" not in wdict or wdict[
                                        "ref_base"] == "":
                                    wdict[
                                        "ref_base"] = self.wgsreader.get_bases(
                                            chrom, int(wdict["pos"]))
                                else:
                                    ref_base = wdict["ref_base"]
                                    if ref_base == "" and wdict[
                                            "alt_base"] not in [
                                                "A",
                                                "T",
                                                "C",
                                                "G",
                                            ]:
                                        raise BadFormatError(
                                            "Reference base required for non SNV"
                                        )
                                    elif ref_base is None or ref_base == "":
                                        wdict[
                                            "ref_base"] = self.wgsreader.get_bases(
                                                chrom, int(pos))
                                prelift_wdict = copy(wdict)
                                if self.do_liftover:
                                    (
                                        wdict["chrom"],
                                        wdict["pos"],
                                        wdict["ref_base"],
                                        wdict["alt_base"],
                                    ) = self.liftover(
                                        wdict["chrom"],
                                        int(wdict["pos"]),
                                        wdict["ref_base"],
                                        wdict["alt_base"],
                                    )
                                if base_re.fullmatch(
                                        wdict["ref_base"]) is None:
                                    raise BadFormatError(
                                        "Invalid reference base")
                                if base_re.fullmatch(
                                        wdict["alt_base"]) is None:
                                    raise BadFormatError(
                                        "Invalid alternate base")
                                p, r, a = (
                                    int(wdict["pos"]),
                                    wdict["ref_base"],
                                    wdict["alt_base"],
                                )
                                (
                                    new_pos,
                                    new_ref,
                                    new_alt,
                                ) = self.standardize_pos_ref_alt("+", p, r, a)
                                wdict["pos"] = new_pos
                                wdict["ref_base"] = new_ref
                                wdict["alt_base"] = new_alt
                                unique, UID = self.vtracker.addVar(
                                    wdict["chrom"], new_pos, new_ref, new_alt)
                                wdict["uid"] = UID
                                if wdict["ref_base"] == wdict["alt_base"]:
                                    raise NoVariantError()
                                if unique:
                                    write_lnum += 1
                                    self.crv_writer.write_data(wdict)
                                    # if self.do_liftover:
                                    # if wdict["pos"] != prelift_wdict["pos"] or wdict["ref_base"] != prelift_wdict["ref_base"] or wdict["alt_base"] != prelift_wdict["alt_base"]:
                                    prelift_wdict["uid"] = UID
                                    self.crl_writer.write_data(prelift_wdict)
                                    # addl_operation errors shouldnt prevent variant from writing
                                    try:
                                        converter.addl_operation_for_unique_variant(  # type: ignore
                                            wdict, no_unique_var)
                                    except Exception as e:
                                        self._log_conversion_error(
                                            read_lnum,
                                            l,
                                            e,
                                            full_line_error=False)
                                    no_unique_var += 1
                                if UID not in UIDMap:
                                    # For this input line, only write to the .crm if the UID has not yet been written to the map file.
                                    self.crm_writer.write_data({
                                        "original_line":
                                        read_lnum,
                                        "tags":
                                        wdict["tags"],
                                        "uid":
                                        UID,
                                        "fileno":
                                        self.input_path_dict2[fname],
                                    })
                                    UIDMap.append(UID)
                            self.crs_writer.write_data(wdict)
                    else:
                        raise ExpectedException(
                            "No valid alternate allele was found in any samples."
                        )
                except Exception as e:
                    self._log_conversion_error(read_lnum, l, e)
                    continue
            f.close()
            cur_time = time()
            if total_lnum % 10000 == 0 or cur_time - last_status_update_time > 3:
                if self.status_writer is not None:
                    self.status_writer.queue_status_update(
                        "status",
                        "Running {} ({}): line {}".format(
                            "Converter", cur_fname, last_read_lnum),
                    )
                last_status_update_time = cur_time
        self.logger.info("error lines: %d" % self.error_lines)
        self._close_files()
        self.end()
        if self.status_writer is not None:
            self.status_writer.queue_status_update("num_input_var", total_lnum)
            self.status_writer.queue_status_update("num_unique_var",
                                                   write_lnum)
            self.status_writer.queue_status_update("num_error_input",
                                                   self.error_lines)
        end_time = time()
        self.logger.info("finished: %s" % asctime(localtime(end_time)))
        runtime = round(end_time - start_time, 3)
        self.logger.info("num input lines: {}".format(total_lnum))
        self.logger.info("runtime: %s" % runtime)
        if self.status_writer is not None:
            self.status_writer.queue_status_update(
                "status",
                "Finished {} ({})".format("Converter",
                                          self.primary_converter.format_name),
            )
        return total_lnum, self.primary_converter.format_name

    def liftover_one_pos(self, chrom, pos):
        if self.lifter is None:
            from .exceptions import SetupError
            raise SetupError("no lifter")
        res = self.lifter.convert_coordinate(chrom, pos - 1)
        if res is None or len(res) == 0:
            res_prev = self.lifter.convert_coordinate(chrom, pos - 2)
            res_next = self.lifter.convert_coordinate(chrom, pos)
            if res_prev is not None and res_next is not None:
                if len(res_prev) == 1 and len(res_next) == 1:
                    pos_prev = res_prev[0][1]
                    pos_next = res_next[0][1]
                    if pos_prev == pos_next - 2:
                        res = [(res_prev[0][0], pos_prev + 1)]
                    elif pos_prev == pos_next + 2:
                        res = [(res_prev[0][0], pos_prev - 1)]
        return res

    def liftover(self, chrom, pos, ref, alt):
        from oakvar.exceptions import LiftoverFailure
        from oakvar.util import reverse_complement
        if self.lifter is None or self.wgsreader is None:
            from .exceptions import SetupError
            raise SetupError("no lifter")
        reflen = len(ref)
        altlen = len(alt)
        if reflen == 1 and altlen == 1:
            res = self.liftover_one_pos(chrom, pos)
            if res is None or len(res) == 0:
                raise LiftoverFailure("Liftover failure")
            if len(res) > 1:
                raise LiftoverFailure("Liftover failure")
            try:
                el = res[0]
            except:
                raise LiftoverFailure("Liftover failure")
            newchrom = el[0]
            newpos = el[1] + 1
        elif reflen >= 1 and altlen == 0:  # del
            pos1 = pos
            pos2 = pos + reflen - 1
            res1 = self.lifter.convert_coordinate(chrom, pos1 - 1)
            res2 = self.lifter.convert_coordinate(chrom, pos2 - 1)
            if res1 is None or res2 is None or len(res1) == 0 or len(
                    res2) == 0:
                raise LiftoverFailure("Liftover failure")
            if len(res1) > 1 or len(res2) > 1:
                raise LiftoverFailure("Liftover failure")
            el1 = res1[0]
            el2 = res2[0]
            newchrom1 = el1[0]
            newpos1 = el1[1] + 1
            newpos2 = el2[1] + 1
            newchrom = newchrom1
            newpos = newpos1
            newpos = min(newpos1, newpos2)
        elif reflen == 0 and altlen >= 1:  # ins
            res = self.lifter.convert_coordinate(chrom, pos - 1)
            if res is None or len(res) == 0:
                raise LiftoverFailure("Liftover failure")
            if len(res) > 1:
                raise LiftoverFailure("Liftover failure")
            el = res[0]
            newchrom = el[0]
            newpos = el[1] + 1
        else:
            pos1 = pos
            pos2 = pos + reflen - 1
            res1 = self.lifter.convert_coordinate(chrom, pos1 - 1)
            res2 = self.lifter.convert_coordinate(chrom, pos2 - 1)
            if res1 is None or res2 is None or len(res1) == 0 or len(
                    res2) == 0:
                raise LiftoverFailure("Liftover failure")
            if len(res1) > 1 or len(res2) > 1:
                raise LiftoverFailure("Liftover failure")
            el1 = res1[0]
            el2 = res2[0]
            newchrom1 = el1[0]
            newpos1 = el1[1] + 1
            newpos2 = el2[1] + 1
            newchrom = newchrom1
            newpos = min(newpos1, newpos2)
        hg38_ref = self.wgsreader.get_bases(newchrom, newpos)
        if hg38_ref == reverse_complement(ref):
            newref = hg38_ref
            newalt = reverse_complement(alt)
        else:
            newref = ref
            newalt = alt
        return [newchrom, newpos, newref, newalt]

    def _log_conversion_error(self, ln, line, e, full_line_error=True):
        """Log exceptions thrown by primary converter.
        All exceptions are written to the .err file with the exception type
        and message. Exceptions are also written to the log file once, with the
        traceback.
        """
        if self.logger is None or self.error_logger is None:
            from .exceptions import LoggerError
            raise LoggerError()
        from traceback import format_exc
        if full_line_error:
            self.error_lines += 1
        err_str = format_exc().rstrip()
        if err_str not in self.unique_excs:
            self.unique_excs.append(err_str)
            if not hasattr(e, "traceback") or e.traceback == False:
                pass
            else:
                self.logger.error(err_str)
        self.error_logger.error("\nLINE:{:d}\nINPUT:{}\nERROR:{}\n#".format(
            ln, line[:-1], str(e)))

    def _close_files(self):
        """Close the input and output files."""
        if self.crv_writer is not None:
            self.crv_writer.close()
        if self.crm_writer is not None:
            self.crm_writer.close()
        if self.crs_writer is not None:
            self.crs_writer.close()

    def end(self):
        pass

    def standardize_pos_ref_alt(self, strand, pos, ref, alt):
        reflen = len(ref)
        altlen = len(alt)
        # Returns without change if same single nucleotide for ref and alt.
        if reflen == 1 and altlen == 1 and ref == alt:
            return pos, ref, alt
        # Trimming from the start and then the end of the sequence
        # where the sequences overlap with the same nucleotides
        new_ref2, new_alt2, new_pos = self.trim_input(ref, alt, pos, strand)
        if new_ref2 == "" or new_ref2 == ".":
            new_ref2 = "-"
        if new_alt2 == "" or new_alt2 == ".":
            new_alt2 = "-"
        return new_pos, new_ref2, new_alt2

    def trim_input(self, ref, alt, pos, strand):
        pos = int(pos)
        reflen = len(ref)
        altlen = len(alt)
        minlen = min(reflen, altlen)
        new_ref = ref
        new_alt = alt
        new_pos = pos
        for nt_pos in range(0, minlen):
            if ref[reflen - nt_pos - 1] == alt[altlen - nt_pos - 1]:
                new_ref = ref[:reflen - nt_pos - 1]
                new_alt = alt[:altlen - nt_pos - 1]
            else:
                break
        new_ref_len = len(new_ref)
        new_alt_len = len(new_alt)
        minlen = min(new_ref_len, new_alt_len)
        new_ref2 = new_ref
        new_alt2 = new_alt
        for nt_pos in range(0, minlen):
            if new_ref[nt_pos] == new_alt[nt_pos]:
                if strand == "+":
                    new_pos += 1
                elif strand == "-":
                    new_pos -= 1
                new_ref2 = new_ref[nt_pos + 1:]
                new_alt2 = new_alt[nt_pos + 1:]
            else:
                new_ref2 = new_ref[nt_pos:]
                new_alt2 = new_alt[nt_pos:]
                break
        return new_ref2, new_alt2, new_pos


def main():
    master_cravat_converter = MasterCravatConverter()
    master_cravat_converter.run()


if __name__ == "__main__":
    master_cravat_converter = MasterCravatConverter()
    master_cravat_converter.run()
