#!/usr/bin/env python
# filename: alignment.py
#
#
# Copyright (c) 2023 Bryan Briney
# License: The MIT license (http://opensource.org/licenses/MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#


# import os
# import platform
# import subprocess as sp
# import sys
# import tempfile
# import uuid
# from abc import ABC, abstractmethod
# from copy import copy, deepcopy
# from io import StringIO
# from itertools import groupby
# from typing import Callable, Iterable, Optional, Union

# import parasail
# from Bio import AlignIO
# from Bio.Align import AlignInfo, MultipleSeqAlignment
# from Bio.SeqRecord import SeqRecord

# from ..core.sequence import Sequence
# from ..io import to_fasta
# from .decorators import lazy_property

# STR_TYPES = [str]

# __all__ = [
#     "mafft",
#     "muscle",
#     "muscle_v3",
#     "local_alignment",
#     "global_alignment",
#     "semiglobal_alignment",
#     "dot_alignment",
#     "LocalAlignment",
#     "GlobalAlignment",
#     "SemiGlobalAlignment",
# ]


# # -------------------------------------
# #
# #     MULTIPLE SEQUENCE ALIGNMENT
# #
# # -------------------------------------


# def mafft(
#     sequences: Union[str, Iterable],
#     alignment_file: Optional[str] = None,
#     fmt: str = "fasta",
#     threads: int = -1,
#     as_file: bool = False,
#     as_string: bool = False,
#     reorder: bool = True,
#     mafft_bin: Optional[str] = None,
#     id_key: Optional[str] = None,
#     seq_key: Optional[str] = None,
#     debug: bool = False,
#     fasta: Optional[str] = None,
# ) -> Union[MultipleSeqAlignment, str]:
#     """
#     Performs multiple sequence alignment with `MAFFT`_.

#     Parameters
#     ----------
#     sequences : (str, iterable)
#         Can be one of several things:
#             1. path to a FASTA-formatted file
#             2. a FASTA-formatted string
#             3. a list of BioPython ``SeqRecord`` objects
#             4. a list of abutils ``Sequence`` objects
#             5. a list of lists/tuples, of the format ``[sequence_id, sequence]``

#     alignment_file : str, optional
#         Path for the output alignment file. Required if ``as_file`` is ``True``.

#     fmt : str, default='fasta'
#         Format of the output alignment. Choices are 'fasta', 'phylip', and 'clustal'.
#         Default is 'fasta'.

#     threads : int, default=-1
#         Number of threads for MAFFT to use. Default is ``-1``, which uses all
#         available CPUs.

#     as_file: bool, default=False
#         If ``True``, returns the path to the alignment file. If ``False``,
#         returns either a BioPython ``MultipleSeqAlignment`` object or the alignment
#         output as a ``str``, depending on `as_string`. If `alignment_file` is not
#         provided, a temporary file will be created with ``tempfile.NamedTemporaryFile``.
#         Note that this temporary file is created in ``"/tmp"`` and may be removed
#         by the operating system without notice.

#     as_string: bool, default=False
#         If ``True``, returns a the alignment output as a string. If ``False``,
#         returns a BioPython ``MultipleSeqAlignment`` object (obtained by calling
#         ``Bio.AlignIO.read()`` on the alignment file).

#     mafft_bin : str, optional
#         Path to a MAFFT executable. Default is ``None``, which results in
#         using the default system MAFFT binary (just calling ``'mafft'``).

#     id_key : str, default=None
#         Key to retrieve the sequence ID. If not provided or missing, ``Sequence.id`` is used.

#     sequence_key : str, default=None
#         Key to retrieve the sequence. If not provided or missing, ``Sequence.sequence`` is used.

#     debug : bool, default=False
#         If ``True``, prints MAFFT's standard output and standard error.
#         Default is ``False``.

#     fasta : str, optional
#         Path to a FASTA-formatted input file. Depricated (use `sequences`
#         for all types if input), but retained for backwards compatibility.


#     Returns
#     -------
#     alignment : str or ``MultipleSeqAlignment``
#         If ``as_file`` is ``True``, returns a path to the output alignment file,
#         Otherwise, returns a BioPython ``MultipleSeqAlignment`` object.


#     .. _MAFFT:
#         https://mafft.cbrc.jp/alignment/software/
#     """
#     # process input
#     if fasta is not None:
#         sequences = fasta
#     ffile = to_fasta(sequences, id_key=id_key, sequence_key=seq_key)
#     # configure output path
#     if alignment_file is None:
#         # as_file = False
#         alignment_file = tempfile.NamedTemporaryFile(delete=False).name
#     else:
#         alignment_file = os.path.abspath(alignment_file)
#     # do the alignment
#     aln_format = ""
#     if fmt.lower() == "clustal":
#         aln_format = "--clustalout "
#     if fmt.lower() == "phylip":
#         aln_format = "--phylipout "
#     if reorder:
#         aln_format += "--reorder "
#     if mafft_bin is None:
#         mafft_bin = "mafft"
#     mafft_cline = "{} --thread {} {}{} > {}".format(
#         mafft_bin, threads, aln_format, ffile, alignment_file
#     )
#     mafft = sp.Popen(
#         str(mafft_cline),
#         stdout=sp.PIPE,
#         stderr=sp.PIPE,
#         universal_newlines=True,
#         shell=True,
#     )
#     stdout, stderr = mafft.communicate()
#     # return output
#     if debug:
#         print(mafft_cline)
#         print(stdout)
#         print(stderr)
#     if os.stat(alignment_file).st_size == 0:
#         err = "WARNING: output alignment file is empty. "
#         err += "Verify that MAFFT is installed and the input data is valid,"
#         print(err)
#         return None
#     if as_file:
#         return alignment_file
#     with open(alignment_file, "r") as f:
#         if as_string:
#             aln = f.read()
#         else:
#             aln = AlignIO.read(f, fmt)
#     os.unlink(alignment_file)
#     return aln


# def muscle(
#     sequences: Union[str, Iterable],
#     alignment_file: Optional[str] = None,
#     as_file: bool = False,
#     as_string: bool = False,
#     muscle_bin: Optional[str] = None,
#     threads: Optional[int] = None,
#     id_key: Optional[str] = None,
#     seq_key: Optional[str] = None,
#     debug: bool = False,
#     fasta: Optional[str] = None,
# ) -> Union[MultipleSeqAlignment, str]:
#     """
#     Performs multiple sequence alignment with `MUSCLE`_ (version 5).

#     Parameters
#     ----------
#     sequences : (str, iterable)
#         Can be one of several things:
#             1. path to a FASTA-formatted file
#             2. a FASTA-formatted string
#             3. a list of BioPython ``SeqRecord`` objects
#             4. a list of abutils ``Sequence`` objects
#             5. a list of lists/tuples, of the format ``[sequence_id, sequence]``

#     alignment_file : str, optional
#         Path for the output alignment file. Required if ``as_file`` is ``True``.

#     as_file: bool, default=False
#         If ``True``, returns the path to the alignment file. If ``False``,
#         returns either a BioPython ``MultipleSeqAlignment`` object or the alignment
#         output as a ``str``, depending on `as_string`. If `alignment_file` is not
#         provided, a temporary file will be created with ``tempfile.NamedTemporaryFile``.
#         Note that this temporary file is created in ``"/tmp"`` and may be removed
#         by the operating system without notice.

#     as_string: bool, default=False
#         If ``True``, returns a the alignment output as a string. If ``False``,
#         returns a BioPython ``MultipleSeqAlignment`` object (obtained by calling
#         ``Bio.AlignIO.read()`` on the alignment file).

#     muscle_bin : str, optional
#         Path to a MUSCLE executable. If not provided, the MUSCLE binary bundled
#         with ``abutils`` will be used.

#     threads : int, default=None
#         Number of threads for MUSCLE to use. If not provided, MUSCLE uses all
#         available CPUs.

#     id_key : str, default=None
#         Key to retrieve the sequence ID. If not provided or missing, ``Sequence.id`` is used.

#     sequence_key : str, default=None
#         Key to retrieve the sequence. If not provided or missing, ``Sequence.sequence`` is used.

#     debug : bool, default=False
#         If ``True``, prints MAFFT's standard output and standard error.
#         Default is ``False``.

#     fasta : str, optional
#         Path to a FASTA-formatted input file. Depricated (use `sequences`
#         for all types if input), but retained for backwards compatibility.


#     Returns
#     -------
#     alignment : str or ``MultipleSeqAlignment``
#         If ``as_file`` is ``True``, returns a path to the output alignment file,
#         Otherwise, returns a BioPython ``MultipleSeqAlignment`` object.


#     .. _MUSCLE:
#         https://www.drive5.com/muscle/
#     """
#     # process input
#     if fasta is not None:
#         sequences = fasta
#     ffile = to_fasta(sequences, id_key=id_key, sequence_key=seq_key)
#     # configure output path
#     if alignment_file is None:
#         # as_file = False
#         alignment_file = tempfile.NamedTemporaryFile(delete=False).name
#     else:
#         alignment_file = os.path.abspath(alignment_file)
#     # muscle binary
#     if muscle_bin is None:
#         mod_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#         muscle_bin = os.path.join(
#             mod_dir, "bin/muscle_{}".format(platform.system().lower())
#         )
#     # do the alignment
#     muscle_cline = f"{muscle_bin} -align {ffile} -output {alignment_file}"
#     if threads is not None:
#         muscle_cline += f" -threads {threads}"
#     muscle = sp.Popen(str(muscle_cline), stdout=sp.PIPE, stderr=sp.PIPE, shell=True)
#     stdout, stderr = muscle.communicate()
#     if debug:
#         print("muscle binary path:", muscle_bin)
#         print("muscle command:", muscle_cline)
#         print(stdout)
#         print(stderr)
#     # output
#     if os.stat(alignment_file).st_size == 0:
#         err = "WARNING: output alignment file is empty. "
#         err += "Please verify that the input data is valid"
#         print(err)
#         return None
#     if as_file:
#         return alignment_file
#     with open(alignment_file, "r") as f:
#         if as_string:
#             aln = f.read()
#         else:
#             aln = AlignIO.read(f, "fasta")
#     os.unlink(alignment_file)
#     return aln


# def muscle_v3(
#     sequences: Union[str, Iterable, None] = None,
#     alignment_file: Optional[str] = None,
#     fmt: str = "fasta",
#     as_file: bool = False,
#     as_string: bool = False,
#     muscle_bin: Optional[str] = None,
#     maxiters: Optional[int] = None,
#     diags: Optional[int] = None,
#     gap_open: Optional[int] = None,
#     gap_extend: Optional[int] = None,
#     id_key: Optional[str] = None,
#     seq_key: Optional[str] = None,
#     debug: bool = False,
#     fasta: Optional[str] = None,
# ) -> Union[MultipleSeqAlignment, str]:
#     """
#     Performs multiple sequence alignment with `MUSCLE`_ (version 3).

#     Parameters
#     ----------
#     sequences : (str, iterable)
#         Can be one of several things:
#             1. path to a FASTA-formatted file
#             2. a FASTA-formatted string
#             3. a list of BioPython ``SeqRecord`` objects
#             4. a list of abutils ``Sequence`` objects
#             5. a list of lists/tuples, of the format ``[sequence_id, sequence]``

#     alignment_file : str, optional
#         Path for the output alignment file. Required if ``as_file`` is ``True``.

#     fmt : str, default='fasta'
#         Format of the output alignment. Choices are 'fasta', 'phylip', and 'clustal'.
#         Default is 'fasta'.

#     threads : int, default=-1
#         Number of threads for MUSCLE to use. Default is ``-1``, which uses all
#         available CPUs.

#     as_file: bool, default=False
#         If ``True``, returns the path to the alignment file. If ``False``,
#         returns either a BioPython ``MultipleSeqAlignment`` object or the alignment
#         output as a ``str``, depending on `as_string`. Requires that
#         `alignment_file` is also provided.

#     as_string: bool, default=False
#         If ``True``, returns a the alignment output as a string. If ``False``,
#         returns a BioPython ``MultipleSeqAlignment`` object (obtained by calling
#         ``Bio.AlignIO.read()`` on the alignment file).

#     maxiters : int, default=-1
#         Passed directly to MUSCLE using the ``-maxiters`` flag.

#     diags : int, default=None
#         Passed directly to MUSCLE using the ``-diags`` flag.

#     gap_open : float, default=None
#         Passed directly to MUSCLE using the ``-gapopen`` flag. Ignored
#         if ``gap_extend`` is not also provided.

#     gap_extend : float, default=None
#         Passed directly to MUSCLE using the ``-gapextend`` flag. Ignored
#         if ``gap_open`` is not also provided.

#     muscle_bin : str, optional
#         Path to a MUSCLE executable. If not provided, the MUSCLE binary bundled
#         with ``abutils`` will be used.

#     debug : bool, default=False
#         If ``True``, prints MAFFT's standard output and standard error.
#         Default is ``False``.

#     fasta : str, optional
#         Path to a FASTA-formatted input file. Depricated (use `sequences`
#         for all types if input), but retained for backwards compatibility.


#     Returns
#     -------
#     alignment : str or ``MultipleSeqAlignment``
#         If ``as_file`` is ``True``, returns a path to the output alignment file,
#         Otherwise, returns a BioPython ``MultipleSeqAlignment`` object.


#     .. _MUSCLE:
#         https://drive5.com/muscle/downloads_v3.htm

#     """
#     # process input
#     if fasta is not None:
#         sequences = fasta
#     fasta_string = to_fasta(
#         sequences,
#         as_string=True,
#         id_key=id_key,
#         sequence_key=seq_key,
#     )
#     # configure output path
#     if alignment_file is None:
#         as_file = False
#         alignment_file = tempfile.NamedTemporaryFile(delete=False).name
#     else:
#         alignment_file = os.path.abspath(alignment_file)
#     # muscle binary
#     if muscle_bin is None:
#         mod_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#         muscle_bin = os.path.join(
#             mod_dir, "bin/muscle3_{}".format(platform.system().lower())
#         )
#     # do the alignment
#     aln_format = ""
#     if fmt == "clustal":
#         aln_format = " -clwstrict"
#     muscle_cline = "{}{} ".format(muscle_bin, aln_format)
#     if maxiters is not None:
#         muscle_cline += " -maxiters {}".format(maxiters)
#     if diags:
#         muscle_cline += " -diags"
#     if all([gap_open is not None, gap_extend is not None]):
#         muscle_cline += " -gapopen {} -gapextend {}".format(gap_open, gap_extend)
#     if debug:
#         print("muscle binary path:", muscle_bin)
#         print("muscle command:", muscle_cline)
#     muscle = sp.Popen(
#         str(muscle_cline),
#         stdin=sp.PIPE,
#         stdout=sp.PIPE,
#         stderr=sp.PIPE,
#         universal_newlines=True,
#         shell=True,
#     )
#     alignment, stderr = muscle.communicate(input=fasta_string)
#     if debug:
#         print(stderr)
#     # output
#     if as_string:
#         return alignment
#     elif as_file:
#         with open(alignment_file, "w") as f:
#             f.write(alignment)
#     else:
#         return AlignIO.read(StringIO(alignment), fmt)


# def consensus(aln, name=None, threshold=0.51, ambiguous="N"):
#     summary_align = AlignInfo.SummaryInfo(aln)
#     consensus = summary_align.gap_consensus(threshold=threshold, ambiguous=ambiguous)
#     if name is None:
#         name = uuid.uuid4()
#     consensus_string = str(consensus).replace("-", "")
#     return (name, consensus_string.upper())


# # ----------------------------
# #
# #     PAIRWISE ALIGNMENT
# #
# # ----------------------------


# class PairwiseAlignment(ABC):
#     """
#     Base class for local, global, and semi-global pairwise alignments.

#     .. note::

#         All comparisons between ``PairwiseAlignment``s are done on the
#         ``score`` attribute. This was done so that sorting alignments
#         like so::

#             list_of_alignments.sort(reverse=True)

#         produces a sorted list of alignments from the highest alignment
#         score to the lowest.

#     Attributes
#     ----------

#         query (Sequence): The input query sequence, as an AbTools
#             ``Sequence`` object.

#         target (Sequence): The input target sequence, as an AbTools
#             ``Sequence`` object.

#         target_id (str): ID of the target sequence.

#         raw_query: The raw query, before conversion to a ``Sequence``.

#         raw_target: The raw target, before conversion to a ``Sequence``.

#     """

#     def __init__(
#         self,
#         query: Sequence,
#         target: Sequence,
#         match: int = 3,
#         mismatch: int = -2,
#         gap_open: int = 5,
#         gap_extend: int = 2,
#         matrix: Optional[parasail.Matrix] = None,
#         gap_open_penalty: Optional[int] = None,
#         gap_extend_penalty: Optional[int] = None,
#     ):
#         self.query = query
#         self.target = target
#         self.match = int(match)
#         self.mismatch = int(mismatch)
#         self.gap_open = self.get_gap_open(gap_open, gap_open_penalty)
#         self.gap_extend = self.get_gap_extend(gap_extend, gap_extend_penalty)
#         self.matrix = self.get_matrix(matrix)
#         self._alignment = None
#         self._traceback = None
#         self._cigar = None

#         # for backwards compatibility:
#         self.raw_query = query
#         self.raw_target = target

#     def __repr__(self):
#         if len(self.aligned_query) > 20:
#             qstring = "{}...{}".format(
#                 self.aligned_query[:10], self.aligned_query[-10:]
#             )
#             mstring = "{}...{}".format(
#                 self.alignment_midline[:10], self.alignment_midline[-10:]
#             )
#             tstring = "{}...{}".format(
#                 self.aligned_target[:10], self.aligned_target[-10:]
#             )
#         else:
#             qstring = self.aligned_query
#             mstring = self.alignment_midline
#             tstring = self.aligned_target
#         lines = []
#         lines.append("Pairwise Alignment")
#         lines.append("------------------\n")
#         lines.append(f"query:  {qstring}")
#         lines.append(f"        {mstring}")
#         lines.append(f"target: {tstring}\n")
#         lines.append(f"score: {self.score}")
#         lines.append(f"type: {self.alignment_type}")
#         lines.append(f"length: {len(self.aligned_query)}")
#         print("\n".join(lines))
#         return ""

#     def __str__(self):
#         lines = []
#         lines.append(self.aligned_query)
#         lines.append(self.alignment_midline)
#         lines.append(self.aligned_target)
#         return "\n".join(lines)

#     def __len__(self):
#         try:
#             return len(self.aligned_query)
#         except AttributeError:
#             return 0

#     def __eq__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score == other
#             return False
#         return self.score == other.score

#     def __lt__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score < other
#             return False
#         return self.score < other.score

#     def __le__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score <= other
#             return False
#         return self.score <= other.score

#     def __gt__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score > other
#             return False
#         return self.score > other.score

#     def __ge__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score >= other
#             return False
#         return self.score >= other.score

#     @lazy_property
#     def profile(self):
#         return parasail.profile_create_16(
#             self.query.sequence, self.match, self.mismatch, self.matrix
#         )

#     @lazy_property
#     def alignment(self):
#         return self.align()

#     @property
#     def cigar(self):
#         cigar_string = self.alignment.cigar.decode.decode("utf-8")
#         return CIGAR(cigar_string=cigar_string)

#     @property
#     def traceback(self):
#         return self.alignment.traceback

#     @lazy_property
#     def score(self):
#         return self.alignment.score

#     @lazy_property
#     def aligned_query(self):
#         return self.traceback.query

#     @lazy_property
#     def aligned_target(self):
#         return self.traceback.ref

#     @lazy_property
#     def alignment_midline(self):
#         return self.traceback.comp.replace(".", " ")

#     @lazy_property
#     def query_begin(self):
#         # return self.alignment.cigar.beg_query
#         query_begin = self.alignment.cigar.beg_query
#         if self.cigar[0].element == "I":
#             query_begin += self.cigar[0].length
#         return query_begin

#     @lazy_property
#     def query_end(self):
#         return self.alignment.end_query

#     @lazy_property
#     def target_begin(self):
#         # return self.alignment.cigar.beg_ref
#         target_begin = self.alignment.cigar.beg_ref
#         if self.cigar[0].element == "D":
#             target_begin += self.cigar[0].length
#         return target_begin

#     @lazy_property
#     def target_end(self):
#         return self.alignment.end_ref

#     @property
#     def target_id(self):
#         return self.target.id

#     def align(self):
#         """
#         docstring for align
#         """
#         aln = self.alignment_function(
#             self.query.sequence,
#             self.target.sequence,
#             self.gap_open,
#             self.gap_extend,
#             self.matrix,
#         )
#         return aln

#     def get_gap_open(self, gap_open, gap_open_penalty):
#         if gap_open_penalty is not None:
#             gap_open = gap_open_penalty
#         return abs(int(gap_open))

#     def get_gap_extend(self, gap_extend, gap_extend_penalty):
#         if gap_extend_penalty is not None:
#             gap_extend = gap_extend_penalty
#         return abs(int(gap_extend))

#     def get_matrix(self, matrix):
#         if matrix is None:
#             residues = "ACDEFGHIKLMNPQRSTVWY"
#             return parasail.matrix_create(residues, self.match, self.mismatch)
#         elif isinstance(matrix, str):
#             if os.path.isfile(matrix):
#                 try:
#                     return parasail.Matrix(matrix)
#                 except:
#                     err = (
#                         f"\nERROR: the provided matrix file:\n{matrix}\ndoes not exist "
#                     )
#                     err += "or is improperly formatted. Please provide a valid matrix file.\n"
#                     print(err)
#                     sys.exit(1)
#             else:
#                 try:
#                     return parasail.Matrix(matrix.lower())
#                 except ValueError:
#                     err = f"\nERROR: the provided matrix name {matrix} was not found. "
#                     err += "Please provide a valid parasail matrix name.\n"
#                     print(err)
#                     sys.exit(1)
#         elif isinstance(matrix, parasail.Matrix):
#             return matrix
#         else:
#             err = "\nERROR: If supplied, matrix must be either the name of a built-in "
#             err += "parasail matrix, the path to a properly formatted matrix file, "
#             err += f"or a parasail.Matrix object. You supplied:\n{matrix}\n"
#             print(err)
#             sys.exit(1)


# class LocalAlignment(PairwiseAlignment):
#     """
#     docstring for LocalAlignment
#     """

#     def __init__(
#         self,
#         query: Sequence,
#         target: Sequence,
#         match: int = 3,
#         mismatch: int = -2,
#         gap_open: int = 5,
#         gap_extend: int = 2,
#         matrix: Union[str, parasail.Matrix, None] = None,
#         gap_open_penalty: Optional[int] = None,
#         gap_extend_penalty: Optional[int] = None,
#         alignment_function: Callable = parasail.sw_trace_striped_16,
#     ):
#         super().__init__(
#             query=query,
#             target=target,
#             matrix=matrix,
#             match=match,
#             mismatch=mismatch,
#             gap_open=gap_open,
#             gap_extend=gap_extend,
#             gap_open_penalty=gap_open_penalty,
#             gap_extend_penalty=gap_extend_penalty,
#         )
#         self.alignment_function = alignment_function
#         self.alignment_type = "local"


# class GlobalAlignment(PairwiseAlignment):
#     """
#     docstring for LocalAlignment
#     """

#     def __init__(
#         self,
#         query: Sequence,
#         target: Sequence,
#         match: int = 3,
#         mismatch: int = -2,
#         gap_open: int = 5,
#         gap_extend: int = 2,
#         matrix: Union[str, parasail.Matrix, None] = None,
#         gap_open_penalty: Optional[int] = None,
#         gap_extend_penalty: Optional[int] = None,
#         alignment_function: Callable = parasail.nw_trace_striped_16,
#     ):
#         super().__init__(
#             query=query,
#             target=target,
#             matrix=matrix,
#             match=match,
#             mismatch=mismatch,
#             gap_open=gap_open,
#             gap_extend=gap_extend,
#             gap_open_penalty=gap_open_penalty,
#             gap_extend_penalty=gap_extend_penalty,
#         )
#         self.alignment_function = alignment_function
#         self.alignment_type = "global"


# class SemiGlobalAlignment(PairwiseAlignment):
#     """
#     docstring for LocalAlignment
#     """

#     def __init__(
#         self,
#         query: Sequence,
#         target: Sequence,
#         match: int = 3,
#         mismatch: int = -2,
#         gap_open: int = 5,
#         gap_extend: int = 2,
#         matrix: Union[str, parasail.Matrix, None] = None,
#         gap_open_penalty: Optional[int] = None,
#         gap_extend_penalty: Optional[int] = None,
#         alignment_function: Callable = parasail.sg_trace_striped_16,
#     ):
#         super().__init__(
#             query=query,
#             target=target,
#             matrix=matrix,
#             match=match,
#             mismatch=mismatch,
#             gap_open=gap_open,
#             gap_extend=gap_extend,
#             gap_open_penalty=gap_open_penalty,
#             gap_extend_penalty=gap_extend_penalty,
#         )
#         self.alignment_function = alignment_function
#         self.alignment_type = "semi-global"


# class CIGAR:
#     """
#     Parsable representation of a CIGAR string. The CIGAR parsing code was taken
#     almost vertabim from the `Cigar`_ package. Init argument is a CIGAR string.
#     Iterating over a ``CIGAR`` object yields ``CIGARElement``s. The length of
#     a ``CIGAR`` object is the length of the alignment encoded by the CIGAR string,
#     not the length of the CIGAR string itself::

#         cig = CIGAR("99=2X99=")
#         len(cig) # --> 200
#         len(cig.cigar_string) # --> 8

#     .. _Cigar
#         https://github.com/brentp/cigar
#     """

#     def __init__(self, cigar_string):
#         self.cigar_string = cigar_string
#         self._cigar_list = None

#     def __str__(self):
#         return self.cigar_string

#     def __repr__(self):
#         print(self.cigar_string)
#         return ""

#     def __len__(self):
#         return sum([c.length for c in self.cigar_list])

#     def __getitem__(self, slice):
#         return self.cigar_list[slice]

#     def __iter__(self):
#         for c in self.cigar_list:
#             yield c

#     @property
#     def cigar_list(self):
#         if self._cigar_list is None:
#             cigar_list = []
#             cig_iter = groupby(self.cigar_string, lambda c: c.isdigit())
#             for _, n in cig_iter:
#                 ce = CIGARElement(
#                     length=int("".join(n)), element="".join(next(cig_iter)[1])
#                 )
#                 cigar_list.append(ce)
#             self._cigar_list = cigar_list
#         return self._cigar_list


# class CIGARElement:
#     """
#     Representation of a single CIGAR element.
#     """

#     def __init__(self, length, element):
#         self.length: int = int(length)
#         self.element: str = element


# def process_targets(target, targets):
#     if target is not None:
#         return [Sequence(target)]
#     return [Sequence(t) for t in targets]


# def local_alignment(
#     query: Union[str, SeqRecord, Sequence, Iterable],
#     target: Union[str, SeqRecord, Sequence, Iterable, None] = None,
#     targets: Optional[Iterable] = None,
#     match: int = 3,
#     mismatch: int = -2,
#     gap_open: int = -5,
#     gap_extend: int = -2,
#     matrix: Union[str, parasail.Matrix, None] = None,
#     alignment_function: Callable = parasail.sw_trace_striped_16,
#     aa: bool = False,
#     gap_open_penalty: Optional[int] = None,
#     gap_extend_penalty: Optional[int] = None,
# ) -> Union[LocalAlignment, Iterable]:
#     """
#     Striped Smith-Waterman local pairwise sequence alignment.

#     Parameters
#     ----------
#     query : str, SeqRecord, Sequence, or list
#         Query sequence. Can be any of the following:
#             1. a nucleotide or amino acid sequence, as a string
#             2. a Biopython ``SeqRecord`` object
#             3. an abutils ``Sequence`` object
#             4. a ``list`` or ``tuple`` of the format ``[seq_id, sequence]``

#     target : str, SeqRecord, Sequence, or list, default=None
#         Target sequence. Can be anything accepted by `query`. One of
#         `target` or `targets` must be provided.

#     target : str, SeqRecord, Sequence, or list, default=None
#         Iterable of multiple target sequences. A ``list`` or ``tuple`` of
#         anything accepted by `query`. One of `target` or `targets` must be provided.

#     match : int, default=3
#         Match score. Should be a positive integer.

#     mismatch : int, default=-2
#         Mismatch score. Typically should be less than or equal to ``0``.

#     gap_open : int, default=-5
#         Gap open score. Must be less than or equal to ``0``.

#     gap_extend : int, default=-2
#         Gap extension score. Must be less than or equal to ``0``.

#     matrix : str, dict or parasail.Matrix, default=None
#         Scoring matrix. Can be either:
#             - the name of a ``parasail`` `built-in substitution matrix`_.
#             - path to a matrix file (in a format accepted by ``parasail``).
#             - a ``parasail.Matrix`` object
#         If not provided, a matrix will be created using `match` and `mismatch`.

#     alignment_function : Callable, default=parasail.sw_trace_striped_16
#         ``parasail`` `local alignment function`_ to be used for alignment.

#     aa : bool, default=False
#         Not used. Retained for backwards compatibility.

#     gap_open_penalty : int, default=None
#         Depricated, but retained for backwards compatibility. Use `gap_open`
#         insteaed.

#     gap_extend_penalty : int, default=None
#         Depricated, but retained for backwards compatibility. Use `gap_exted`
#         insteaed.


#     Returns
#     -------
#     alignment : LocalAlignment or iterable
#         If a single target sequence is provided (via ``target``), a single ``LocalAlignment``
#         object will be returned. If multiple target sequences are supplied (via ``targets``),
#         a list of ``LocalAlignment`` objects will be returned.


#     .. _local alignment function
#         https://github.com/jeffdaily/parasail-python#standard-function-naming-convention

#     .. _built-in substitution matrix
#         https://github.com/jeffdaily/parasail-python#substitution-matrices
#     """
#     # input processing
#     query = Sequence(query)
#     targets = process_targets(target, targets)
#     # alignment
#     alignments = []
#     for target in targets:
#         aln = LocalAlignment(
#             query=query,
#             target=target,
#             match=match,
#             mismatch=mismatch,
#             gap_open=gap_open,
#             gap_extend=gap_extend,
#             matrix=matrix,
#             alignment_function=alignment_function,
#             gap_open_penalty=gap_open_penalty,
#             gap_extend_penalty=gap_extend_penalty,
#         )
#         alignments.append(aln)
#     if len(alignments) == 1:
#         return alignments[0]
#     return alignments


# def global_alignment(
#     query: Union[str, SeqRecord, Sequence, Iterable],
#     target: Union[str, SeqRecord, Sequence, Iterable, None] = None,
#     targets: Optional[Iterable] = None,
#     match: int = 3,
#     mismatch: int = -2,
#     gap_open: int = -5,
#     gap_extend: int = -2,
#     matrix: Union[str, parasail.Matrix, None] = None,
#     alignment_function: Callable = parasail.nw_trace_striped_16,
#     aa: bool = False,
#     gap_open_penalty: Optional[int] = None,
#     gap_extend_penalty: Optional[int] = None,
# ) -> Union[GlobalAlignment, Iterable]:
#     """
#     Needleman-Wunch global pairwise sequence alignment.

#     Parameters
#     ----------
#     query : str, SeqRecord, Sequence, or list
#         Query sequence. Can be any of the following:
#             1. a nucleotide or amino acid sequence, as a string
#             2. a Biopython ``SeqRecord`` object
#             3. an abutils ``Sequence`` object
#             4. a ``list`` or ``tuple`` of the format ``[seq_id, sequence]``

#     target : str, SeqRecord, Sequence, or list, default=None
#         Target sequence. Can be anything accepted by `query`. One of
#         `target` or `targets` must be provided.

#     target : str, SeqRecord, Sequence, or list, default=None
#         Iterable of multiple target sequences. A ``list`` or ``tuple`` of
#         anything accepted by `query`. One of `target` or `targets` must be provided.

#     match : int, default=3
#         Match score. Should be a positive integer.

#     mismatch : int, default=-2
#         Mismatch score. Typically should be less than or equal to ``0``.

#     gap_open : int, default=-5
#         Gap open score. Must be  less than or equal to ``0``.

#     gap_extend : int, default=-2
#         Gap extension score. Must be less than or equal to ``0``.

#     matrix : str, dict or parasail.Matrix, default=None
#         Scoring matrix. Can be either:
#             - the name of a ``parasail`` `built-in substitution matrix`_.
#             - path to a matrix file (in a format accepted by ``parasail``).
#             - a ``parasail.Matrix`` object
#         If not provided, a matrix will be created using `match` and `mismatch`.

#     alignment_function : Callable, default=parasail.sw_trace_striped_16
#         ``parasail`` `global alignment function`_ to be used for alignment.

#     aa : bool, default=False
#         Not used. Retained for backwards compatibility.

#     gap_open_penalty : int, default=None
#         Depricated, but retained for backwards compatibility. Use `gap_open`
#         insteaed.

#     gap_extend_penalty : int, default=None
#         Depricated, but retained for backwards compatibility. Use `gap_exted`
#         insteaed.


#     Returns
#     -------
#     alignment : GlobalAlignment or iterable
#         If a single target sequence is provided (via ``target``), a single ``GlobalAlignment``
#         object will be returned. If multiple target sequences are supplied (via ``targets``),
#         a list of ``GlobalAlignment`` objects will be returned.


#     .. _global alignment function
#         https://github.com/jeffdaily/parasail-python#standard-function-naming-convention

#     .. _built-in substitution matrix
#         https://github.com/jeffdaily/parasail-python#substitution-matrices
#     """
#     # input processing
#     query = Sequence(query)
#     targets = process_targets(target, targets)
#     # alignment
#     alignments = []
#     for target in targets:
#         aln = GlobalAlignment(
#             query=query,
#             target=target,
#             match=match,
#             mismatch=mismatch,
#             gap_open=gap_open,
#             gap_extend=gap_extend,
#             matrix=matrix,
#             alignment_function=alignment_function,
#             gap_open_penalty=gap_open_penalty,
#             gap_extend_penalty=gap_extend_penalty,
#         )
#         alignments.append(aln)
#     if len(alignments) == 1:
#         return alignments[0]
#     return alignments


# def semiglobal_alignment(
#     query: Union[str, SeqRecord, Sequence, Iterable],
#     target: Union[str, SeqRecord, Sequence, Iterable, None] = None,
#     targets: Optional[Iterable] = None,
#     match: int = 3,
#     mismatch: int = -2,
#     gap_open: int = -5,
#     gap_extend: int = -2,
#     matrix: Union[str, parasail.Matrix, None] = None,
#     alignment_function: Callable = parasail.sg_trace_striped_16,
#     aa: bool = False,
#     gap_open_penalty: Optional[int] = None,
#     gap_extend_penalty: Optional[int] = None,
# ) -> Union[SemiGlobalAlignment, Iterable]:
#     """
#     Semi-global pairwise sequence alignment.

#     Parameters
#     ----------
#     query : str, SeqRecord, Sequence, or list
#         Query sequence. Can be any of the following:
#             1. a nucleotide or amino acid sequence, as a string
#             2. a Biopython ``SeqRecord`` object
#             3. an abutils ``Sequence`` object
#             4. a ``list`` or ``tuple`` of the format ``[seq_id, sequence]``

#     target : str, SeqRecord, Sequence, or list, default=None
#         Target sequence. Can be anything accepted by `query`. One of
#         `target` or `targets` must be provided.

#     target : str, SeqRecord, Sequence, or list, default=None
#         Iterable of multiple target sequences. A ``list`` or ``tuple`` of
#         anything accepted by `query`. One of `target` or `targets` must be provided.

#     match : int, default=3
#         Match score. Should be a positive integer.

#     mismatch : int, default=-2
#         Mismatch score. Typically should be less than or equal to ``0``.

#     gap_open : int, default=-5
#         Gap open score. Must be  less than or equal to ``0``.

#     gap_extend : int, default=-2
#         Gap extension score. Must be less than or equal to ``0``.

#     matrix : str, dict or parasail.Matrix, default=None
#         Scoring matrix. Can be either:
#             - the name of a ``parasail`` `built-in substitution matrix`_.
#             - path to a matrix file (in a format accepted by ``parasail``).
#             - a ``parasail.Matrix`` object
#         If not provided, a matrix will be created using `match` and `mismatch`.

#     alignment_function : Callable, default=parasail.sw_trace_striped_16
#         ``parasail`` `semi-global alignment function`_ to be used for alignment.

#     aa : bool, default=False
#         Not used. Retained for backwards compatibility.

#     gap_open_penalty : int, default=None
#         Depricated, but retained for backwards compatibility. Use `gap_open`
#         insteaed.

#     gap_extend_penalty : int, default=None
#         Depricated, but retained for backwards compatibility. Use `gap_exted`
#         insteaed.


#     Returns
#     -------
#     alignment : SemiGlobalAlignment or iterable
#         If a single target sequence is provided (via ``target``), a single ``SemiGlobalAlignment``
#         object will be returned. If multiple target sequences are supplied (via ``targets``),
#         a list of ``SemiGlobalAlignment`` objects will be returned.


#     .. _semi-global alignment function
#         https://github.com/jeffdaily/parasail-python#standard-function-naming-convention

#     .. _built-in substitution matrix
#         https://github.com/jeffdaily/parasail-python#substitution-matrices
#     """
#     # input processing
#     query = Sequence(query)
#     targets = process_targets(target, targets)
#     # alignment
#     alignments = []
#     for target in targets:
#         aln = SemiGlobalAlignment(
#             query=query,
#             target=target,
#             match=match,
#             mismatch=mismatch,
#             gap_open=gap_open,
#             gap_extend=gap_extend,
#             matrix=matrix,
#             alignment_function=alignment_function,
#             gap_open_penalty=gap_open_penalty,
#             gap_extend_penalty=gap_extend_penalty,
#         )
#         alignments.append(aln)
#     if len(alignments) == 1:
#         return alignments[0]
#     return alignments


# def local_alignment(
#     query: Union[str, SeqRecord, Sequence, Iterable],
#     target: Union[str, SeqRecord, Sequence, Iterable, None] = None,
#     targets: Optional[Iterable] = None,
#     match: int = 3,
#     mismatch: int = -2,
#     gap_open: int = -5,
#     gap_extend: int = -2,
#     matrix: Union[str, dict, None] = None,
#     aa: bool = False,
#     gap_open_penalty: Optional[int] = None,
#     gap_extend_penalty: Optional[int] = None,
# ):
#     """
#     Striped Smith-Waterman local pairwise alignment.

#     Parameters
#     ----------
#     query : str, SeqRecord, Sequence, or list
#         Query sequence. Can be any of the following:
#             1. a nucleotide or amino acid sequence, as a string
#             2. a Biopython ``SeqRecord`` object
#             3. an abutils ``Sequence`` object
#             4. a ``list`` or ``tuple`` of the format ``[seq_id, sequence]``

#     target : str, SeqRecord, Sequence, or list, default=None
#         Target sequence. Can be anything accepted by `query`. One of
#         `target` or `targets` must be provided.

#     target : str, SeqRecord, Sequence, or list, default=None
#         Iterable of multiple target sequences. A ``list`` or ``tuple`` of
#         anything accepted by `query`. One of `target` or `targets` must be provided.

#     match : int, default=3
#         Match score. Should be a positive integer.

#     mismatch : int, default=-2
#         Mismatch score. Typically should be less than or equal to ``0``.

#     gap_open : int, default=-5
#         Gap open score. Typically should be less than or equal to ``0``.

#     gap_extend : int, default=-2
#         Gap extension score. Typically should be less than or equal to ``0``.

#     matrix : str, dict, default=None
#         Scoring matrix for amino acid alignments. Can be either:
#             - The name of a built-in matrix. Current options are
#             ``"blosum62"`` and ``"pam250"``.
#             - A nested dictionary, giving an alignment score for each residue
#             pair. Should be formatted such that retrieving the alignment score
#             for ``"A"`` and ``"G"`` is accomplished by::
#                 matrix['A']['G']
#         If not provided, ``"blosum62"`` is used.

#     aa : bool, default=False
#         Must be set to ``True`` if sequences are amino acids.

#     gap_open_penalty : int, default=None
#         Depricated, but retained for backwards compatibility. Use `gap_open`
#         insteaed. Should be a positive integer.

#     gap_extend_penalty : int, default=None
#         Depricated, but retained for backwards compatibility. Use `gap_exted`
#         insteaed. Should be a positive integer.


#     Returns
#     -------
#     alignment : SSWAlignment or iterable
#         If a single target sequence is provided (via ``target``), a single ``SSWAlignment``
#         object will be returned. If multiple target sequences are supplied (via ``targets``),
#         a list of ``SSWAlignment`` objects will be returned.
#     """
#     if aa and matrix is None:
#         matrix = "blosum62"
#     if not any([target, targets]):
#         err = "\nERROR: You must supply either ``target`` or ``targets``.\n"
#         raise RuntimeError(err)
#     if target:
#         targets = [
#             target,
#         ]
#     # to maintain backward compatibility with earlier abutils API
#     if gap_open_penalty is not None:
#         gap_open = -gap_open_penalty
#     if gap_extend_penalty is not None:
#         gap_extend = -gap_extend_penalty
#     # do the alignment
#     alignments = []
#     for t in targets:
#         try:
#             alignment = SSWAlignment(
#                 query=query,
#                 target=t,
#                 match=match,
#                 mismatch=mismatch,
#                 matrix=matrix,
#                 gap_open=-gap_open,
#                 gap_extend=-gap_extend,
#                 aa=aa,
#             )
#             alignments.append(alignment)
#         except IndexError:
#             continue
#     if len(alignments) == 1:
#         return alignments[0]
#     return alignments


# def local_alignment_biopython(
#     query,
#     target=None,
#     targets=None,
#     match=3,
#     mismatch=-2,
#     matrix=None,
#     gap_open=-5,
#     gap_extend=-2,
#     aa=False,
# ):
#     if not target and not targets:
#         err = "ERROR: You must supply a target sequence (or sequences)."
#         raise RuntimeError(err)
#     if target:
#         targets = [
#             target,
#         ]
#     alignments = []
#     for t in targets:
#         try:
#             alignment = alignment = BiopythonAlignment(
#                 query=query,
#                 target=t,
#                 match=match,
#                 mismatch=mismatch,
#                 matrix=matrix,
#                 gap_open=gap_open,
#                 gap_extend=gap_extend,
#                 aa=aa,
#             )
#             alignments.append(alignment)
#         except IndexError:
#             continue
#     if len(alignments) == 1:
#         return alignments[0]
#     return alignments


# def global_alignment(
#     query,
#     target=None,
#     targets=None,
#     match=3,
#     mismatch=-2,
#     gap_open=-5,
#     gap_extend=-2,
#     score_match=None,
#     score_mismatch=None,
#     score_gap_open=None,
#     score_gap_extend=None,
#     matrix=None,
#     aa=False,
# ):
#     """
#     Needleman-Wunch global pairwise alignment.

#     With ``global_alignment``, you can score an alignment using different
#     paramaters than were used to compute the alignment. This allows you to
#     compute pure identity scores (match=1, mismatch=0) on pairs of sequences
#     for which those alignment parameters would be unsuitable. For example::

#         seq1 = 'ATGCAGC'
#         seq2 = 'ATCAAGC'

#     using identity scoring params (match=1, all penalties are 0) for both alignment
#     and scoring produces the following alignment::

#         ATGCA-GC
#         || || ||
#         AT-CAAGC

#     with an alignment score of 6 and an alignment length of 8 (identity = 75%). But
#     what if we want to calculate the identity of a gapless alignment? Using::

#         global_alignment(seq1, seq2,
#                          gap_open=20,
#                          score_match=1,
#                          score_mismatch=0,
#                          score_gap_open=10,
#                          score_gap_extend=1)

#     we get the following alignment::

#         ATGCAGC
#         ||  |||
#         ATCAAGC

#     which has an score of 5 and an alignment length of 7 (identity = 71%). Obviously,
#     this is an overly simple example (it would be much easier to force gapless alignment
#     by just iterating over each sequence and counting the matches), but there are several
#     real-life cases in which different alignment and scoring paramaters are desirable.

#     Args:

#         query: Query sequence. ``query`` can be one of four things:

#             1. a nucleotide or amino acid sequence, as a string

#             2. a Biopython ``SeqRecord`` object

#             3. an AbTools ``Sequence`` object

#             4. a list/tuple of the format ``[seq_id, sequence]``

#         target: A single target sequence. ``target`` can be anything that
#             ``query`` accepts.

#         targets (list): A list of target sequences, to be proccssed iteratively.
#             Each element in the ``targets`` list can be anything accepted by
#             ``query``.

#         match (int): Match score for alignment. Should be a positive integer. Default is 3.

#         mismatch (int): Mismatch score for alignment. Should be a negative integer. Default is -2.

#         gap_open (int): Penalty for opening gaps in alignment. Should be a negative integer.
#             Default is -5.

#         gap_extend (int): Penalty for extending gaps in alignment. Should be a negative
#             integer. Default is -2.

#         score_match (int): Match score for scoring the alignment. Should be a positive integer.
#             Default is to use the score from ``match`` or ``matrix``, whichever is provided.

#         score_mismatch (int): Mismatch score for scoring the alignment. Should be a negative
#             integer. Default is to use the score from ``mismatch`` or ``matrix``, whichever
#             is provided.

#         score_gap_open (int): Gap open penalty for scoring the alignment. Should be a negative
#             integer. Default is to use ``gap_open``.

#         score_gap_extend (int): Gap extend penalty for scoring the alignment. Should be a negative
#             integer. Default is to use ``gap_extend``.

#         matrix (str, dict): Alignment scoring matrix. Two options for passing the alignment matrix:

#             - The name of a built-in matrix. Current options are ``blosum62`` and ``pam250``.

#             - A nested dictionary, giving an alignment score for each residue pair. Should be
#               formatted such that retrieving the alignment score for A and G is accomplished by::

#                 matrix['A']['G']

#         aa (bool): Must be set to ``True`` if aligning amino acid sequences. Default
#             is ``False``.

#     Returns:

#         If a single target sequence is provided (via ``target``), a single ``NWAlignment``
#         object will be returned. If multiple target sequences are supplied (via ``targets``),
#         a list of ``NWAlignment`` objects will be returned.
#     """
#     if not target and not targets:
#         err = "ERROR: You must supply a target sequence (or sequences)."
#         raise RuntimeError(err)
#     if target:
#         targets = [
#             target,
#         ]
#     if type(targets) not in (list, tuple):
#         err = "ERROR: ::targets:: requires an iterable (list or tuple)."
#         err += "For a single sequence, use ::target::"
#         raise RuntimeError(err)
#     alignments = []
#     for t in targets:
#         alignment = NWAlignment(
#             query=query,
#             target=t,
#             match=match,
#             mismatch=mismatch,
#             gap_open=gap_open,
#             gap_extend=gap_extend,
#             score_match=score_match,
#             score_mismatch=score_mismatch,
#             score_gap_open=score_gap_open,
#             score_gap_extend=score_gap_extend,
#             matrix=matrix,
#             aa=aa,
#         )
#         alignments.append(alignment)
#     if target is not None:
#         return alignments[0]
#     return alignments


# class BaseAlignment(object):
#     """
#     Base class for local and global pairwise alignments.

#     .. note::

#         All comparisons between ``BaseAlignments``
#         are done on the ``score`` attribute (which must be implemented
#         by any classes that subclass ``BaseAlignment``). This was done
#         so that sorting alignments like so::

#             alignments = [list of alignments]
#             alignments.sort(reverse=True)

#         results in a sorted list of alignments from the highest alignment
#         score to the lowest.

#     Attributes:

#         query (Sequence): The input query sequence, as an AbTools
#             ``Sequence`` object.

#         target (Sequence): The input target sequence, as an AbTools
#             ``Sequence`` object.

#         target_id (str): ID of the target sequence.

#         raw_query: The raw query, before conversion to a ``Sequence``.

#         raw_target: The raw target, before conversion to a ``Sequence``.

#     """

#     def __init__(
#         self, query, target, matrix, match, mismatch, gap_open, gap_extend, aa
#     ):
#         super(BaseAlignment, self).__init__()
#         self.raw_query = query
#         self.raw_target = target
#         self.query = self._process_sequence(query, aa=aa)
#         self.target = self._process_sequence(target, aa=aa)
#         self._matrix = matrix
#         self._match = int(match)
#         self._mismatch = int(mismatch)
#         self._gap_open = int(gap_open)
#         self._gap_extend = int(gap_extend)
#         self._aa = bool(aa)

#     def __repr__(self):
#         if len(self.aligned_query) > 20:
#             qstring = "{}...{}".format(
#                 self.aligned_query[:10], self.aligned_query[-10:]
#             )
#             mstring = "{}...{}".format(
#                 self.alignment_midline[:10], self.alignment_midline[-10:]
#             )
#             tstring = "{}...{}".format(
#                 self.aligned_target[:10], self.aligned_target[-10:]
#             )
#         else:
#             qstring = self.aligned_query
#             mstring = self.alignment_midline
#             tstring = self.aligned_target
#         return_string = "\n\n"
#         return_string += "Pairwise Alignment\n"
#         return_string += "------------------\n\n"
#         return_string += "query:  {}\n".format(qstring)
#         return_string += "        {}\n".format(mstring)
#         return_string += "target: {}\n\n".format(tstring)
#         return_string += "score: {}\n".format(str(self.score))
#         return_string += "type: {}\n".format(self.alignment_type)
#         return_string += "length: {}".format(str(len(self.aligned_query)))
#         print(return_string)
#         return ""

#     def __str__(self):
#         return_string = ""
#         return_string += "{}\n".format(self.aligned_query)
#         return_string += "{}\n".format(self.alignment_midline)
#         return_string += "{}\n".format(self.aligned_target)
#         return return_string

#     def __len__(self):
#         return len(self.aligned_query)

#     def __eq__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score == other
#             return False
#         return self.score == other.score

#     def __lt__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score == other
#             return False
#         return self.score < other.score

#     def __le__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score == other
#             return False
#         return self.score <= other.score

#     def __gt__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score == other
#             return False
#         return self.score > other.score

#     def __ge__(self, other):
#         if not hasattr(other, "score"):
#             if type(other) in [int, float]:
#                 return self.score == other
#             return False
#         return self.score >= other.score

#     @property
#     def target_id(self):
#         return self._target_id

#     @target_id.setter
#     def target_id(self, target_id):
#         self._target_id = target_id

#     @property
#     def alignment_midline(self):
#         if self._alignment_midline is None:
#             self._alignment_midline = self._get_alignment_midline()
#         return self._alignment_midline

#     @staticmethod
#     def _process_sequence(sequence, aa):
#         if type(sequence) == Sequence:
#             return sequence
#         return Sequence(sequence)

#     def _get_alignment_midline(self):
#         midline = ""
#         for q, t in zip(self.aligned_query, self.aligned_target):
#             if q == t:
#                 midline += "|"
#             else:
#                 midline += " "
#         return midline


# class SSWAlignment(BaseAlignment):
#     """
#     Structure for performing and analyzing a Smith-Waterman local alignment.

#     .. note:

#         Exposed attributes and methods are the same as ``NWAlignment``, so
#         local and global alignmnts can be handled in the same way. In fact,
#         since comparisons are made based on score, local and global alignments
#         can be directly compared with constructions like::

#             local_aln == global_aln
#             local_aln > global_aln
#             alignments = sorted([global_aln, local_aln])

#     Attributes:

#         alignment_type (str): Is 'local' for all ``SSWAlignment`` objects.

#         aligned_query (str): The aligned query sequence (including gaps).

#         aligned_target (str): The aligned target sequence (including gaps).

#         alignment_midline (str): Midline for the aligned sequences, with ``|`` indicating
#           matches and a gap indicating mismatches::

#             print(aln.aligned_query)
#             print(aln.alignment_midline)
#             print(aln.aligned_target)

#             # ATGC
#             # || |
#             # ATCC

#         score (int): Alignment score.

#         query_begin (int): Position in the raw query sequence at which
#             the optimal alignment begins.

#         query_end (int): Position in the raw query sequence at which the
#             optimal alignment ends.

#         target_begin (int): Position in the raw target sequence at which
#             the optimal alignment begins.

#         target_end (int): Position in the raw target sequence at which the
#             optimal alignment ends.
#     """

#     def __init__(
#         self,
#         query,
#         target,
#         match=3,
#         mismatch=-2,
#         matrix=None,
#         gap_open=5,
#         gap_extend=2,
#         aa=False,
#     ):
#         super(SSWAlignment, self).__init__(
#             query, target, matrix, match, mismatch, gap_open, gap_extend, aa
#         )

#         self.alignment_type = "local"
#         self._alignment = self._align()
#         self._aligned_query = self._alignment.aligned_query_sequence
#         self._aligned_target = self._alignment.aligned_target_sequence
#         self._alignment_midline = None
#         self.score = self._alignment.optimal_alignment_score
#         self.cigar = self._alignment.cigar
#         self.query_begin = self._alignment.query_begin
#         self.query_end = self._alignment.query_end
#         self.target_begin = self._alignment.target_begin
#         self.target_end = self._alignment.target_end_optimal
#         self._alignment = None

#     @property
#     def alignment(self):
#         return self._alignment

#     @property
#     def aligned_query(self):
#         return self._aligned_query

#     @aligned_query.setter
#     def aligned_query(self, aligned_query):
#         self._aligned_query = aligned_query
#         self._alignment_midline = None

#     @property
#     def aligned_target(self):
#         return self._aligned_target

#     @aligned_target.setter
#     def aligned_target(self, aligned_target):
#         self._aligned_target = aligned_target
#         self._alignment_midline = None

#     def _align(self):
#         aligner = StripedSmithWaterman(
#             self.query.sequence,
#             match_score=self._match,
#             mismatch_score=self._mismatch,
#             gap_open_penalty=self._gap_open,
#             gap_extend_penalty=self._gap_extend,
#             substitution_matrix=self._matrix,
#             protein=self._aa,
#         )
#         return aligner(self.target.sequence)


# class BiopythonAlignment(BaseAlignment):
#     def __init__(
#         self,
#         query,
#         target,
#         match=3,
#         mismatch=-2,
#         matrix=None,
#         gap_open=5,
#         gap_extend=2,
#         aa=False,
#     ):
#         super(BiopythonAlignment, self).__init__(
#             query, target, matrix, match, mismatch, gap_open, gap_extend, aa
#         )

#         self.alignment_type = "local"
#         self._aln = self._align()
#         aln_query, aln_target, score, begin, end = self._aln
#         self._aligned_query = aln_query[begin:end]
#         self._aligned_target = aln_target[begin:end]
#         self._alignment_midline = None
#         self.score = score
#         self.query_begin = self._get_begin_pos(aln_query, begin)
#         self.query_end = self._get_end_pos(aln_query, end)
#         self.target_begin = self._get_begin_pos(aln_target, begin)
#         self.target_end = self._get_end_pos(aln_target, end)

#     @property
#     def alignment(self):
#         return self._aln

#     @property
#     def aligned_query(self):
#         return self._aligned_query

#     @aligned_query.setter
#     def aligned_query(self, aligned_query):
#         self._aligned_query = aligned_query
#         self._alignment_midline = None

#     @property
#     def aligned_target(self):
#         return self._aligned_target

#     @aligned_target.setter
#     def aligned_target(self, aligned_target):
#         self._aligned_target = aligned_target
#         self._alignment_midline = None

#     def _align(self):
#         aln = pairwise2.align.localms(
#             self.query.sequence,
#             self.target.sequence,
#             self._match,
#             self._mismatch,
#             self._gap_open,
#             self._gap_extend,
#         )
#         return aln[0]

#     def _get_begin_pos(self, seq, begin):
#         dashes = seq.count("-", 0, begin)
#         return begin - dashes

#     def _get_end_pos(self, seq, end):
#         return len(seq[:end].replace("-", ""))


# class NWAlignment(BaseAlignment):
#     """
#     Structure for performing and analyzing a Needleman-Wunch global alignment.

#     .. note:

#         Exposed attributes and methods are the same as ``SSWAlignment``, so
#         local and global alignmnts can be handled in the same way. In fact,
#         since comparisons are made based on score, local and global alignments
#         can be directly compared with constructions like::

#             local_aln == global_aln
#             local_aln > global_aln
#             alignments = sorted([global_aln, local_aln])

#     Attributes:

#         alignment_type (str): Is 'global' for all ``NWAlignment`` objects.

#         aligned_query (str): The aligned query sequence (including gaps).

#         aligned_target (str): The aligned target sequence (including gaps).

#         alignment_midline (str): Midline for the aligned sequences, with
#           ``|`` indicating matches and a gap indicating mismatches::

#             print(aln.aligned_query)
#             print(aln.alignment_midline)
#             print(aln.aligned_target)

#             # ATGC
#             # || |
#             # ATCC

#         score (int): Alignment score.

#         query_begin (int): Position in the raw query sequence at which
#             the optimal alignment begins.

#         query_end (int): Position in the raw query sequence at which the
#             optimal alignment ends.

#         target_begin (int): Position in the raw target sequence at which
#             the optimal alignment begins.

#         target_end (int): Position in the raw target sequence at which the
#             optimal alignment ends.
#     """

#     def __init__(
#         self,
#         query,
#         target,
#         match=3,
#         mismatch=-2,
#         gap_open=-5,
#         gap_extend=-2,
#         score_match=None,
#         score_mismatch=None,
#         score_gap_open=None,
#         score_gap_extend=None,
#         matrix=None,
#         aa=False,
#     ):
#         super(NWAlignment, self).__init__(
#             query, target, matrix, match, mismatch, gap_open, gap_extend, aa
#         )
#         self.alignment_type = "global"
#         self._score_match = int(score_match) if score_match is not None else None
#         self._score_mismatch = (
#             int(score_mismatch) if score_mismatch is not None else None
#         )
#         self._score_gap_open = (
#             int(score_gap_open) if score_gap_open is not None else None
#         )
#         self._score_gap_extend = (
#             int(score_gap_extend) if score_gap_extend is not None else None
#         )
#         self._matrix = matrix
#         self._alignment = self._align()
#         self._aligned_query = self._alignment[0]
#         self._aligned_target = self._alignment[1]
#         self._alignment_midline = None
#         self.score = self._score_alignment()

#     @property
#     def alignment(self):
#         return self._aln

#     @property
#     def aligned_query(self):
#         return self._aligned_query

#     @aligned_query.setter
#     def aligned_query(self, aligned_query):
#         self._aligned_query = aligned_query
#         self._alignment_midline = None

#     @property
#     def aligned_target(self):
#         return self._aligned_target

#     @aligned_target.setter
#     def aligned_target(self, aligned_target):
#         self._aligned_target = aligned_target
#         self._alignment_midline = None

#     def _get_matrix_file(self, match=None, mismatch=None, matrix=None):
#         matrix_dir = os.path.join(
#             os.path.dirname(os.path.abspath(__file__)), "matrices"
#         )
#         builtins = [os.path.basename(f) for f in list_files(matrix_dir)]
#         if matrix is None:
#             matrix_name = "match{}mismatch{}".format(abs(match), abs(mismatch))
#             if matrix_name not in builtins:
#                 self._build_matrix_from_params(
#                     match, mismatch, os.path.join(matrix_dir, matrix_name)
#                 )
#             return os.path.join(matrix_dir, matrix_name)
#         elif isinstance(matrix, dict):
#             dhash = hashlib.md5()
#             encoded = json.dumps(matrix, sort_keys=True).encode()
#             dhash.update(encoded)
#             matrix_name = dhash.hexdigest()
#             if matrix_name not in builtins:
#                 self._build_matrix_from_dict(
#                     matrix, os.path.join(matrix_dir, matrix_name)
#                 )
#             return os.path.join(matrix_dir, matrix_name)
#         else:
#             if self._matrix.lower() in builtin_names:
#                 return os.path.join(matrix_dir, self._matrix.lower())
#             else:
#                 err = "The supplied matrix name ({}) does not exist. ".format(matrix)
#                 err += "Built-in matrices are: {}".format("\n".join(builtins))
#                 raise RuntimeError(err)

#         # if matrix is not None:
#         #     matrix_name = matrix
#         # else:
#         #     matrix_name = 'match{}mismatch{}'.format(abs(match), abs(mismatch))
#         # if matrix_name.lower() in builtins:
#         #     return os.path.join(matrix_dir, matrix_name)
#         # builtin_names = [os.path.basename(f) for f in list_files(matrix_dir)]
#         # if self._matrix is not None:
#         #     if self._matrix.lower() in builtin_names:
#         #         return os.path.join(matrix_dir, self._matrix.lower())
#         #     else:
#         #         err = 'The supplied matrix name ({}) does not exist. '.format(matrix)
#         #         err += 'Built-in matrices are: {}'.format(', '.join(builtins))
#         #         raise RuntimeError(err)
#         # else:
#         #     self._build_matrix_from_params(match, mismatch, os.path.join(matrix_dir, matrix_name))
#         #     return os.path.join(matrix_dir, matrix_name)

#     def _align(self):
#         matrix = self._get_matrix_file(
#             match=self._match, mismatch=self._mismatch, matrix=self._matrix
#         )
#         aln = nw.global_align(
#             self.query.sequence,
#             self.target.sequence,
#             gap_open=self._gap_open,
#             gap_extend=self._gap_extend,
#             matrix=matrix,
#         )
#         return aln

#     def _score_alignment(self):
#         if all([self._score_match is not None, self._score_mismatch is not None]):
#             matrix = self._get_matrix_file(
#                 match=self._score_match, mismatch=self._score_mismatch
#             )
#         elif self._matrix is not None:
#             matrix = self._get_matrix_file(matrix=self._matrix)
#         else:
#             matrix = self._get_matrix_file(match=self._match, mismatch=self._mismatch)
#         gap_open = (
#             self._score_gap_open if self._score_gap_open is not None else self._gap_open
#         )
#         gap_extend = (
#             self._score_gap_extend
#             if self._score_gap_extend is not None
#             else self._gap_extend
#         )
#         aln = nw.score_alignment(
#             self.aligned_query,
#             self.aligned_target,
#             gap_open=gap_open,
#             gap_extend=gap_extend,
#             matrix=matrix,
#         )
#         return aln

#     @staticmethod
#     def _build_matrix_from_params(match, mismatch, matrix_file):
#         mstring = " {}".format(match) if len(str(match)) == 1 else str(match)
#         mmstring = " {}".format(mismatch) if len(str(mismatch)) == 1 else str(mismatch)
#         residues = [
#             "A",
#             "C",
#             "D",
#             "E",
#             "F",
#             "G",
#             "H",
#             "I",
#             "K",
#             "L",
#             "M",
#             "N",
#             "P",
#             "Q",
#             "R",
#             "S",
#             "T",
#             "V",
#             "W",
#             "Y",
#             "*",
#         ]
#         header = "   " + "  ".join(residues)
#         matlist = [
#             header,
#         ]
#         for r1 in residues:
#             resline = [
#                 r1,
#             ]
#             for r2 in residues:
#                 resline.append(mstring if r1 == r2 else mmstring)
#             matlist.append(" ".join(resline))
#         open(matrix_file, "w").write("\n".join(matlist))
#         return matrix_file

#     @staticmethod
#     def _build_matrix_from_dict(matrix, matrix_file):
#         residues = sorted(matrix.keys())
#         header = "   " + "  ".join(residues)
#         matlist = [
#             header,
#         ]
#         for r1 in residues:
#             resline = [
#                 r1,
#             ]
#             for r2 in residues:
#                 s = matrix[r1][r2]
#                 score = " {}".format(s) if len(str(s)) == 1 else str(s)
#                 resline.append(score)
#             matlist.append(" ".join(resline))
#         with open(matrix_file, "w") as f:
#             f.write("\n".join(matlist))
#         return matrix_file

#     @staticmethod
#     def _get_builtin_matrix(matrix_name):
#         matrix_dir = os.path.join(
#             os.path.dirname(os.path.abspath(__file__)), "matrices"
#         )
#         matrices = [os.path.basename(f) for f in list_files(matrix_dir)]
#         if matrix_name.lower() not in matrices:
#             err = "The maxtrix name you provided ({}) is not built-in.".format(
#                 matrix_name
#             )
#             err += "Built in matrices are: {}".format(", ".join(matrices))
#             raise RuntimeError()
#         return os.path.join(matrix_dir, matrix_name.lower())


# # -------------------------------------
# #
# #       VISUAL ALIGNMENT
# #
# # -------------------------------------


# def dot_alignment(
#     sequences,
#     seq_field=None,
#     name_field=None,
#     root=None,
#     root_name=None,
#     cluster_threshold=0.75,
#     as_fasta=False,
#     just_alignment=False,
# ):
#     """
#     Creates a dot alignment (dots indicate identity, mismatches are represented by the mismatched
#     residue) for a list of sequences.

#     Parameters
#     -----------
#     sequences : Iterable
#         A list of Sequence objects to be aligned.

#     seq_field : str
#         Name of the sequence field key. Default is ``vdj_nt``.

#     name_field : str
#         Name of the name field key. Default is ``seq_id``.

#     root : str, Sequence
#         The sequence used to 'root' the alignment. This sequence will be at the top of the alignment
#         and is the sequence against which dots (identity) will be evaluated. Can be provided either
#         as a string corresponding to the name of one of the sequences in ``sequences`` or as a
#         Sequence object. If not provided, ``sequences`` will be clustered at ``cluster_threshold``
#         and the centroid of the largest cluster will be used.

#     root_name : str
#         Name of the root sequence. If not provided, the existing name of the root sequence
#         (``name_field``) will be used. If ``root`` is not provided, the default ``root_name`` is
#         ``'centroid'``.

#     cluster_threshold : float
#         Threshold with which to cluster sequences if ``root`` is not provided. Default is ``0.75``.

#     as_fasta : bool
#         If ``True``, returns the dot alignment as a FASTA-formatted string, rather than a string
#         formatted for human readability

#     just_alignment : bool
#         If ``True``, returns just the dot-aligned sequences as a list.

#     Returns
#     --------
#     If ``just_alignment`` is ``True``, a list of dot-aligned sequences (without sequence names) will
#     be returned. If ``as_fasta`` is ``True``, a string containing the dot-aligned sequences in FASTA
#     format will be returned. Otherwise, a formatted string containing the aligned sequences (with
#     sequence names) will be returned.

#     """
#     import abstar

#     from .cluster import cluster

#     sequences = deepcopy(sequences)
#     root = copy(root)

#     # if custom seq_field is specified, copy to the .alignment_sequence attribute
#     if seq_field is not None:
#         if not all([seq_field in list(s.annotations.keys()) for s in sequences]):
#             print(
#                 "\nERROR: {} is not present in all of the supplied sequences.\n".format(
#                     seq_field
#                 )
#             )
#             sys.exit(1)
#         for s in sequences:
#             s.alignment_sequence = s[seq_field]
#     else:
#         for s in sequences:
#             s.alignment_sequence = s.sequence

#     # if custom name_field is specified, copy to the .id attribute
#     if name_field is not None:
#         if not all([name_field in list(s.annotations.keys()) for s in sequences]):
#             print(
#                 "\nERROR: {} is not present in all of the supplied sequences.\n".format(
#                     name_field
#                 )
#             )
#             sys.exit(1)
#         for s in sequences:
#             s.alignment_id = s[name_field]
#     else:
#         for s in sequences:
#             s.alignment_id = s.id

#     # parse the root sequence
#     if all([root is None, root_name is None]):
#         clusters = cluster(sequences, threshold=cluster_threshold, quiet=True)
#         clusters = sorted(clusters, key=lambda x: x.size, reverse=True)
#         centroid = clusters[0].centroid
#         root = abstar.run(("centroid", centroid.sequence))
#         root.alignment_id = "centroid"
#         root.alignment_sequence = root[seq_field]
#     elif type(root) in STR_TYPES:
#         root = [s for s in sequences if s.alignment_id == root][0]
#         if not root:
#             print(
#                 "\nERROR: The name of the root sequence ({}) was not found in the list of input sequences.".format(
#                     root
#                 )
#             )
#             print("\n")
#             sys.exit(1)
#         sequences = [s for s in sequences if s.alignment_id != root.alignment_id]
#     elif type(root) == Sequence:
#         if seq_field is not None:
#             if seq_field not in list(root.annotations.keys()):
#                 print(
#                     "\nERROR: {} is not present in the supplied root sequence.\n".format(
#                         seq_field
#                     )
#                 )
#                 sys.exit(1)
#             root.alignment_sequence = root[seq_field]
#         if name_field is not None:
#             if name_field not in list(root.annotations.keys()):
#                 print(
#                     "\nERROR: {} is not present in the supplied root sequence.\n".format(
#                         name_field
#                     )
#                 )
#                 sys.exit(1)
#             root.alignment_id = root[name_field]
#         sequences = [s for s in sequences if s.alignment_id != root.alignment_id]
#     else:
#         print(
#             "\nERROR: If root is provided, it must be the name of a sequence \
#               found in the supplied list of sequences or it must be a Sequence object."
#         )
#         print("\n")
#         sys.exit(1)

#     if root_name is not None:
#         root.alignment_id = root_name
#     else:
#         root_name = root.alignment_id

#     # compute and parse the alignment
#     seqs = [(root.alignment_id, root.alignment_sequence)]
#     seqs += [(s.alignment_id, s.alignment_sequence) for s in sequences]
#     aln = muscle(seqs)
#     g_aln = [a for a in aln if a.id == root_name][0]
#     dots = [
#         (root_name, str(g_aln.seq)),
#     ]
#     for seq in [a for a in aln if a.id != root_name]:
#         s_aln = ""
#         for g, q in zip(str(g_aln.seq), str(seq.seq)):
#             if g == q == "-":
#                 s_aln += "-"
#             elif g == q:
#                 s_aln += "."
#             else:
#                 s_aln += q
#         dots.append((seq.id, s_aln))
#     if just_alignment:
#         return [d[1] for d in dots]
#     name_len = max([len(d[0]) for d in dots]) + 2
#     dot_aln = []
#     for d in dots:
#         if as_fasta:
#             dot_aln.append(">{}\n{}".format(d[0], d[1]))
#         else:
#             spaces = name_len - len(d[0])
#             dot_aln.append(d[0] + " " * spaces + d[1])
#     return "\n".join(dot_aln)
