#!/usr/bin/python

import sys
from Bio.Seq import Seq
from Bio import Alphabet

class LocatableSeqError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class LocatableSeq(Seq):
    def __init__(self, data, alphabet = Alphabet.generic_alphabet, start=0,
        external_seq_length=None, internal_ref=True, nd=None):
        '''A Seq object that can be located in an external coordinate system.
        
        It can be used to locate Seqs inside a MSA. The default behaviour
        of this LocatableSeq should be identical to the Seq behaviour.
        This sequence is not mutable, but it can be relocated and the nd 
        can be also changed.
        Arguments:
        data            - The sequence (str)
        alphabet        - A valid alphabet
        start           - Location of the first residue in the external
                        coordinate system [default 0]
        external_seq_length     - Length of the sequence that is used as the 
                                external reference [default None]
        internal_ref    - If True the Seq would interpred the indexes in its in
                        it's internal reference system, if false it will use
                        the external coordinates. [default True]
        nd              - The character to return when an index outside the
                        sequence is asked.
        '''
        Seq.__init__(self, data, alphabet)
        self.relocate(start, external_seq_length)
        self.internal_ref = True    #Public rw
        self.nd = nd                #Public rw

    def relocate(self, start, external_seq_length=None):
        self.start = start                  #Public ro
        self.end = start + len(self) - 1    #Public ro
        self.set_external_seq_length(external_seq_length)
    def set_external_seq_length(self, external_seq_length):
        self.external_seq_length = external_seq_length #Public ro
        if self.external_seq_length and \
            self.external_seq_length - len(self) - self.start < 0:
            raise LocatableSeqError('Sequence does not fit in the external reference')

    def __getitem__(self, index) :                 # Seq API requirement
        if isinstance(index, int) :
            #Return a single letter as a string
            if self.internal_ref:
                return self.data[index]
            else:
                i = index - self.start
                if i < 0 and self.external_seq_length is None:
                    raise LocatableSeqError('Negative indexes are not well defined, unless external seq length is set')
                #if the index is negative it should take into account the
                #length of the external sequence
                if i < 0:
                    i = i + self.external_seq_length 
                if i < 0 or i >= len(self):
                    return self.nd
                else:
                    return self.data[i]
        else:
            if self.internal_ref:
                #Return the (sub)sequence as another Seq object
                new_start = self.start + index.start
                return LocatableSeq(self.data[index], self.alphabet, new_start)
            else:
                i0 = index.start - self.start
                i1 = index.stop - self.start
                if index.step is not None:
                    #TODO the step dancing
                    raise LocatableSeqError('slice steps with external reference is not implemented yet')
                if index.start < 0 or index.stop < 0:
                    #TODO the step dancing
                    raise LocatableSeqError('slice steps with negateive indexes is not implemented yet')
                sl = len(self)
                new_start = index.start
                if i0 >= 0 and i0< sl and i1 >= 0 and i1< sl: 
                    return LocatableSeq(self.data[i0:i1], self.alphabet, \
                                                                    new_start)
                s = ''
                if i0 < 0:
                    num_nd_left = abs(i0)
                    i0 = 0
                    if self.nd is None:
                        s = ''
                        if i1 > 0:
                            new_start = self.start
                    else:
                        s = self.nd * num_nd_left
                        if i1 > 0:
                            new_start = self.start + i0 - 1 
                sr = ''
                if i1 >= sl:
                    num_nd_right = i1 - sl
                    if self.nd is not None:
                        sr = self.nd * num_nd_right
                s =  s + self.data[i0:i1] + sr
                return LocatableSeq(s, self.alphabet, new_start)               
    
    def __add__(self, other):
        #The default behaviour of this method could not be equal to
        #the behaviour of the __add__ in the Seq class
        #   012345
        #     ATGT seq1
        #   AT     seq2
        #   ATT    seq3
        #   ATATGT seq1 + seq2
        #   AT?TGT seq1 + seq3
        #TODO
        raise LocatableSeqError('__add_ is not implemented yet')
    def __radd__(self, other):
        #TODO
        raise LocatableSeqError('__radd_ is not implemented yet')

import unittest

class LocatableSeqTests(unittest.TestCase):
    def test_equivalent_to_seq(self):
        seq = LocatableSeq(data='ATGC')
        self.failUnless(seq[0] == 'A')
        self.failUnless(seq[-1] == 'C')
        s = seq[1:3]
        self.failUnless(isinstance(s, LocatableSeq))
        self.failUnless(s.start == 1)
        self.failUnless(s.tostring() == 'TG')
        s = seq[-2:-1]
        self.failUnless(s.start == 2)
        self.failUnless(s.tostring() == 'G')

    def test_external_reference(self):
        # 0  1  2  3  4  5  6  7  8
        #-9 -8 -7 -6 -5 -4 -3 -2 -1 
        #       0  1  2  3  
        #      -4 -3 -2 -1
        #       A  T  G  C
        seq = LocatableSeq(data='ATGC', start=2, external_seq_length=9)
        seq.internal_ref = False
        self.failUnless(seq[2] == 'A')
        self.failUnless(seq[-4] == 'C')
        self.failUnless(seq[-3] == None)
        self.failUnless(seq[1] == None)
        seq.nd = ' '
        self.failUnless(seq[0] == ' ')
        s = seq[2:4]
        self.failUnless(isinstance(s, LocatableSeq))
        self.failUnless(s.start == 2)
        self.failUnless(s.tostring() == 'AT')
        s = seq[1:4]
        self.failUnless(isinstance(s, LocatableSeq))
        self.failUnless(s.start == 1)
        self.failUnless(s.tostring() == ' AT')
        seq.nd = None
        s = seq[1:4]
        self.failUnless(s.start == 2)
        self.failUnless(s.tostring() == 'AT')
        s = seq[5:7]
        self.failUnless(s.start == 5)
        self.failUnless(s.tostring() == 'C')
        seq.nd = ' '
        s = seq[5:7]
        self.failUnless(s.start == 5)
        self.failUnless(s.tostring() == 'C ')
        s = seq[6:9]
        self.failUnless(s.start == 6)
        self.failUnless(s.tostring() == '   ')
        seq.nd = None
        s = seq[6:9]
        self.failUnless(s.start == 6)
        self.failUnless(s.tostring() == '')
        s = seq[0:2]
        self.failUnless(s.start == 0)
        self.failUnless(s.tostring() == '')
        seq.nd = ' '
        s = seq[6:9]
        self.failUnless(s.start == 6)
        self.failUnless(s.tostring() == '   ')
        s = seq[0:2]
        self.failUnless(s.start == 0)
        self.failUnless(s.tostring() == '  ')


    def test_exceptions(self):
        #negative index asked without setting external_seq_length
        seq = LocatableSeq(data='ATGC')
        try:
            seq.internal_ref = False
            seq[-1]
        except LocatableSeqError:
            pass
        else:
            self.fail("Expected a LocatableSeqError")
        #the sequence should fit inside the external sequence
        try:
            seq.relocate(2, 5)
        except LocatableSeqError:
            pass
        else:
            self.fail("Expected a LocatableSeqError")


def main():
    unittest.main()

if __name__ == "__main__":
    sys.exit(main())

