// the challenge with GTF is that there is no parent relationship
// every feature line has a gene_id and a transcript_id but there are no ids that uniquely id each feature
// in eukaryotes a gene can have multiple transcripts
// in prokaryotes a transcript can have multiple genes
// here we just create transcript features with children features and let 'gene_ids' simply be attributes not a feature in themselves

define([
           'dojo/_base/declare',
           'dojo/_base/array',
           'dojo/_base/lang',
           'dojo/json',
           'JBrowse/Util/GTF'
       ],
       function(
           declare,
           array,
           lang,
           JSON,
           GTF
       ) {


return declare( null, {

    constructor: function( args ) {
        lang.mixin( this, {
                        featureCallback:   args.featureCallback || function() {},
                        endCallback:       args.endCallback || function() {},
                        commentCallback:   args.commentCallback || function() {},
                        errorCallback:     args.errorCallback || function(e) { console.error(e); },
                        directiveCallback: args.directiveCallback || function() {},

                        // features that we have to keep on hand for now because they
                        // might be referenced by something else
                        under_construction_top_level : [],
                        // index of the above by ID
                        under_construction_by_id : {},

                        completed_references: {},

                        // features that reference something we have not seen yet
                        // structured as:
                        // {  'some_id' : {
                        //     'Parent' : [ orphans that have a Parent attr referencing it ],
                        //     'Derives_from' : [ orphans that have a Derives_from attr referencing it ],
                        // }
                        under_construction_orphans : {},

                        // if this is true, the parser ignores the
                        // rest of the lines in the file.  currently
                        // set when the file switches over to FASTA
                        eof: false
                    });
    },

    addLine: function( line ) {
        var match;
        if( this.eof ) {
            // do nothing
        } else if( /^\s*[^#\s>]/.test(line) ) { //< feature line, most common case
            var f = GTF.parse_feature( line );
            this._buffer_feature( f );
        }
        // directive or comment
        else if(( match = /^\s*(\#+)(.*)/.exec( line ) )) {
            var hashsigns = match[1], contents = match[2];
            if( hashsigns.length == 3 ) { //< sync directive, all forward-references are resolved.
                this._return_all_under_construction_features();
            }
            else if( hashsigns.length == 2 ) {
                var directive = GTF.parse_directive( line );
                if( directive.directive == 'FASTA' ) {
                    this._return_all_under_construction_features();
                    this.eof = true;
                } else {
                    this._return_item( directive );
                }
            }
            else {
                contents = contents.replace(/\s*/,'');
                this._return_item({ comment: contents });
            }
        }
        else if( /^\s*$/.test( line ) ) {
            // blank line, do nothing
        }
        else if( /^\s*>/.test(line) ) {
            // implicit beginning of a FASTA section.  just stop
            // parsing, since we don't currently handle sequences
            this._return_all_under_construction_features();
            this.eof = true;
        }
        else { // it's a parse error
            line = line.replace( /\r?\n?$/g, '' );
            throw "GTF parse error.  Cannot parse '"+line+"'.";
        }
    },

    _return_item: function(i) {
        if( i[0] )
            this.featureCallback( i );
        else if( i.directive )
            this.directiveCallback( i );
        else if( i.comment )
            this.commentCallback( i );
    },

    finish: function() {
        this._return_all_under_construction_features();
        this.endCallback();
    },

    /**
     * return all under-construction features, called when we know
     * there will be no additional data to attach to them
     */
    _return_all_under_construction_features: function() {
        // since the under_construction_top_level buffer is likely to be
        // much larger than the item_buffer, we swap them and unshift the
        // existing buffer onto it to avoid a big copy.
        array.forEach( this.under_construction_top_level,
                       this._return_item,
                       this );

        this.under_construction_top_level = [];
        this.under_construction_by_id = {};
        this.completed_references = {};

        // if we have any orphans hanging around still, this is a
        // problem. die with a parse error
        for( var o in this.under_construction_orphans ) {
            for( var orphan in o ) {
                throw "parse error: orphans "+JSON.stringify( this.under_construction_orphans );
            }
        }
    },

    container_attributes: { Parent : 'child_features', Derives_from : 'derived_features' },
    line_number: 0,

    // do the right thing with a newly-parsed feature line
    _buffer_feature: function( feature_line ) {
        feature_line.child_features = [];
        feature_line.derived_features = [];

        // NOTE: a feature is an arrayref of one or more feature lines.
        this.line_number=this.line_number+1;
        var feature_number = this.line_number; // no such thing as unique ID in GTF. make one up.
        var is_transcript = (feature_line.type == 'transcript'); //trying to support the Cufflinks convention of adding a transcript line
        var ids     = is_transcript ? feature_line.attributes.transcript_id  || [] : [feature_number];
        var parents = is_transcript ? [] : feature_line.attributes.transcript_id || [];
        var derives = feature_line.attributes.Derives_from || [];

        if( !ids.length && !parents.length && !derives.length ) {
            // if it has no IDs and does not refer to anything, we can just
            // output it
            this._return_item([ feature_line ]);
            return;
        }
        array.forEach( parents, function( id ) {
            if(! ( this.under_construction_by_id[id] )) {
                this._buffer_feature(this._create_transcript(feature_line));
            }
        },this);

        var feature;
        array.forEach( ids, function( id ) {
            var existing;
            if(( existing = this.under_construction_by_id[id] )) {
                // another location of the same feature
                existing.push( feature_line );
                feature = existing;
            }
            else {
                // haven't seen it yet
                feature = [ feature_line ];
                if( ! parents.length && ! derives.length ) {
                    this.under_construction_top_level.push( feature );
                }
                this.under_construction_by_id[id] = feature;

                // see if we have anything buffered that refers to it
                this._resolve_references_to( feature, id );
            }
        },this);

        // try to resolve all its references
        this._resolve_references_from( feature || [ feature_line ], { Parent : parents, Derives_from : derives }, ids );
    },

    _create_transcript: function(feature){
        var result =JSON.parse(JSON.stringify(feature));
        result.type='transcript';
        //result.attributes={'transcript_id':result.attributes.transcript_id, 'gene_id':result.attributes.gene_id};
        return result;
    },

   //there are no unique ids so no chance for collision just use first elements
   _expand_feature: function(parent_feature, child_feature){
        parent_feature[0].start = Math.min(parent_feature[0].start, child_feature[0].start);
        parent_feature[0].end = Math.max(parent_feature[0].end, child_feature[0].end);
   },

    _resolve_references_to: function( feature, id ) {
        var references = this.under_construction_orphans[id];
        if( ! references )
            return;

        for( var attrname in references ) {
            var pname = this.container_attributes[attrname] || attrname.toLowerCase();
            array.forEach( feature, function( loc ) {
                loc[pname].push( references[attrname] );
                delete references[attrname];
            });
        }
    },
    _resolve_references_from: function( feature, references, ids ) {
        // go through our references
        //  if we have the feature under construction, put this feature in the right place
        //  otherwise, put this feature in the right slot in the orphans

        var pname;
        for( var attrname in references ) {
            array.forEach( references[attrname], function( to_id ) {
                var other_feature;
                if(( other_feature = this.under_construction_by_id[ to_id ] )) {
                    this._expand_feature(other_feature, feature);
                    if( ! pname )
                        pname = this.container_attributes[attrname] || attrname.toLowerCase();
                    if( ! array.some( ids, function(i) { return this.completed_references[i+','+attrname+','+to_id]++; },this) ) {
                        array.forEach( other_feature, function( loc ) {
                            loc[pname].push( feature );
                        });
                    }
                }
                else {
                    ( this.under_construction_orphans[to_id][attrname] = this.under_construction_orphans[to_id][attrname] || [] )
                        .push( feature );
                }
            },this);
        }
    }
});
});
