import re
import sys

import things
import annotation
import utils
from urllib.parse import unquote, quote

def build_column_9( id=None, parent=None, other=None ):
    ## either the id or parent must be defined
    if id is None and parent is None:
        raise Exception("ERROR: attempt to build a GFF3 9th column with no ID or Parent attributes")

    colstring = None

    ## ID should always be first, if there is one
    if id is not None:
        colstring = "ID={0}".format(id)

    if parent is not None:
        if colstring is not None:
            colstring += ";"

        colstring += "Parent={0}".format(parent)

    # other is a dict of key,value pairs
    if other is not None:
        for att in sorted(other):
            if other[att] is not None:
                if colstring is not None:
                    colstring += ";"

                colstring += "{0}={1}".format(att, escape(other[att]))

    return colstring


def build_column_9_from_dict( atts ):
    """
    Accepts a dict of key/value pairs to print in column 9 and joins them
    into a single string, taking care to print the ID first, then Parent, if present.
    """
    colstring = ''

    if 'ID' in atts:
        colstring = "ID={0}".format(atts['ID'])
        del atts['ID']

    if 'Parent' in atts:
        if len(colstring) > 0:
            colstring += ';'

        colstring += "Parent={0}".format(atts['Parent'])
        del atts['Parent']

    # now print the rest
    for att in atts:
        if len(colstring) > 0:
            colstring += ';'

        colstring += "{0}={1}".format(att, escape(atts[att]))

    return colstring


def order_column_9(colstring):
    c9 = column_9_dict(colstring)
    colstring = build_column_9_from_dict(c9)
    return colstring


def set_column_9_value(colstring, key, val):
    """
    Pass a column 9 string and you can either set or update an existing
    value for it.  The full column 9 string is returned.
    """
    c9 = column_9_dict(colstring)
    c9[key] = val
    return build_column_9_from_dict(c9)



def column_9_dict(colstring):
    '''
    This was borrowed from the URL below and then modified for Python 3
    ftp://ftp.informatics.jax.org/%2Fpub/mgigff/gff3.py
    '''
    SEMI	= ';'
    COMMA	= ','
    EQ	        = '='
    WSP_RE = re.compile(r'^\s*$')

    if colstring == ".":
        return {}
    c9 = {}
    for t in colstring.split(SEMI):
        if WSP_RE.match(t):
            continue
        tt = t.split(EQ)
        if len(tt) != 2:
            raise Exception("Bad column 9 format: {0}".format(colstring) )
        n = unquote(tt[0].strip())
        [*v] = map(unquote, tt[1].strip().split(COMMA))
        if len(v) == 1:
            v = v[0]
        c9[n] = v

    return c9


def column_9_value(value, key):
    '''
    Pass a case-sensitive key and this function will return the value,
    if present, else None.
    This was borrowed from the URL below and then modified for Python 3
    ftp://ftp.informatics.jax.org/%2Fpub/mgigff/gff3.py
    '''
    SEMI	= ';'
    COMMA	= ','
    EQ	        = '='
    WSP_RE = re.compile(r'^\s*$')

    if value == ".":
        return {}
    c9 = {}
    for t in value.split(SEMI):
        if WSP_RE.match(t):
            continue
        tt = t.split(EQ)
        if len(tt) != 2:
            raise Exception("Bad column 9 format: {0}".format(value) )
        n = unquote(tt[0].strip())
        [*v] = map(unquote, tt[1].strip().split(COMMA))
        if len(v) == 1:
            v = v[0]
        c9[n] = v

    if key in c9:
        # this is a list if there were comma-separated values
        return c9[key]
    else:
        return None


def escape(s):
    """
    ;  =>  %3B
    =  =>  %3D
    &  =>  %26
    ,  =>  %2C
    """
    # before the comma-based one can be used build_column_9 needs to be able to
    #  support list values for 'other' attributes
    #return ''.join(x if x not in ';=&,' else quote(x) for x in s)
    return ''.join(x if x not in ';=&' else quote(x) for x in s)


def unescape(s):
    """
    %3B  =>  ;
    %3D  =>  =
    %26  =>  &
    %2C  =>  ,
    """
    return unquote(s)


def add_annotation(features=None, polypeptide_set=None):
    for polypeptide in polypeptide_set.polypeptides:
        if polypeptide.id in features:
            features[polypeptide.id].annotation = polypeptide.annotation
        else:
            raise Exception("ERROR: Failed to add annotation from {0} because it couldn't be found in the passed input feature set (gff)".format(polypeptide.id))


def get_gff3_features(gff3_file, assemblies=None):
    '''
    Parses the passed GFF3 file and returns two dicts, loaded with biocode.biothings objects:
    1. The first dict are the Assembly objects, keyed on assembly ID.  Each Assembly has all of the
       children populated, so you can fully recover gene, RNA, exon, CDS, etc. features iterating on
       the assembly.
    2. The second dist is a flat structure of all the descendent feature objects of the Assemblies
       keyed by the feature IDs.
    See the documentation for each feature type in biocode.things for more info
    '''

    if assemblies is None:
        assemblies = dict()

    features = dict()

    # these are related to parsing any embedded FASTA
    in_fasta_section = False
    is_assembly_fasta = False
    current_fasta_id = None
    lnum = 0
    FASTA_RE = re.compile(r'^\#\#FASTA\s*$')

    for line in open(gff3_file):
        lnum = lnum + 1

        if in_fasta_section == True:
            m = re.search('>(\S+)\s*(.*)', line)
            if m:
                current_fasta_id = m.group(1)

                if current_fasta_id in assemblies:
                    is_assembly_fasta = True
                else:
                    is_assembly_fasta = False

            else:
                if current_fasta_id is None:
                    if (len(str(line.rstrip())) > 0):
                        raise Exception("FASTA parse error - sequence appears without preceding fasta id at line " + str(lnum))
                if is_assembly_fasta == True:
                    # must be a sequence line for an assembly
                    # python 2.6+ makes string concatenation amortized O(n)
                    #  http://stackoverflow.com/a/4435752/1368079
                    assemblies[current_fasta_id].residues += str(line.rstrip())
                    assemblies[current_fasta_id].length = len( assemblies[current_fasta_id].residues )

            continue

        elif FASTA_RE.match(line):
            # all data to the end of the file must be FASTA
            in_fasta_section = True
            continue

        # ignore all other comments
        if line.startswith('#'):
            continue

        cols = line.split("\t")

        if len(cols) != 9:
            continue

        mol_id = cols[0]

        # initialize this assembly if we haven't seen it yet
        if mol_id not in assemblies:
            assemblies[mol_id] = biocode.things.Assembly(id=mol_id, residues='')

        current_assembly = assemblies[mol_id]
        rfmin = int(cols[3]) - 1
        rfmax = int(cols[4])
        rstrand = None
        atts = column_9_dict(cols[8])
        feat_id = atts.get('ID')
        parent_id = atts.get('Parent')
        parent_feat = None

        # sanity check
        if rfmin > rfmax:
            raise Exception("ERROR: Coordinates in GFF for feature id {0} appear to be reversed and violate GFF3 specification: {1} > {2}".format(feat_id, cols[3], cols[4]))

        if 'locus_tag' in atts:
            locus_tag = atts['locus_tag']
        else:
            locus_tag = None

        # shared features are not yet supported
        if isinstance(parent_id, list):
            raise Exception("This line contains a shared feature with multiple parents.  This isn't yet supported:\n{0}".format(line))

        if parent_id is not None:
            if parent_id in features:
                parent_feat = features[parent_id]
            else:
                raise Exception("Error in GFF3: Parent {0} referenced by a child feature before it was defined".format(parent_id) )

        if cols[6] == '-':
            rstrand = -1
        elif cols[6] == '+':
            rstrand = 1
        else:
            rstrand = 0

        phase = cols[7]
        
        fmin_partial = False
        fmax_partial = False

        if 'Partial' in atts:
            if atts['Partial'] == '5prime,3prime':
                fmin_partial = True
                fmax_partial = True
            elif atts['Partial'] == '3prime':
                if rstrand == 1:
                    fmax_partial = True
                else:
                    fmin_partial = True
            elif atts['Partial'] == '5prime':
                if rstrand == 1:
                    fmin_partial = True
                else:
                    fmax_partial = True

        if cols[2] == 'gene':
            gene = biocode.things.Gene(id=feat_id, locus_tag=locus_tag)
            gene.locate_on(target=current_assembly, fmin=rfmin, fmin_partial=fmin_partial, fmax=rfmax, fmax_partial=fmax_partial, strand=rstrand)
            features[feat_id] = gene
            current_assembly.add_gene(gene)

        elif cols[2] == 'mRNA':
            mRNA = biocode.things.mRNA(id=feat_id, parent=parent_feat, locus_tag=locus_tag)
            mRNA.locate_on(target=current_assembly, fmin=rfmin, fmin_partial=fmin_partial, fmax=rfmax, fmax_partial=fmax_partial, strand=rstrand)
            parent_feat.add_mRNA(mRNA)
            features[feat_id] = mRNA

        elif cols[2] == 'rRNA':
            rRNA = biocode.things.rRNA(id=feat_id, parent=parent_feat, locus_tag=locus_tag)
            rRNA.locate_on(target=current_assembly, fmin=rfmin, fmin_partial=fmin_partial, fmax=rfmax, fmax_partial=fmax_partial, strand=rstrand)
            parent_feat.add_rRNA(rRNA)
            rRNA.annotation = parse_annotation_from_column_9(cols[8])
            features[feat_id] = rRNA

        elif cols[2] == 'tmRNA':
            tmRNA = biocode.things.tmRNA(id=feat_id, parent=parent_feat, locus_tag=locus_tag)
            tmRNA.locate_on(target=current_assembly, fmin=rfmin, fmin_partial=fmin_partial, fmax=rfmax, fmax_partial=fmax_partial, strand=rstrand)
            parent_feat.add_tmRNA(tmRNA)
            tmRNA.annotation = parse_annotation_from_column_9(cols[8])
            features[feat_id] = tmRNA

        elif cols[2] == 'tRNA':
            anticodon = None

            if 'anticodon' in atts:
                anticodon = atts['anticodon']
            
            tRNA = biocode.things.tRNA(id=feat_id, parent=parent_feat, locus_tag=locus_tag, anticodon=anticodon)
            tRNA.locate_on(target=current_assembly, fmin=rfmin, fmin_partial=fmin_partial, fmax=rfmax, fmax_partial=fmax_partial, strand=rstrand)
            parent_feat.add_tRNA(tRNA)
            tRNA.annotation = parse_annotation_from_column_9(cols[8])
            features[feat_id] = tRNA

        elif cols[2] == 'exon':
            exon = biocode.things.Exon(id=feat_id, parent=parent_feat)
            exon.locate_on(target=current_assembly, fmin=rfmin, fmin_partial=fmin_partial, fmax=rfmax, fmax_partial=fmax_partial, strand=rstrand)
            parent_feat.add_exon(exon)
            features[feat_id] = exon

        elif cols[2] == 'CDS':
            if phase == '.':
                phase = 0
            else:
                phase = int(phase)

            CDS = biocode.things.CDS(id=feat_id, parent=parent_feat, phase=phase)
            CDS.locate_on(target=current_assembly, fmin=rfmin, fmin_partial=fmin_partial, fmax=rfmax, fmax_partial=fmax_partial, strand=rstrand, phase=phase)
            parent_feat.add_CDS(CDS)
            features[feat_id] = CDS

        elif cols[2] == 'polypeptide':
            polypeptide = biocode.things.Polypeptide(id=feat_id, parent=parent_feat)
            polypeptide.locate_on(target=current_assembly, fmin=rfmin, fmin_partial=fmin_partial, fmax=rfmax, fmax_partial=fmax_partial, strand=rstrand)
            parent_feat.add_polypeptide(polypeptide)
            polypeptide.annotation = parse_annotation_from_column_9(cols[8])
            features[feat_id] = polypeptide

        elif cols[2] == 'region':
            # Regions where the ID value matches col[0] attach attributes to the molecule.  So far,
            #  we're only looking for a definition of circularity
            if 'Is_circular' in atts:
                if atts['Is_circular'].lower() == 'true':
                    current_assembly.is_circular = True
                else:
                    current_assembly.is_circular = False

        elif cols[2] == 'five_prime_UTR':
            utr = biocode.things.FivePrimeUTR(id=feat_id, parent=parent_feat)
            utr.locate_on(target=current_assembly, fmin=rfmin, fmax=rfmax, strand=rstrand)
            parent_feat.add_five_prime_UTR(utr)
            features[feat_id] = utr

        elif cols[2] == 'three_prime_UTR':
            utr = biocode.things.ThreePrimeUTR(id=feat_id, parent=parent_feat)
            utr.locate_on(target=current_assembly, fmin=rfmin, fmax=rfmax, strand=rstrand)
            parent_feat.add_three_prime_UTR(utr)
            features[feat_id] = utr

        else:
            sys.stderr.write( "Skipping feature {0} with type {1}\n".format(feat_id, cols[2]) )
            continue

        if cols[2] == 'region':
            current_assembly.length = rfmax - rfmin
        else:
            features[feat_id].length = rfmax - rfmin

    return (assemblies, features)


def parse_annotation_from_column_9(col9):
    annot = biocode.annotation.FunctionalAnnotation()
    atts = column_9_dict(col9)

    ## List of attributes which may be in column 9 that we want to skip as
    #   as not being involved with annotation.
    skip = ['ID', 'Parent']

    for att in atts:
        if att == 'product_name' or att == 'product':
            if isinstance(atts[att], str):
                annot.product_name = atts[att]
            else:
                annot.product_name = ', '.join(atts[att])
        elif att == 'Dbxref':
            ec_nums = list()

            ## TODO: Review this whole conditional
            if isinstance(atts['Dbxref'], str):
                if atts['Dbxref'].startswith("EC"):
                    ec_nums.append(atts['Dbxref'])
                else:
                    annot.add_dbxref(atts['Dbxref'])
            else:
                for dbxref in atts['Dbxref']:
                    if dbxref.startswith("EC"):
                        ec_nums.append(dbxref)
                    else:
                        annot.add_dbxref(dbxref)

            for ec_num in ec_nums:
                ec_annot = biocode.annotation.ECAnnotation(number=ec_num)
                annot.add_ec_number(ec_annot)
        elif att == 'Ontology_term':
            ont_terms = list()

            if isinstance(atts['Ontology_term'], str) and atts['Ontology_term'].startswith("GO"):
                ont_terms.append(atts['Ontology_term'])
            else:
                for term in atts['Ontology_term']:
                    if term.startswith("GO"):
                        ont_terms.append(term)

            for go_id in ont_terms:
                go_annot = biocode.annotation.GOAnnotation(go_id=go_id)
                annot.add_go_annotation(go_annot)
        elif att == 'gene_symbol':
            annot.gene_symbol = atts[att]
        elif att not in skip:
            ## just save any other attributes provided
            annot.other_attributes[att] = atts[att]

        
        
    return annot

def parse_gff3_by_relationship( gff3_file ):
    '''
    Parses a GFF3 file, caring only about the ID/Parent relationships and returning a dict where
    they keys are the feature IDs and the values are an dict of dicts.   The depth of this depends
    on the depth of parent/child relationships, but every node looks like this:
    $feat_id = { children: [], fmin: 30, cols: [] }
    Every feature must have either and ID or Parent defined, else an error will be thrown.
    The structure returned looks like this:
    {
    $molecule_id = {
                         fmin: 30,
                         cols: [...],
                         children: {
                             same as this structure
                         }
                   }, ...
    }
    WARNING:
    This will currently fail on any GFF that:
       - allows the same IDs on multiple lines, such as those for  discontiguous features (allowed in spec)
       - features with shared parents
    '''
    ## might try turning this into a defaultdict at a later point: http://ohuiginn.net/mt/2010/07/nested_dictionaries_in_python.html
    fgraph = dict()
    current_molecule_id = None
    ## key = id, value = parent_id
    parentage = dict()
    ## where features go during processing that have a parent which hasn't been seen yet
    purgatory = list()

    for line in open(gff3_file):
        cols = line.split("\t")

        if len(cols) == 9:
            cols[8] = cols[8].rstrip()
        else:
            continue

        mol_id = cols[0]
        id = column_9_value(cols[8], 'ID')
        # shared parents will cause failure here
        parent = column_9_value(cols[8], 'Parent')

        if id:
            parentage[id] = parent

        if mol_id != current_molecule_id:
            if current_molecule_id is not None:
                _reunite_children( fgraph, current_molecule_id, purgatory )

            ## reset for the next molecule
            purgatory = list()
            current_molecule_id = mol_id

            if current_molecule_id not in fgraph:
                fgraph[current_molecule_id] = dict()

        molecule = fgraph[current_molecule_id]

        if parent:
            uparent = _get_ultimate_parent( parentage, parent )
            molecule[uparent]['children'].append( {'id': id, 'cols': cols} )
        else:
            if id is None:
                raise Exception("ERROR: Encountered a line without a Parent or ID assigned: {0}".format(line))

            ## this shouldn't exist already
            if id in molecule:
                raise Exception("ERROR: found duplicate id ({0}) in input file".format(id) )

            molecule[id] = {'fmin': cols[3], 'children': list(), 'cols': cols}

    ## don't forget to handle the last one
    _reunite_children( fgraph, current_molecule_id, purgatory )

    return fgraph


def print_gff3_from_assemblies(assemblies=None, ofh=None, source=None):
    """
    Utility function to write a complete GFF3 file from a list() of biothings.Assembly objects
    """
    if type(ofh).__name__ != 'TextIOWrapper':
        ofh = sys.stdout #TextIOWrapper

    if source is None:
        source = '.'
    
    ofh.write("##gff-version 3\n")

    has_fasta = False

    for assembly_id in sorted(assemblies):
        current_assembly = assemblies[assembly_id]

        if current_assembly.length:
            has_fasta = True

            if current_assembly.is_circular == True:
                circ_str = ';Is_circular=true'
            elif current_assembly.is_circular == False:
                circ_str = ';Is_circular=false'
            else:
                circ_str = ''
                
            ofh.write("{0}\t{1}\tregion\t1\t{2}\t.\t+\t.\tID={0};Name={0}{3}\n".format(assembly_id, source, current_assembly.length, circ_str))
        
        for gene in sorted(assemblies[assembly_id].genes()):
            rnas_found = 0
            mRNAs = gene.mRNAs()
            
            for mRNA in sorted(mRNAs):
                mRNA_loc = mRNA.location_on(current_assembly)
                rnas_found += 1

                if rnas_found > 1:
                    gene.remove_mRNA(mRNA)
                    
                    print("INFO: splitting mRNA off gene {0}".format(gene.id))
                    new_gene = things.Gene(id="{0}_{1}".format(gene.id, rnas_found))
                    new_gene.locate_on(target=current_assembly, fmin=mRNA_loc.fmin, fmax=mRNA_loc.fmax, strand=mRNA_loc.strand)
                    new_gene.add_RNA(mRNA)
                    new_gene.print_as(fh=ofh, format='gff3', source=source)

            if len(mRNAs) > 1:
                gene_loc = gene.location_on(current_assembly)
                mRNA_loc = mRNAs[0].location_on(current_assembly)
                gene_loc.fmin = mRNA_loc.fmin
                gene_loc.fmax = mRNA_loc.fmax
                gene_loc.strand = mRNA_loc.strand

            gene.print_as(fh=ofh, format='gff3')
    
    # handle the fasta section
    if has_fasta:
        ofh.write("##FASTA\n")

        for assembly_id in sorted(assemblies):
            ofh.write(">{0}\n".format(assembly_id))
            ofh.write("{0}\n".format(biocode.utils.wrapped_fasta(assemblies[assembly_id].residues)))

    if type(ofh).__name__ == 'TextIOWrapper':
        ofh.close()


def print_biogene( gene=None, fh=None, source=None, on=None ):
    '''
    This method accepts a Gene object located on an Assembly object (from things.py) and prints
    the feature graph for that gene in GFF3 format, including the gene, mRNA, CDS and exon features.
    '''
    ## handle defaults
    if source is None:
        source = '.'

    if gene is None:
        raise Exception( "ERROR: The print_biogene() function requires a biogene to be passed via the 'gene' argument" );

    ## we can auto-detect the molecule if the user didn't pass one
    #   and if there's only one.
    if on is None:
        on = gene.location().on

    gene_loc = gene.location_on( on )
    strand = '0'

    if gene_loc.strand == 1:
        strand = '+'
    elif gene_loc.strand == -1:
        strand = '-'

    gene_annot_atts = dict()
    if gene.locus_tag is not None:
        gene_annot_atts['locus_tag'] = gene.locus_tag

    gene_partiality_string = _partiality_string(gene_loc)
    if gene_partiality_string is not None:
        gene_annot_atts['Partial'] = gene_partiality_string

    columns = ['.']*9
    columns[0:3] = [gene_loc.on.id, source, 'gene']
    columns[3:7] = [str(gene_loc.fmin + 1), str(gene_loc.fmax), '.', strand]
    columns[8] = build_column_9( id=gene.id, parent=None, other=gene_annot_atts )

    ## print the gene line
    fh.write( "\t".join(columns) + "\n" )

    ## modifications for the RNA
    for RNA in sorted(gene.RNAs()):
        RNA_loc = RNA.location_on( on )

        if RNA_loc is None:
            raise Exception("ERROR: Expected RNA {0} to be located on {1} but it wasn't".format(RNA.id, on.id))

        rna_annot_atts = dict()
        if RNA.locus_tag is not None:
            rna_annot_atts['locus_tag'] = RNA.locus_tag

        rna_partiality_string = _partiality_string(RNA_loc)
        if rna_partiality_string is not None:
            rna_annot_atts['Partial'] = rna_partiality_string

        # ncRNAs might have annotation elements at the RNA level
        if RNA.annotation:
            # tRNAs should have a product and anticodon
            if RNA.annotation.product_name:
                rna_annot_atts['product_name'] = RNA.annotation.product_name

        if hasattr(RNA, 'anticodon') and RNA.anticodon:
            rna_annot_atts['anticodon'] = RNA.anticodon

        columns[2] = RNA.__class__.__name__
        columns[3:5] = [str(RNA_loc.fmin + 1), str(RNA_loc.fmax)]
        columns[8] = build_column_9( id=RNA.id, parent=RNA.parent.id, other=rna_annot_atts )
        fh.write( "\t".join(columns) + "\n" )

        ## Write any 5' UTRs
        for utr in sorted(RNA.five_prime_UTRs()):
            utr_loc = utr.location_on( on )

            if utr_loc is None:
                raise Exception("ERROR: Expected UTR {0} to be located on {1} but it wasn't".format(utr.id, on.id) )

            columns[2] = 'five_prime_UTR'
            columns[3:5] = [str(utr_loc.fmin + 1), str(utr_loc.fmax)]
            columns[7] = str(utr_loc.phase)
            columns[8] = build_column_9( id=utr.id, parent=utr.parent.id, other=None )
            fh.write( "\t".join(columns) + "\n" )

        ## handle each CDS for this mRNA
        for CDS in sorted(RNA.CDSs()):
            CDS_loc = CDS.location_on( on )

            if CDS_loc is None:
                raise Exception("ERROR: Expected CDS {0} to be located on {1} but it wasn't".format(CDS.id, on.id) )

            cds_partiality_string = _partiality_string(CDS_loc)
            cds_annot_atts = dict()
            if cds_partiality_string is not None:
                cds_annot_atts['Partial'] = cds_partiality_string

            columns[2] = 'CDS'
            columns[3:5] = [str(CDS_loc.fmin + 1), str(CDS_loc.fmax)]
            columns[7] = str(CDS_loc.phase)
            columns[8] = build_column_9( id=CDS.id, parent=RNA.id, other=cds_annot_atts )
            fh.write( "\t".join(columns) + "\n" )

        columns[7] = '.'

        ## handle each exon for this RNA
        for exon in sorted(RNA.exons()):
            exon_loc = exon.location_on( on )

            if exon_loc is None:
                raise Exception("ERROR: Expected exon {0} to be located on {1} but it wasn't".format(exon.id, on.id))

            exon_partiality_string = _partiality_string(exon_loc)
            exon_annot_atts = dict()
            if exon_partiality_string is not None:
                exon_annot_atts['Partial'] = exon_partiality_string

            columns[2] = 'exon'
            columns[3:5] = [str(exon_loc.fmin + 1), str(exon_loc.fmax)]
            columns[8] = build_column_9( id=exon.id, parent=RNA.id, other=exon_annot_atts )
            fh.write( "\t".join(columns) + "\n" )

        # are there polypeptides?
        for polypeptide in sorted(RNA.polypeptides()):
            if len(polypeptide.locations) == 0:
                polypeptide_loc = RNA_loc
            else:
                polypeptide_loc = polypeptide.location_on(on)

            annot = polypeptide.annotation
            assertions = dict()
            dbxref_strs = list()
            ontology_strs = list()

            if annot is not None:
                if annot.product_name is not None:
                    assertions['product_name'] = annot.product_name

                if annot.gene_symbol is not None:
                    assertions['gene_symbol'] = annot.gene_symbol

                # should this construction be in build_column_9() instead?
                # this is treating each EC number as a string, but each should be an ECAnnotation object
                for ec in annot.ec_numbers:
                    dbxref_strs.append("EC:{0}".format(ec.number))

                for go in annot.go_annotations:
                    ontology_strs.append("GO:{0}".format(go.go_id))

                for dbxref in annot.dbxrefs:
                    dbxref_strs.append("{0}:{1}".format(dbxref.db, dbxref.identifier))

                # are there other attributes to handle?
                for att in annot.other_attributes:
                    assertions[att] = annot.other_attributes[att]

            if len(dbxref_strs) > 0:
                assertions['Dbxref'] = ",".join(dbxref_strs)

            if len(ontology_strs) > 0:
                assertions['Ontology_term'] = ",".join(ontology_strs)

            if 'Partial' in rna_annot_atts:
                assertions['Partial'] = rna_annot_atts['Partial']

            columns[2] = 'polypeptide'
            columns[3:5] = [str(polypeptide_loc.fmin + 1), str(polypeptide_loc.fmax)]
            columns[8] = build_column_9( id=polypeptide.id, parent=polypeptide.parent.id, other=assertions )
            fh.write( "\t".join(columns) + "\n" )

        ## Write any 5' UTRs
        for utr in sorted(RNA.three_prime_UTRs()):
            utr_loc = utr.location_on( on )

            if utr_loc is None:
                raise Exception("ERROR: Expected UTR {0} to be located on {1} but it wasn't".format(utr.id, on.id) )

            columns[2] = 'three_prime_UTR'
            columns[3:5] = [str(utr_loc.fmin + 1), str(utr_loc.fmax)]
            columns[7] = str(utr_loc.phase)
            columns[8] = build_column_9( id=utr.id, parent=utr.parent.id, other=None )
            fh.write( "\t".join(columns) + "\n" )


def print_biomatch( match=None, fh=None, source=None, on=None, mode=None ):
    '''
    This method accepts a Match object located on an Assembly object (from things.py) and prints
    the feature graph for that gene in GFF3 format.  How this is printed depends on the mode passed,
    but both modes suggested by the GFF3 specification are supported.
    mode=parts : Default.  In this mode, only MatchPart segments are printed but each shares the
    ID of the parent Match feature to indicate aligned segments.  The type for each row is the
    value of the parent Match.subclass.
    mode=match_and_parts : In this mode, both Match and MatchPart features are printed, with
    parent/child relationships explicitly defined and no shared IDs among any of the features.
    '''
    ## handle defaults
    if source is None:
        source = '.'

    if mode is None:
        mode = 'parts'

    ## we can auto-detect the molecule if the user didn't pass one and if there's only one.
    if on is None:
        on = match.location().on

    match_loc = match.location_on( on )
    strand = '0'

    if match_loc.strand == 1:
        strand = '+'
    elif match_loc.strand == -1:
        strand = '-'

    if mode == 'match_and_parts':
        columns = ['.']*9
        columns[0:3] = [match_loc.on.id, source, match.subclass]
        columns[3:7] = [str(match_loc.fmin + 1), str(match_loc.fmax), '.', strand]
        columns[8] = build_column_9( id=match.id, parent=None, other=None )
        fh.write( "\t".join(columns) + "\n" )

    for mp in match.parts:
        columns = ['.']*9
        mp_loc = mp.location_on(on)
        columns[3:7] = [str(mp_loc.fmin + 1), str(mp_loc.fmax), '.', strand]

        if mode == 'parts':
            columns[0:3] = [mp_loc.on.id, source, match.subclass]
            columns[8] = build_column_9( id=match.id, parent=None, other=None )
        elif mode == 'match_and_parts':
            columns[0:3] = [mp_loc.on.id, source, 'match_part']
            columns[8] = build_column_9( id=mp.id, parent=match.id, other=None )

        fh.write( "\t".join(columns) + "\n" )


def _reunite_children( fg, mol_id, kids ):
    sys.stderr.write("WARNING: biocodegff._reunite_children called but not yet implemented.  You are ahead of your time.\n")
    pass


def _get_ultimate_parent( p, id ):
    if p is None:
        return id

    oldest = id

    while p[oldest] is not None:
        oldest = p[oldest]

    return oldest

def _partiality_string(loc):
    if loc.strand == 1:
        if loc.fmax_partial == True:
            if loc.fmin_partial == True:
                return '5prime,3prime'
            else:
                return '3prime'
        elif loc.fmin_partial == True:
            return '5prime'

    elif loc.strand == -1:
        if loc.fmax_partial == True:
            if loc.fmin_partial == True:
                return '5prime,3prime'
            else:
                return '5prime'
        elif loc.fmin_partial == True:
            return '3prime'

    return None
