################################################################
#       DNA_construct: a higher level for construct compilation
#       Author: Andrey Shur
#       Latest update: 7/31/2020
#
################################################################


# Copyright (c) 2020, Build-A-Cell. All rights reserved.
# See LICENSE file in the project root directory for details.

import copy
from warnings import warn

from .component import Component
from .components_basic import DNA, RNA
from .dna_part import DNA_part
from .dna_part_cds import CDS
from .dna_part_misc import AttachmentSite
from .dna_part_promoter import Promoter
from .dna_part_rbs import RBS
from .dna_part_terminator import Terminator
from .species import (ComplexSpecies, OrderedMonomer, OrderedPolymer,
                      OrderedPolymerSpecies)
from .utils import all_comb, remove_bindloc, rev_dir

#integrase_sites = ["attB","attP","attL","attR","FLP","CRE"]


class Construct(Component,OrderedPolymer):
    def __init__(self,
                parts_list,
                name=None,
                circular=False,
                mechanisms=None,  # custom mechanisms
                parameters=None,  # customized parameters
                attributes=None,
                initial_conc=None, 
                copy_parts=True,
                **keywords):
        """this represents a bunch of parts in a row.
        A parts list has [[part,direction],[part,direction],...]
        Each part must be an OrderedMonomer"""
        #TODO get_part, dna_part search engine like get_component from mixture
        myparts = []
        for part in parts_list:
            newpart = []
            if(type(part)==list or type(part)==tuple):
                if(copy_parts):
                    newpart = [copy.deepcopy(part[0]).remove(),part[1]]
                else:
                    newpart = [part[0].remove(),part[1]]
            elif(isinstance(part,OrderedMonomer)):
                npartd = None
                npart = None
                if(part.direction is not None):
                    #remember the direction that is in the part
                    npartd = copy.deepcopy(part.direction)
                else:
                    #if no direction is specified, forward is default
                    npartd = "forward"
                if(copy_parts):
                    #we have to copy the part
                    npart = copy.deepcopy(part)
                else:
                    npart = part
                #forget everything about being part of a polymer
                npart.remove()
                #for the purposes of OrderedPolymer, you still need [part,direction]
                newpart = [npart,npartd]
            myparts += [newpart]
        OrderedPolymer.__init__(self,myparts)
        #self.parts_list = self._polymer
        self.circular=circular
        if(name is None):
            name = self.make_name() #automatic naming
        self.name = name
        Component.__init__(self=self,name=name,length = len(parts_list),
                    mechanisms=mechanisms,parameters=parameters,
                    attributes=attributes,initial_conc = initial_conc,
                     **keywords)
        self.update_parameters()
        self.transcripts = []
        if(not hasattr(self,"material_type")):
            self.material_type=None #set this when you inherit this class
        self.update_base_species(self.name)
        self.out_components = None
        self.predicted_rnas = None
        self.predicted_proteins = None
    @property
    def parts_list(self):
        return self._polymer
    def make_name(self):
        output = ""
        outlst = []
        for part in self.parts_list:
            pname = part.name
            if(part.direction=="reverse"):
                pname+="_r"
            outlst += [pname]
        output = '_'.join(outlst)
        if(self.circular):
            output+="_o"
        return output
    def get_part(self,part = None, part_type=None, name = None, index = None):
        """
        Function to get parts from Construct.parts_list.

        One of the 3 keywords must not be None.

        part: an instance of a DNA_part. Searches Construct.parts_list for a DNA_part with the same type and name.
        part_type: a class of DNA_part. For example, Promoter. Searches Construct.parts_list for a DNA_part with the same type.
        name: str. Searches Construct.parts_list for a DNA_part with the same name
        index: int. returns Construct.parts_list[index]

        if nothing is found, returns None.
        """

        if [part, name, index,part_type].count(None) != 3:
            raise ValueError(f"get_component requires a single keyword. Recieved component={part}, name={name}, index={index}.")
        if not (isinstance(part, DNA_part) or part is None):
            raise ValueError(f"component must be of type DNA_part. Recieved {part}.")
        if not (type(part_type) == type or part_type is None):
            raise ValueError(f"part_type must be a type. Recieved {part_type}.")
        if not (isinstance(name, str) or name is None):
            raise ValueError(f"name must be of type str. Recieved {name}.")
        if not (isinstance(index, int) or index is None):
            raise ValueError(f"index must be of type int. Recieved {index}.")

        matches = []
        if index is not None:
            matches.append(self.parts_list[index])
        else:
            for comp in self.parts_list:
                if part is not None:
                    if type(part) == type(comp) and comp.name == part.name:
                        matches.append(comp)
                elif name is not None:
                    if comp.name == name:
                        matches.append(comp)
                elif part_type is not None:
                    if(isinstance(comp,part_type)):
                        matches.append(comp)
        if len(matches) == 0:
            return None
        elif len(matches) == 1:
            return matches[0]
        else:
            warn("get_part found multiple matching components. A list has been returned.")
            return matches 


    def reverse(self):
        """reverses everything, without actually changing the DNA.
        also updates the name and stuff, since this is now a different Construct"""
        OrderedPolymer.reverse(self)
        self.reset_stored_data()
        self.name = self.make_name()
        self.update_base_species()
        return self
    def set_mixture(self, mixture):
        self.mixture = mixture
        for part in self.parts_list:
            part.set_mixture(mixture)
    def update_base_species(self, base_name=None, attributes = None):
        if base_name is None:
            self.base_species = self.set_species(self.name, material_type = self.material_type, attributes = attributes)
        else:
            self.base_species = self.set_species(base_name, material_type = self.material_type, attributes = attributes)
    def update_parameters(self, overwrite_parameters = True):
        """update parameters of all parts in the construct"""
        Component.update_parameters(self = self,parameter_database=self.parameter_database)
        for part in self.parts_list:
            part.update_parameters(parameter_database = self.parameter_database,
                                    overwrite_parameters = overwrite_parameters)

    def add_mechanism(self, mechanism, mech_type = None, overwrite = False, optional_mechanism = False):
        Component.add_mechanism(self, mechanism, mech_type = mech_type, \
                                overwrite = overwrite, optional_mechanism = optional_mechanism)
        for part in self.parts_list:
            part.add_mechanism( mechanism, mech_type = mech_type, \
                                overwrite = overwrite, optional_mechanism = optional_mechanism)
    def explore_txtl(self):
        """this function finds promoters and terminators and stuff in the construct"""
        proteins = {}
        rnas = {}
        for direction in ["forward","reverse"]:
            explorer = TxTl_Explorer()
            explorer.direction=direction
            if(direction == "reverse"):
                #if we go backwards then also list the parts backwards
                #deepcopy so we don't mangle the list
                newlist = copy.deepcopy(self.parts_list)[::-1]
                #TODO can we use the OrderedPolymer.reverse() function here?
            else:
                newlist = copy.deepcopy(self.parts_list)
            part_index = 0
            keep_going = 1
            second_looping = 0
            while keep_going: #we may have to loop through the parts twice because the construct is circular.
                                #this does not account for infinite loops, RCA, etc.
                part = newlist[part_index] #pull out the part
                keep_going = explorer.see(part) #the explorer keeps track of everything
                part_index+=1 #keep going+
                if(part_index==len(self.parts_list)):
                    part_index = 0 #this takes care of looping
                    if(self.circular and second_looping == 0):
                        #print('restart from beginning')
                        second_looping = 1 #during the second loop, we don't care about promoters
                        explorer.second_loop() #tell the explorer that we don't care about promoters
                    else:
                        explorer.end() #this completes all "in progress" RNAs and proteins
                        break
            #proteins.update(explorer.get_proteins())
            rnas.update(explorer.get_rnas())
        proteins = {}
        for promoter in rnas:
            _,prots = rnas[promoter].explore_txtl()
            proteins.update(prots)
        return rnas,proteins
    def __repr__(self):
        """this is just for display purposes"""
        return "DNA_construct = "+ self.make_name()
    def show(self):
        txt = self.name
        rnas,proteins = self.explore_txtl()
        if(len(rnas)>0):
            for promoter in rnas:
                txt += "\n\t"+repr(promoter)
                txt += "\n\t" + repr(rnas[promoter])
                if(rnas[promoter] in proteins):
                    for rbs in proteins[rnas[promoter]]:
                        txt += "\n\t"+repr(rbs)
                        for protein in proteins[rnas[promoter]][rbs]:
                            txt += "\n\tprotein = "+repr(protein)
        return txt
    def __contains__(self,obj2):
        """checks if this construct contains a certain part, or a copy of a certain part"""
        if(isinstance(obj2,DNA_part)):
            #if we got a DNA part it could mean one of two things:
            #1 we want to know if a dna part is anywhere
            #2 we want to know if a specific DNA part is in here
            #this is complicated by the fact that we want to have the same DNA part be reusable
            #in many locations
            if(obj2.parent==self):
                #the object should already know if it's a part of me
                return True
            elif(obj2.parent==None):
                #this object has been orphaned. 
                #that means we are looking for matching objects in any position
                new_obj2 = copy.deepcopy(obj2).unclone()
                uncloned_list = [a.unclone() for a in copy.deepcopy(self.parts_list)]
                return new_obj2 in uncloned_list
            else:
                return False
        elif(isinstance(obj2,str)):
            #if we get a string, that means we want to know if the name exists anywhere
            return obj2 in str(self)
    def get_species(self):
        ocomplx = []
        for part in self.parts_list:
            partspec = copy.copy(part.dna_species)
            partspec.material_type = self.material_type
            ocomplx += [partspec.set_dir(part.direction)]
        out_species = OrderedPolymerSpecies(ocomplx,base_species = self.base_species,circular = self.circular,\
                                                    name = self.name,material_type=self.material_type)
        
        return out_species
    
    def located_allcomb(self,spec_list):
        """recursively trace all paths through a list
        [[[part1,1],[part2,5]],[[part3,1]],[[part4,5],[part5,12]]]
        ====================>
        compacted_indexes = [1,5,12]
        prototype_list = [[part1,part3],[part2,part4],[part5]]
        comb_list = [[1],[5],[12],[1,5],[1,12],[5,12],[1,5,12]]
        ===========================
        then, take the lists from comb_list and create all possible lists
        out of prototype_list that includes those elements"""
        #first we have to construct the list we are tracing paths through
        spec_indexes = [a.position for a in spec_list] #extract all indexes
        compacted_indexes = sorted(list(set(spec_indexes)))
        prototype_list = [None]*len(compacted_indexes)
        for spec in spec_list:
            #go through every element and put it in the right place
            proto_ind = compacted_indexes.index(spec.position) #where to put it?
            if(prototype_list[proto_ind] is None):
                #if nothing's been placed here, then create a list
                prototype_list[proto_ind] = [spec]
            else:
                #if something is already here, then add to the list
                prototype_list[proto_ind]+= [spec]
        # at this point we have a list that looks like this:
        # [[[part1,0],[part2,0]],[[part2,3]],[[part3,12],[part5,12]]
        # next step is to pick one of the first list (either [part1,0] or [part2,0])
        # one of the second list (only [part2,3] is our option), one of the third list, etc
        # for all possible choices made this way
        comb_list = all_comb(compacted_indexes)
        def recursive_path(in_list):
            if(len(in_list)==1):
                out_list = []
                for a in in_list[0]:
                    out_list+= [[a]]
                return out_list
            elif(len(in_list)==0):
                return []
            else:
                out_list = []
                for a in in_list[0]:
                    out_list += [[a] + z for z in recursive_path(in_list[1:])]
                return out_list
        outlist = []
        for combo in comb_list:
            combo_sublists = []
            for combo_index in combo:
                combo_sublists += [prototype_list[compacted_indexes.index(combo_index)]]
            outlist+= recursive_path(combo_sublists)
        return outlist
    def make_polymers(self,species_lists,backbone):
        """makes polymers from lists of species
        inputs:
        species_lists: list of species which are to be assembled into a polymer
        backbone: the base_species which all these polymers should have"""
        polymers = []
        for combo in species_lists:
            #members of allcomb are now OrderedMonomers, which contain direction and position
            #there could be multiple OrderedPolymerSpecies we are making combinatorial.
            #for example, RNAs
            new_backbone = copy.deepcopy(backbone)
            new_material = backbone.material_type
            for spec in combo:
                new_material = OrderedPolymerSpecies.default_material
                new_backbone.replace(spec.position,spec)
            newname = None
            self_species = self.get_species()
            if(new_backbone==self_species):
                #if we have just re-created ourselves, then make sure to call it that
                newname = self_species.name
                polymers += [copy.deepcopy(self_species)]
            else:
                new_backbone.material_type = new_material
                polymers += [new_backbone] #we make a new OrderedComplexSpecies
        return polymers
    def update_combinatorial_complexes(self,active_components):
        """given an input list of components, we produce all complexes
        yielded by those components, mixed and matched to make all possible combinatorial
        complexes, where each component is assumed to only care about binding to one spot"""
        species = [self.get_species()]
        for part in active_components:
            #first we make binary complexes
            sp_list =  part.update_species()
            species+=remove_bindloc(sp_list)
        #print("initial species")
        #print(species)
        unique_complexes = {}
        possible_backbones = {self.base_species:self.get_species()}
        #possible_backbones = {a.name:a.get_species() for a in [self]+[a for a in proteins]}
        #species need to be uniqueified
        #print(species)
        unique_species = list(set(species)) 
        for specie in unique_species:
            #in this list we extract all the variants of the complexes from possible_backbones
            #that exist in our species list.
            if(isinstance(specie,OrderedPolymerSpecies) and specie.base_species in possible_backbones):
                #we only care about OrderedPolymerSpecies made from this construct
                if(specie.base_species in unique_complexes):
                    unique_complexes[specie.base_species] += [specie]
                else:
                    unique_complexes[specie.base_species] = [specie]
        #unique_complexes now has a list of all the non-combinatorial complexes we can make
        combinatorial_complexes = []
        for bb_name in unique_complexes:
            #for each backbone, make all combinatorial combinations.
            comp_binders = []
            for unit in unique_complexes[bb_name]:
                #for each complex for this backbone, find out what is bound at which location
                comp_bound = None
                pos_i = 0
                for pos in unit:
                    #find the position that has a ComplexSpecies in it
                    if(isinstance(pos,ComplexSpecies) and comp_bound is None):
                        comp_bound = copy.deepcopy(pos) #this is how we record the location
                    elif(isinstance(pos,ComplexSpecies)):
                        raise ValueError("complex exists in two locations!")
                        #in this case a mechanism has somehow yielded a DNA with items bound at two spots.
                        #this really shouldn't happen because each component can only bind to one spot.
                        #TODO perhaps the thing to do here is to assume that these two locations are
                        #always bound together? For example if integrase binds in two seperate locations?
                    pos_i+=1
                if(comp_bound is not None):
                    comp_binders += [comp_bound] #record what is bound, and at what position
            allcomb = self.located_allcomb(comp_binders) #all possible combinations of binders are made here
            allcomb += [[]] #unbound dna should also be used
            #now, all possibilities have been enumerated.
            #we construct the OrderedPolymerSpecies
            combinatorial_complexes += self.make_polymers(allcomb,possible_backbones[bb_name])
            
        return combinatorial_complexes
    def update_components(self,rnas=None,proteins=None):
        multivalent_self = self.get_species()
        self.update_parameters()
        if((rnas is None) or (proteins is None)):
            rnas = self.predicted_rnas
            proteins = self.predicted_proteins
            if(rnas is None or proteins is None):
                warn("{} is running explore_txtl possibly more than once".format(str(self)))
                rnas,proteins = self.explore_txtl()
        active_components = []
        for part in self.parts_list:
            if(hasattr(part,"update_component")):
                updated_components = part.update_component(multivalent_self[part.position],\
                                                                                rnas,proteins)
                if(updated_components is not None):
                    active_components += [updated_components]
        combinatorial_complexes = self.update_combinatorial_complexes(active_components)
        combinatorial_components = []
        for comb_specie in combinatorial_complexes:
            if(isinstance(comb_specie,OrderedPolymerSpecies) and comb_specie.base_species == self.base_species):
                for part in active_components:
                    part_pos = part.position
                    if(isinstance(comb_specie[part_pos],ComplexSpecies)):
                        #in this case the position of interest is already complexed. Skip!
                        pass
                    else:
                        combinatorial_components += [part.update_component(comb_specie[part_pos],\
                                                                                    rnas,proteins)]
        return combinatorial_components
    def __hash__(self):
        return hash(self.__repr__())
    def __eq__(self,construct2):
        """equality means comparing the parts list in a way that is not too deep"""
        if(self.__repr__()==construct2.__repr__()):
            return True
        else:
            return False
    
    def update_species(self):
        species = [self.get_species()]
        rnas = None
        proteins = None
        rnas,proteins = self.explore_txtl()
        self.predicted_rnas = rnas
        self.predicted_proteins = proteins
        #rnas:
        #this is a dictionary of the form:
        #{promoter:rna_construct,promoter2:rna_construct2}
        #proteins:
        #this is a dictionary of the form:
        #{rna_construct:{RBS:[Product1,Product2],RBS2:[Product3]}}
        out_components = self.update_components(rnas,proteins)
        #TODO save out_components
        for part in out_components:

            sp_list =  part.update_species()
            
            species+=remove_bindloc(sp_list)
        self.out_components = out_components
        for rna in proteins:
            if(not rna == self):
                #this part makes sure we don't do an infinite loop if we are in fact an RNA_construct
                species += rna.update_species()
        return species
    def reset_stored_data(self):
        self.out_components = None
        self.predicted_rnas = None
        self.predicted_proteins = None
    def changed(self):
        self.reset_stored_data()
        self.name = self.make_name()
    def update_reactions(self,norna=False):
        
        reactions = []
        rnas = None
        proteins = None
        rnas = self.predicted_rnas
        proteins = self.predicted_proteins
        if((rnas is None) or (proteins is None)):
            warn("{} is running explore_txtl possibly more than once".format(str(self)))
            rnas,proteins = self.explore_txtl()
        #rnas,proteins = self.explore_txtl()

        if(self.out_components is None):
            raise AttributeError("construct "+str(self) +" has no components! This happens because update_reactions has been run before update_species")
        for part in self.out_components:
            rx_list = []
            #_ = part.update_species()
            #update_components creates new components which are copies of the parts_list components
            #but the dna_to_bind attribute has been changed.
            #thus we need to make sure they have update_species() run on them before
            #doing update_reactions, in case any of these parts have memory
            reactions+= part.update_reactions()
        for rna in proteins:
            if(not rna == self):
                #this part makes sure we don't do an infinite loop if we are in fact an RNA_construct
                reactions += rna.update_reactions()
        return reactions

class DNA_construct(Construct,DNA):
    def __init__(self,
                parts_list,
                name=None,
                circular=False,
                mechanisms=None,  # custom mechanisms
                parameters=None,  # customized parameters
                attributes=None,
                initial_conc=None,
                copy_parts=True,
                **keywords):


        self.material_type = "dna"
        Construct.__init__(self=self, parts_list =parts_list, name = name, \
                            circular=circular, mechanisms=mechanisms, \
                            parameters=parameters, attributes=attributes, \
                            initial_conc=initial_conc, \
                            copy_parts=copy_parts, **keywords)


        pind = 0
        for part in self.parts_list:
            if(type(part.color)==type(None)):
                #if the color isn't set, let's set it now!
                part.color = pind
            if(type(part.color2)==type(None)):
                if(isinstance(part,AttachmentSite) and part.site_type in ["attL","attR"]):
                    #this is the only scenario in which we need a color2
                    #TODO make this more general
                    pind+=1
                    part.color2 = pind
            pind+=1
        
        


class TxTl_Explorer:
    def __init__(self,possible_rxns=("transcription","translation"),direction="forward"):
        """this class goes through a parts_list of a DNA_construct and decides what RNAs are made
        and what proteins are made based on orientation and location of parts"""
        self.current_rnas = {}
        self.current_proteins = {}
        self.made_rnas = {}
        self.all_rnas = {}
        self.made_proteins = {}
        self.current_rbs = None
        self.possible_rxns = possible_rxns
        self.direction=direction
        self.second_looping = False
    def see(self,part):
        """the explorer sees a part. This does different stuff depending on
        1) if there is a current rna or current rbs
        2) what the part is
        it returns whether:
        1: not done, keep going
        0: done, stop
        """
        effective_direction = part.direction
        if(self.direction=="reverse"):
            effective_direction = rev_dir(effective_direction)
        if(self.current_rnas!={}):
            #this means we are making an RNA
            terminated = 0
            #TODO how do we know that promoters transcribe and RBSes translate?
            #maybe you can transcribe or translate in both directions?
            if(isinstance(part,RBS) and "translation" in self.possible_rxns and \
                                                    effective_direction == "forward"):
                
                self.current_proteins[part]= []
                for promoter in self.current_rnas:
                    #this tells us that whatever RNAs are currently being made contain this RBS
                    if(part not in self.current_rnas[promoter][1]):
                        self.current_rnas[promoter][1] += [part]
                if(self.current_rbs == None):
                    #this means nobody is translating yet
                    self.current_rbs = part
                else:
                    #another RBS is translating. this could mean a few things:
                    #1: translational coupling. the previous translation ends and turns
                    #   into this one, with an added coupling term when that gets implemented
                    #2: it terminates the previous translation. Chances are it would, right?
                    self.current_rbs = part
                    # TODO add coupling
            elif(effective_direction in part.no_stop_codons and self.current_rbs != None):
                #this means we can translate through the current part!
                self.current_proteins[self.current_rbs] += [[part,effective_direction]]
            elif(self.current_rbs != None):
                #this means we CAN'T translate through the current part, but we are translating
                self.current_proteins[self.current_rbs] += [[part,effective_direction]]
                self.current_rbs = None #current RBS has done its work
            for promoter in list(self.current_rnas.keys()):
                self.current_rnas[promoter][0]+=[[part,effective_direction]]
                #we add the current part
                if(isinstance(part,Terminator) and effective_direction == "forward"):
                    terminated = 1 #a terminator ends everything. This does not account for leakiness
                    self.terminate_transcription(promoter)
            if(terminated):
                assert(self.current_rnas=={}) #this should be true if terminate_transcription() worked
                # this means that all RNAs have been stopped. But, since the proteins didn't know
                # which RNAs they belonged to, they are still hanging around so we have to
                # clean them up
                self.current_proteins = {}
                self.current_rbs = None
                terminated = 0
        if((effective_direction=="forward") and \
                (isinstance(part,Promoter) and "transcription" in self.possible_rxns)):
            #this if statement makes sure that the current part wants to transcribe, and
            #that is something that we are allowed to do
            self.current_rnas.update({part:[[],[]]})
            return 1
        elif((("transcription" not in self.possible_rxns) or self.second_looping) and \
                                         self.current_rnas=={}):
            return 0 #this means we are pretty sure there is nothing else left to do.
        else:
            return 1 #this means keep going! As far as we know there could be more to see
    def second_loop(self):
        """if we already went around the plasmid, then what we're checking for is continuing
        transcripts or proteins. We don't want to start making new transcripts
        because we already checked this area for promoters"""
        if(self.second_looping==False):
            new_rxns = []
            for rxn in self.possible_rxns:
                if(rxn!="transcription"):
                    #remove transcription from the set of possible reactions!
                    new_rxns += [rxn]
            self.possible_rxns = new_rxns
            self.second_looping=True
        else:
            self.end()
    def make_rna(self,promname="external"):
        """we are making an rna! imposed by external force"""
        if(promname in self.current_rnas):
            warn("RNA already started; it looks like this: "+str(self.current_rnas[promname]))
        else:
            self.current_rnas.update({promname:[[],[]]})
    def terminate_transcription(self,promoter=None):
        """this terminates the transcription of a specific RNA, started by the promoter
        given in the argument. the RNA is deleted from the list of currently transcribing RNAs,
        and any proteins that it was making are catalogued"""
        if(promoter==None and len(self.current_rnas)>0):
            #if you don't specify then it terminates the first one
            promoter = list(self.current_rnas.keys())[0]
        elif(len(self.current_rnas)==0):
            #in this case you terminated transcription when nothing was transcribing
            return
        rna_partslist = self.current_rnas[promoter][0]
        #print(self.current_rnas)
        #print(self.current_proteins)
        #TODO copy parts here and not in the constructor
        rna_construct = RNA_construct(copy.deepcopy(rna_partslist),made_by = promoter)
        rna_construct.set_mixture(promoter.mixture)
        #current_rna_name = str(promoter)+"-"+str(rna_construct)
        self.made_proteins[rna_construct]={}
        #compile the name, keeping track of which promoter made the rna and all the parts on it
        for rbs in self.current_rnas[promoter][1]:
            #ending the RNA also ends all the proteins being generated here.
            proteins_per_rbs = []
            # TODO this will run multiple times for each RNA. make it so it runs once!
            for protein_part in self.current_proteins[rbs]:
                if(protein_part[1] == "forward"):
                    #it is essential to be forwards
                    if(isinstance(protein_part[0],CDS) and protein_part[0].protein != None):
                        #this happens if we do in fact make a protein
                        proteins_per_rbs+=[protein_part[0]]
            #print(rna_partslist)
            #print("currently we are translating from "+str(rbs)+ " which is located at " + str(rbs.pos))
            #TODO correct_rbs is possibly not needed
            #print(rbs)
            #print(rna_partslist)
            correct_rbs = rna_construct.parts_list[rna_partslist.index([rbs,"forward"])]
            self.made_proteins[rna_construct].update({correct_rbs:proteins_per_rbs})

            # so this has to be done at the end of the RNA only because only then do we know
            # exactly what that full RNA is going to be.
            # we want to clear the current proteins, but there could be multiple RNAs that run over the same proteins,
            # so wait until we are done tallying all the RNAs before removing everything from current_proteins
        del self.current_rnas[promoter] # this removes the current RNA from the list, because it's
                                        # being terminated!!
        self.all_rnas.update({promoter:rna_construct})
        return
    def end(self):
        """we've reached the end of the dna! End everything!"""
        for promoter in list(self.current_rnas.keys()):
            self.terminate_transcription(promoter)
        self.current_rnas = {}
        self.current_proteins = {}
        self.current_rbs = None
    def get_proteins(self):
        """return all the proteins made during the latest exploration"""
        return self.made_proteins
    def get_rnas(self):
        """return all RNAs made during the latest exploration"""
        return self.all_rnas


class RNA_construct(Construct,RNA):
    def __init__(self,parts_list,name=None,made_by="unknown",**keywords):
        """an RNA_construct is a lot like a DNA_construct except it can only translate, and
        can only be linear"""
        self.material_type = "rna"
        self.my_promoter = made_by
        Construct.__init__(self=self,parts_list=parts_list,circular=False,name=name,**keywords)

    def explore_txtl(self):
        """an RNA has no tx, only TL! central dogma exists, right?"""
        # lets try to make this more modular shall we?
        explorer = TxTl_Explorer(possible_rxns = ("translation",))
        explorer.make_rna(self.my_promoter)
        part_index = 0
        keep_going = 1
        while keep_going:
            
            part = self.parts_list[part_index]
            keep_going = explorer.see(part)
            part_index+=1
            if(part_index==len(self.parts_list)):
                part_index = 0
                if(self.circular):
                    explorer.second_loop()
                else:
                    explorer.end()
                    break
        proteins = explorer.get_proteins()
        rnadict = {self.my_promoter:self}
        return rnadict, proteins
    #def get_species(self):
    #    outspec = Construct.get_species(self)
        #for spec in outspec:
        #    spec.material_type = "rna"
        #outspec.material_type="rna"
    #    return outspec
    def __repr__(self):
        """the name of an RNA should be different from DNA, right?"""
        output = "rna = "+self.name
        return output



#NOT IMPLEMENTED circular matching code below
#apparently below won't work because you can't hash the parts_list
    #this means the equality won't work for circular constructs. oops!
    '''
        if(len(self.parts_list) != len(construct2.parts_list)):
            return False
        else:
            if(self.circular == construct2.circular):
                if(self.circular == False):
                    for p1,p2 in zip(self.parts_list,construct2.parts_list):
                        if((p1.name != p2.name) or type(p1)!=type(p2) or p1.pos!=p2.pos):
                            #name and type are the crucial elements
                            #the pos should always match up. If it doesn't, then one of the two
                            #DNA_constructs is super wrong somehow...
                            
                            return False
                    #if you look through the whole list and don't find a mismatch then it's a match
                    return True
                elif(self.circular == True):
                    #in this case we need to rotate them until they align
                    #this opens up a can of worms though. How can we refer to positions of things
                    #if the sequence can be rotated???
                    poslist = [a for a in range(len(construct2.parts_list))]
                    for part_id in range(len(construct2.parts_list)):
                        #try every possible rotation of the thing we're comparing against
                        permuted_id_list = poslist[part_id:]+poslist[:part_id]
                        #we're just making a new list with the parts_list rotated
                        permuted_p2list = [construct2.parts_list[a] for a in permuted_id_list]
                        match = True #if you make it through the loop, then the match is true
                        for p1,p2 in zip(self.parts_list,permuted_p2list):
                            #compare each element to each other element
                            if((p1.name != p2.name) or type(p1)!=type(p2)):
                                match = False
                                #as soon as you find something wrong, quit
                                break
                        if(match==False):
                            #try the next permutation
                            continue
                        else:
                            #if we made it all the way here then it matches!
                            return True
                    #if we are here then we've gone through all permutations and hit "continue" every time.
                    #that means there is no orientation in which it matches
                    return False
    #'''
