# -*- coding: utf-8 -*-
"""Utility class for creating an hgvsp SequenceVariant object,
given a transcript with variants applied.
Used in hgvsc to hgvsp conversion.

"""

from __future__ import absolute_import, division, print_function, unicode_literals

import hgvs
from ..edit import (AAExt, AAFs, AARefAlt, AASub, Dup)
from ..exceptions import (HGVSError)
from ..location import (AAPosition, Interval)
from ..posedit import (PosEdit)
from six.moves import range

DBG = False


class AltSeqToHgvsp(object):
    def __init__(self, ref_data, alt_data):
        """Constructor

        :param ref_data: reference transcript record
        :type ref_data: recordtype
        :param alt_data: alt transcript record
        :type ref_data: recordtype
        """
        self._ref_data = ref_data
        self._alt_data = alt_data
        self._protein_accession = self._ref_data.protein_accession
        self._ref_seq = self._ref_data.aa_sequence
        self._alt_seq = self._alt_data.aa_sequence
        self._is_frameshift = self._alt_data.is_frameshift
        self._frameshift_start = self._alt_data.frameshift_start
        self._is_substitution = self._alt_data.is_substitution
        self._is_ambiguous = self._alt_data.is_ambiguous
        self._is_init_met = False

        if DBG:
            print("len ref seq:{} len alt seq:{}".format(len(self._ref_seq), len(self._alt_seq)))
            print("fs start:{} protein ac:{}".format(self._frameshift_start,
                                                     self._protein_accession))
            print(self._ref_seq)
            print(self._alt_seq)
            print("aa variant start: {}".format(self._alt_data.variant_start_aa))
            print(self._ref_data.transcript_sequence)
            print(self._alt_data.transcript_sequence)

    def build_hgvsp(self):
        """Compare two amino acid sequences; generate an hgvs tag from the output

        :return list of variants in sequence order
        :rtype list of dict
        """

        variants = []

        if not self._is_ambiguous and len(self._alt_seq) > 0:

            do_delins = True
            if self._ref_seq == self._alt_seq:
                # Silent p. variant
                start = self._alt_data.variant_start_aa
                if start - 1 < len(self._ref_seq):
                    deletion = self._ref_seq[start - 1]
                    insertion = deletion
                else:
                    start = ""
                    deletion = ""
                    insertion = ""
                    self._is_frameshift = False
                variants.append({"start": start, "ins": insertion, "del": deletion})
                do_delins = False
            elif self._is_substitution:
                if len(self._ref_seq) == len(self._alt_seq):
                    diff_pos = [(i, self._ref_seq[i], self._alt_seq[i])
                                for i in range(len(self._ref_seq))
                                if self._ref_seq[i] != self._alt_seq[i]]
                    if len(diff_pos) == 1:
                        (start, deletion, insertion) = diff_pos[0]
                        variants.append({"start": start + 1, "ins": insertion, "del": deletion})
                        do_delins = False

            if do_delins:
                if self._alt_data.is_frameshift:
                    start = self._alt_data.variant_start_aa - 1
                    while self._ref_seq[start] == self._alt_seq[start]:
                        start += 1
                    insertion = list(self._alt_seq[start:])
                    deletion = list(self._ref_seq[start:])
                    variants.append({"start": start + 1, "ins": insertion, "del": deletion})

                else:    # non-frameshifting delins or dup
                    # get size diff from diff in ref/alt lengths
                    start = self._alt_data.variant_start_aa - 1
                    delta = len(self._alt_seq) - len(self._ref_seq)
                    while self._ref_seq[start] == self._alt_seq[start]:
                        start += 1
                    offset = start + abs(delta)

                    if delta > 0:    # net insertion
                        insertion = list(self._alt_seq[start:offset])
                        deletion = []
                        ref_sub = self._ref_seq[start:]
                        alt_sub = self._alt_seq[offset:]
                    elif delta < 0:    # net deletion
                        insertion = []
                        deletion = list(self._ref_seq[start:offset])
                        ref_sub = self._ref_seq[offset:]
                        alt_sub = self._alt_seq[start:]
                    else:
                        insertion = []
                        deletion = []
                        ref_sub = self._ref_seq[start:]
                        alt_sub = self._alt_seq[start:]

                    # from start, get del/ins out to last difference
                    diff_indices = [i for i in range(len(ref_sub)) if ref_sub[i] != alt_sub[i]]
                    if not diff_indices and not deletion and insertion[0] == '*':
                        diff_indices.append(0)
                    if diff_indices:
                        max_diff = diff_indices[-1] + 1
                        insertion.extend(list(alt_sub[:max_diff]))
                        deletion.extend(list(ref_sub[:max_diff]))

                    variants.append({"start": start + 1, "ins": insertion, "del": deletion})

            if DBG:
                print(variants)

        if self._is_ambiguous:
            var_ps = [
                self._create_variant(
                    None,
                    None,
                    '',
                    '',
                    acc=self._protein_accession,
                    is_ambiguous=self._is_ambiguous)
            ]
        elif len(self._alt_seq) == 0:
            var_ps = [
                self._create_variant(
                    None,
                    None,
                    '',
                    '',
                    acc=self._protein_accession,
                    is_ambiguous=self._is_ambiguous,
                    is_no_protein=True)
            ]
        else:
            var_ps = [
                self._convert_to_sequence_variants(x, self._protein_accession) for x in variants
            ]

        if len(var_ps) > 1:
            raise HGVSError("Got multiple AA variants - not supported")
        return var_ps[0]

    #
    # internal methods
    #

    def _convert_to_sequence_variants(self, variant, acc):
        """Convert AA variant to an hgvs representation

        :param variant: contains start, del, and ins
        :type variant: dict
        :param acc: protein accession
        :type acc: str
        :return hgvs string
        :rtype str
        """
        start = variant['start']
        insertion = ''.join(variant['ins'])
        deletion = ''.join(variant['del'])

        # defaults
        is_dup = False    # assume not dup
        fsext_len = None    # fs or ext length
        is_sub = False
        is_ext = False

        if start == 1:    # initial methionine is modified
            aa_start = aa_end = AAPosition(base=start, aa=deletion)
            ref = ''
            alt = ''
            self._is_init_met = True
            self._is_ambiguous = True    # side-effect

        if insertion and insertion.find("*") == 0:    # stop codon at variant position
            aa_start = aa_end = AAPosition(base=start, aa=deletion[0])
            ref = ''
            alt = '*'
            is_sub = True

        elif start == len(self._ref_seq):    # extension
            if self._alt_seq[-1] == '*':
                fsext_len = len(insertion) - len(deletion)    # don't include the former stop codon
            else:
                fsext_len = '?'
            subst_at_stop_codon = insertion[0]

            aa_start = aa_end = AAPosition(base=start, aa='*')
            ref = ''
            alt = subst_at_stop_codon
            is_ext = True

        elif self._is_frameshift:    # frameshift
            aa_start = aa_end = AAPosition(base=start, aa=deletion[0])
            ref = ''

            try:
                fsext_len = str(
                    insertion.index("*") + 1)    # start w/ 1st change; ends w/ * (inclusive)
            except ValueError:
                fsext_len = "?"

            alt = insertion[0]

        else:    # no frameshift - sub/delins/dup
            if insertion == deletion:    # silent
                aa_start = aa_end = AAPosition(base=start, aa=deletion)
                ref = alt = ''

            elif len(insertion) == len(deletion) == 1:    # substitution
                aa_start = aa_end = AAPosition(base=start, aa=deletion)
                ref = ''
                alt = insertion
                is_sub = True

            elif len(deletion) > 0:    # delins OR deletion OR stop codon at variant position
                ref = deletion
                end = start + len(deletion) - 1
                if len(insertion) > 0:    # delins
                    aa_start = AAPosition(base=start, aa=deletion[0])
                    if end > start:
                        aa_end = AAPosition(base=end, aa=deletion[-1])
                    else:
                        aa_end = aa_start
                    alt = insertion

                else:    # deletion OR stop codon at variant position
                    if len(deletion) + start == len(
                            self._ref_seq):    # stop codon at variant position
                        aa_start = AAPosition(base=start, aa=deletion[0])
                        aa_end = AAPosition(base=start, aa=deletion[0])
                        ref = ''
                        alt = '*'
                        is_sub = True
                    else:    # deletion
                        aa_start = AAPosition(base=start, aa=deletion[0])
                        if end > start:
                            aa_end = AAPosition(base=end, aa=deletion[-1])
                        else:
                            aa_end = aa_start
                        alt = None

            elif len(deletion) == 0:    # insertion OR duplication OR extension

                is_dup, dup_start = self._check_if_ins_is_dup(start, insertion)

                if is_dup:    # duplication
                    dup_end = dup_start + len(insertion) - 1
                    aa_start = AAPosition(base=dup_start, aa=insertion[0])
                    aa_end = AAPosition(base=dup_end, aa=insertion[-1])
                    ref = alt = None

                else:    # insertion
                    start -= 1
                    end = start + 1

                    aa_start = AAPosition(base=start, aa=self._ref_seq[start - 1])
                    aa_end = AAPosition(base=end, aa=self._ref_seq[end - 1])
                    ref = None
                    alt = insertion

            else:    # should never get here
                raise ValueError("unexpected variant: {}".format(variant))

        var_p = self._create_variant(
            aa_start,
            aa_end,
            ref,
            alt,
            fsext_len=fsext_len,
            is_dup=is_dup,
            acc=acc,
            is_ambiguous=self._is_ambiguous,
            is_sub=is_sub,
            is_ext=is_ext,
            is_init_met=self._is_init_met
        )

        return var_p

    def _check_if_ins_is_dup(self, start, insertion):
        """Helper to identify an insertion as a duplicate

        :param start: 1-based insertion start
        :type start: int
        :param insertion: sequence
        :type insertion: str
        :return (is duplicate, variant start)
        :rtype (bool, int)
        """
        is_dup = False    # assume no
        variant_start = None

        dup_candidate_start = start - len(insertion) - 1
        dup_candidate = self._ref_seq[dup_candidate_start:dup_candidate_start + len(insertion)]
        if insertion == dup_candidate:
            is_dup = True
            variant_start = dup_candidate_start + 1

        return is_dup, variant_start

    def _create_variant(self,
                        start,
                        end,
                        ref,
                        alt,
                        fsext_len=None,
                        is_dup=False,
                        acc=None,
                        is_ambiguous=False,
                        is_sub=False,
                        is_ext=False,
                        is_no_protein=False,
                        is_init_met=False):
        """Creates a SequenceVariant object"""

        if is_init_met:
            posedit = AARefAlt(ref=ref, alt=alt, init_met=True)
        elif is_ambiguous:
            posedit = None
        else:
            interval = Interval(start=start, end=end)
            # Note - order matters
            if is_no_protein:
                edit = '0'
            elif is_sub:
                edit = AASub(ref=ref, alt=alt)
            elif is_ext:
                edit = AAExt(ref=ref, alt=alt, aaterm='*', length=fsext_len)
            elif self._is_frameshift:
                edit = AAFs(ref=ref, alt=alt, length=fsext_len)
            elif is_dup:
                edit = Dup()
            elif ref == alt == '':
                edit = AARefAlt(ref='', alt='')
            else:
                edit = AARefAlt(ref=ref, alt=alt)
            posedit = PosEdit(
                pos=interval,
                edit=edit,
                uncertain=hgvs.global_config.mapping.inferred_p_is_uncertain)
        var_p = hgvs.sequencevariant.SequenceVariant(acc, 'p', posedit)
        return var_p


# <LICENSE>
# Copyright 2018 HGVS Contributors (https://github.com/biocommons/hgvs)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# </LICENSE>
