define( [
            'dojo/_base/declare',
            'dojo/_base/lang',
            'dojo/_base/array',
            'dojo/json',
            'dojo/aspect',
            'dojo/dom-construct',
            'dojo/dom-geometry',
            'dojo/dom-class',
            'dojo/dom-style',
            'dojo/query',
            'dojo/on',
            'dojo/when',
            'dijit/Destroyable',
            'JBrowse/View/InfoDialog',
            'dijit/Dialog',
            'dijit/Menu',
            'dijit/PopupMenuItem',
            'dijit/MenuItem',
            'dijit/CheckedMenuItem',
            'dijit/MenuSeparator',
            'dijit/RadioMenuItem',
            'JBrowse/Util',
            'JBrowse/Component',
            'JBrowse/FeatureFiltererMixin',
            'JBrowse/Errors',
            'JBrowse/Model/Location',
            'JBrowse/View/TrackConfigEditor',
            'JBrowse/View/ConfirmDialog',
            'JBrowse/View/Track/BlockBased/Block',
            'JBrowse/View/DetailsMixin'
        ],
        function( declare,
                  lang,
                  array,
                  JSON,
                  aspect,
                  domConstruct,
                  domGeom,
                  domClass,
                  domStyle,
                  query,
                  on,
                  when,
                  Destroyable,
                  InfoDialog,
                  Dialog,
                  dijitMenu,
                  dijitPopupMenuItem,
                  dijitMenuItem,
                  dijitCheckedMenuItem,
                  dijitMenuSeparator,
                  dijitRadioMenuItem,
                  Util,
                  Component,
                  FeatureFiltererMixin,
                  Errors,
                  Location,
                  TrackConfigEditor,
                  ConfirmDialog,
                  Block,
                  DetailsMixin
                ) {

// we get `own` and `destroy` from Destroyable, see dijit/Destroyable docs

return declare( [Component,DetailsMixin,FeatureFiltererMixin,Destroyable],
/**
 * @lends JBrowse.View.Track.BlockBased.prototype
 */
{
    /**
     * Base class for all JBrowse tracks.
     * @constructs
     */
    constructor: function( args ) {
        args = args || {};

        this.refSeq = args.refSeq;
        this.name = args.label || this.config.label;
        this.key = args.key || this.config.key || this.name;

        this._changedCallback = args.changeCallback || function(){};
        this.height = 0;
        this.shown = true;
        this.empty = false;
        this.browser = args.browser;

        this.setFeatureFilterParentComponent( this.browser.view );

        this.store = args.store;



        // retrieve any user-set style info
        lang.mixin( this.config.style, this.getUserStyles() );
    },

    // get/set persistent per-user style information for this track
    updateUserStyles: function( settings ) {
        // set in this object
        lang.mixin( this.config.style, settings );
        // set in the saved style
        var saved = JSON.parse( this.browser.cookie("track-style-" + this.name ) || '{}' );
        lang.mixin( saved, settings );
        this.browser.cookie( "track-style-" + this.name, saved );
        // redraw this track
        this.redraw();
    },
    getUserStyles: function() {
        return JSON.parse( this.browser.cookie("track-style-" + this.name ) || '{}' );
    },


    /**
     * Returns object holding the default configuration for this track
     * type.  Might want to override in subclasses.
     * @private
     */
    _defaultConfig: function() {
        return {
            maxFeatureSizeForUnderlyingRefSeq: 250000,
            subfeatureDetailLevel: 2
        };
    },

    heightUpdate: function(height, blockIndex) {

        if (!this.shown) {
            this.heightUpdateCallback(0);
            return;
        }

        if (blockIndex !== undefined)
            this.blockHeights[blockIndex] = height;

        this.height = Math.max( this.height, height );

        if ( ! this.inShowRange ) {
            this.heightUpdateCallback( Math.max( this.labelHeight, this.height ) );

            // reposition any height-overflow markers in our blocks
            query( '.height_overflow_message', this.div )
                .style( 'top', this.height - 16 + 'px' );
        }
    },

    setViewInfo: function( genomeView, heightUpdate, numBlocks,
                           trackDiv,
                           widthPct, widthPx, scale) {
        this.genomeView = genomeView;
        this.heightUpdateCallback = heightUpdate;
        this.div = trackDiv;
        this.widthPct = widthPct;
        this.widthPx = widthPx;

        this.leftBlank = document.createElement("div");
        this.leftBlank.className = "blank-block";
        this.rightBlank = document.createElement("div");
        this.rightBlank.className = "blank-block";
        this.div.appendChild(this.rightBlank);
        this.div.appendChild(this.leftBlank);

        this.sizeInit(numBlocks, widthPct);
        this.labelHTML = "";
        this.labelHeight = 0;

        if( this.config.pinned )
            this.setPinned( true );

        if( ! this.label ) {
            this.makeTrackLabel();
        }
        this.setLabel( this.key );
    },

    makeTrackLabel: function() {

        var params = {
                className: "track-label dojoDndHandle",
                id: "label_" + this.name,
                style: {
                    position: 'absolute'
                }
            };

        if (typeof this.browser.config.trackLabels !== 'undefined' && this.browser.config.trackLabels==='no-block') {
            params.style.top = "-30px";
        }

        var labelDiv = dojo.create(
            'div', params ,this.div);

        this.label = labelDiv;

        if ( ( this.config.style || {} ).trackLabelCss){
            labelDiv.style.cssText += ";" + this.config.style.trackLabelCss;
        }

        var closeButton = dojo.create('div',{
            className: 'track-close-button'
        },labelDiv);
        this.own( on( closeButton, 'click', dojo.hitch(this,function(evt){
                this.browser.view.suppressDoubleClick( 100 );
                this.browser.publish( '/jbrowse/v1/v/tracks/hide', [this.config]);
                evt.stopPropagation();
        })));

        var labelText = dojo.create('span', { className: 'track-label-text' }, labelDiv );
        var menuButton = dojo.create('div',{
            className: 'track-menu-button'
        },labelDiv);
        dojo.create('div', {}, menuButton ); // will be styled with an icon by CSS
        this.labelMenuButton = menuButton;

        // make the track menu with things like 'save as'
        this.makeTrackMenu();
    },

    hide: function() {
        if (this.shown) {
            this.div.style.display = "none";
            this.shown = false;
        }
    },

    show: function() {
        if (!this.shown) {
            this.div.style.display = "block";
            this.shown = true;
        }
    },

    initBlocks: function() {
        this.blocks = new Array(this.numBlocks);
        this.blockHeights = new Array(this.numBlocks);
        for (var i = 0; i < this.numBlocks; i++) this.blockHeights[i] = 0;
        this.firstAttached = null;
        this.lastAttached = null;
        this._adjustBlanks();
    },

    clear: function() {
        if (this.blocks) {
            for (var i = 0; i < this.numBlocks; i++)
                this._hideBlock(i);
        }
        this.initBlocks();
        this.makeTrackMenu();
    },

    setLabel: function(newHTML) {
        if (this.label === undefined || this.labelHTML == newHTML )
            return;

        this.labelHTML = newHTML;
        query('.track-label-text',this.label)
            .forEach(function(n){ n.innerHTML = newHTML; });
        this.labelHeight = this.label.offsetHeight;
    },

    /**
     * Stub.
     */
    transfer: function() {},

    /**
     *  Stub.
     */
    startZoom: function(destScale, destStart, destEnd) {},

    /**
     * Stub.
     */
    endZoom: function(destScale, destBlockBases) {
    },


    showRange: function(first, last, startBase, bpPerBlock, scale,
                        containerStart, containerEnd, finishCallback) {

        if( this.fatalError ) {
            this.showFatalError( this.fatalError );
            return;
        }

        if ( this.blocks === undefined || ! this.blocks.length )
            return;

        // this might make more sense in setViewInfo, but the label element
        // isn't in the DOM tree yet at that point
        if ((this.labelHeight == 0) && this.label)
            this.labelHeight = this.label.offsetHeight;

        this.inShowRange = true;
        this.height = this.labelHeight;

        var firstAttached = (null == this.firstAttached ? last + 1 : this.firstAttached);
        var lastAttached =  (null == this.lastAttached ? first - 1 : this.lastAttached);

        var i, leftBase;
        var maxHeight = 0;
        var blockShowingPromises = [];
        //fill left, including existing blocks (to get their heights)
        for (i = lastAttached; i >= first; i--) {
            leftBase = startBase + (bpPerBlock * (i - first));
            blockShowingPromises.push( new Promise((resolve,reject) => {
                this._showBlock(i, leftBase, leftBase + bpPerBlock, scale,
                    containerStart, containerEnd, resolve);
            }))
        }
        //fill right
        for (i = lastAttached + 1; i <= last; i++) {
            leftBase = startBase + (bpPerBlock * (i - first));
            blockShowingPromises.push( new Promise((resolve,reject) => {
                this._showBlock(i, leftBase, leftBase + bpPerBlock, scale,
                    containerStart, containerEnd, resolve);
            }))
        }
        // if we have a finishing callback, call it when we have finished all our _showBlock calls
        if( finishCallback )
            Promise.all(blockShowingPromises)
                .then(finishCallback, finishCallback)

        //detach left blocks
        var destBlock = this.blocks[first];
        for (i = firstAttached; i < first; i++) {
            this.transfer(this.blocks[i], destBlock, scale,
                          containerStart, containerEnd);
            this.cleanupBlock(this.blocks[i]);
            this._hideBlock(i);
        }
        //detach right blocks
        destBlock = this.blocks[last];
        for (i = lastAttached; i > last; i--) {
            this.transfer(this.blocks[i], destBlock, scale,
                          containerStart, containerEnd);
            this.cleanupBlock(this.blocks[i]);
            this._hideBlock(i);
        }

        this.firstAttached = first;
        this.lastAttached = last;
        this._adjustBlanks();
        this.inShowRange = false;

        this.heightUpdate(this.height);
        this.updateStaticElements( this.genomeView.getPosition() );
    },

    cleanupBlock: function( block ) {
        if( block )
            block.destroy();
    },

    /**
     * Called when this track object is destroyed.  Cleans up things
     * to avoid memory leaks.
     */
    destroy: function() {
        array.forEach( this.blocks || [], function( block ) {
            this.cleanupBlock( block );
        }, this);
        delete this.blocks;
        delete this.div;

        this.inherited( arguments );
    },

    _hideBlock: function(blockIndex) {
        if (this.blocks[blockIndex]) {
            this.div.removeChild( this.blocks[blockIndex].domNode );
            this.cleanupBlock( this.blocks[blockIndex] );
            this.blocks[blockIndex] = undefined;
            this.blockHeights[blockIndex] = 0;
        }
    },

    _adjustBlanks: function() {
        if ((this.firstAttached === null)
            || (this.lastAttached === null)) {
            this.leftBlank.style.left = "0px";
            this.leftBlank.style.width = "50%";
            this.rightBlank.style.left = "50%";
            this.rightBlank.style.width = "50%";
        } else {
            this.leftBlank.style.width = (this.firstAttached * this.widthPct) + "%";
            this.rightBlank.style.left = ((this.lastAttached + 1)
                                          * this.widthPct) + "%";
            this.rightBlank.style.width = ((this.numBlocks - this.lastAttached - 1)
                                           * this.widthPct) + "%";
        }
    },

    hideAll: function() {
        if (null == this.firstAttached) return;
        for (var i = this.firstAttached; i <= this.lastAttached; i++)
            this._hideBlock(i);


        this.firstAttached = null;
        this.lastAttached = null;
        this._adjustBlanks();
    },

    // hides all blocks that overlap the given region/location
    hideRegion: function( location ) {
        if (null == this.firstAttached) return;
        // hide all blocks that overlap the given region
        for (var i = this.firstAttached; i <= this.lastAttached; i++)
            if( this.blocks[i] && location.ref == this.refSeq.name && !(  this.blocks[i].leftBase > location.end || this.blocks[i].rightBase < location.start ) )
                this._hideBlock(i);

        this._adjustBlanks();
    },

    /**
     *   _changeCallback invoked here is passed in constructor,
     *         and typically is GenomeView.showVisibleBlocks()
     */
    changed: function() {
        this.hideAll();
        if( this._changedCallback )
            this._changedCallback();
    },

    _makeLoadingMessage: function() {
        var msgDiv = dojo.create(
            'div', {
                className: 'loading',
                innerHTML: '<div class="text">Loading</span>',
                title: 'Loading data...',
                style: { visibility: 'hidden' }
            });
        window.setTimeout(function() { msgDiv.style.visibility = 'visible'; }, 200);
        return msgDiv;
    },

    showFatalError: function( error ) {
        query( '.block', this.div )
            .concat( query( '.blank-block', this.div ) )
            .concat( query( '.error', this.div ) )
            .orphan();
        this.blocks = [];
        this.blockHeights = [];

        this.fatalErrorMessageElement = this._renderErrorMessage( error || this.fatalError, this.div );
        this.heightUpdate( domGeom.position( this.fatalErrorMessageElement ).h );
        this.updateStaticElements( this.genomeView.getPosition() );
    },

    // generic handler for all types of errors
    _handleError: function( error, viewArgs ) {
        var errorContext = dojo.mixin( {}, error );
        dojo.mixin( errorContext, viewArgs );

        var isObject = typeof error == 'object';

        if( isObject && error instanceof Errors.TimeOut && errorContext.block )
            this.fillBlockTimeout( errorContext.blockIndex, errorContext.block, error );
        else if( isObject && error instanceof Errors.DataOverflow ) {
            if( errorContext.block )
                this.fillTooManyFeaturesMessage( errorContext.blockIndex, errorContext.block, viewArgs.scale, error );
            else
                array.forEach( this.blocks, function( block, blockIndex ) {
                    if( block )
                        this.fillTooManyFeaturesMessage( blockIndex, block, viewArgs.scale, error );
                },this);
        }
        else {
            console.error( error.stack || ''+error, error );
            this.fatalError = error;
            this.showFatalError( error );
        }
    },


    fillBlockError: function( blockIndex, block, error ) {
        error = error || this.fatalError || this.error;

        domConstruct.empty( block.domNode );
        var msgDiv = this._renderErrorMessage( error, block.domNode );
        this.heightUpdate( dojo.position(msgDiv).h, blockIndex );
    },

    _renderErrorMessage: function( message, parent ) {
        return domConstruct.create(
            'div', {
                className: 'error',
                innerHTML: '<h2>Error</h2><div class="text">An error was encountered when displaying this track.</div>'
                    +( message ? '<div class="codecaption">Diagnostic message</div><code>'+message+'</code>' : '' ),
                title: 'An error occurred'
            }, parent );
    },

    fillTooManyFeaturesMessage: function( blockIndex, block, scale, error ) {
        var message = (error && error.message || 'Too much data to show').replace(/\.$/,'');

        this.fillMessage(
            blockIndex,
            block,
            message
                + (scale >= this.browser.view.maxPxPerBp ? '': '; zoom in to see detail')
                + '.'
        );
    },

    redraw: function() {
        this.clear();
        this.genomeView.showVisibleBlocks(true);
    },

    markBlockHeightOverflow: function( block ) {
        if( block.heightOverflowed )
            return;

        block.heightOverflowed  = true;
        domClass.add( block.domNode, 'height_overflow' );
        domConstruct.create( 'div', {
                                 className: 'height_overflow_message',
                                 innerHTML: 'Max height reached',
                                 style: {
                                     top: (this.height-16) + 'px',
                                     height: '16px'
                                 }
                             }, block.domNode );
    },

    _showBlock: function(blockIndex, startBase, endBase, scale,
                         containerStart, containerEnd, finishCallback) {
        if ( this.empty || this.fatalError ) {
            this.heightUpdate( this.labelHeight );
            if (finishCallback) finishCallback();
            return;
        }

        if (this.blocks[blockIndex]) {
            this.heightUpdate(this.blockHeights[blockIndex], blockIndex);
            if (finishCallback) finishCallback();
            return;
        }

        var block = new Block({
            startBase: startBase,
            endBase: endBase,
            scale: scale,
            node: {
                className: 'block',
                style: {
                    left:  (blockIndex * this.widthPct) + "%",
                    width: this.widthPct + "%"
                }
            }
        });
        this.blocks[blockIndex] = block;
        this.div.appendChild( block.domNode );

        var args = [blockIndex,
                    block,
                    this.blocks[blockIndex - 1],
                    this.blocks[blockIndex + 1],
                    startBase,
                    endBase,
                    scale,
                    this.widthPx,
                    containerStart,
                    containerEnd];

        if( this.fatalError ) {
            this.fillBlockError( blockIndex, block );
            if (finishCallback) finishCallback();
            return;
        }

        // loadMessage is an opaque mask div that we place over the
        // block until the fillBlock finishes
        var loadMessage = this._makeLoadingMessage();
        block.domNode.appendChild( loadMessage );

        var finish = function() {
            if( block && loadMessage.parentNode )
                block.domNode.removeChild( loadMessage );
            if( finishCallback )
                finishCallback()
        };

        var viewargs = {
                blockIndex: blockIndex,
                block:      block,
                leftBlock:  this.blocks[blockIndex - 1],
                rightBlock: this.blocks[blockIndex + 1],
                leftBase:   startBase,
                rightBase:  endBase,
                scale:      scale,
                stripeWidth:    this.widthPx,
                containerStart: containerStart,
                containerEnd:   containerEnd,
                finishCallback: finish
            };
        try {
            this.fillBlock( viewargs );
        } catch( e ) {
            this._handleError( e, viewargs );
            finish();
        }
    },

    moveBlocks: function(delta) {
        var newBlocks = new Array(this.numBlocks);
        var newHeights = new Array(this.numBlocks);
        var i;
        for (i = 0; i < this.numBlocks; i++)
            newHeights[i] = 0;

        var destBlock;
        if ((this.lastAttached + delta < 0)
            || (this.firstAttached + delta >= this.numBlocks)) {
            this.firstAttached = null;
            this.lastAttached = null;
        } else {
            this.firstAttached = Math.max(0, Math.min(this.numBlocks - 1,
                                                      this.firstAttached + delta));
            this.lastAttached = Math.max(0, Math.min(this.numBlocks - 1,
                                                     this.lastAttached + delta));
            if (delta < 0)
                destBlock = this.blocks[this.firstAttached - delta];
            else
                destBlock = this.blocks[this.lastAttached - delta];
        }

        for (i = 0; i < this.blocks.length; i++) {
            var newIndex = i + delta;
            if ((newIndex < 0) || (newIndex >= this.numBlocks)) {
                //We're not keeping this block around, so delete
                //the old one.
                if (destBlock && this.blocks[i])
                    this.transfer(this.blocks[i], destBlock);
                this._hideBlock(i);
            } else {
                //move block
                newBlocks[newIndex] = this.blocks[i];
                if (newBlocks[newIndex])
                    newBlocks[newIndex].domNode.style.left =
                    ((newIndex) * this.widthPct) + "%";

                newHeights[newIndex] = this.blockHeights[i];
            }
        }
        this.blocks = newBlocks;
        this.blockHeights = newHeights;
        this._adjustBlanks();
    },

    sizeInit: function(numBlocks, widthPct, blockDelta) {
        var i, oldLast;
        this.numBlocks = numBlocks;
        this.widthPct = widthPct;
        if (blockDelta) this.moveBlocks(-blockDelta);
        if (this.blocks && (this.blocks.length > 0)) {
            //if we're shrinking, clear out the end blocks
            var destBlock = this.blocks[numBlocks - 1];
            for (i = numBlocks; i < this.blocks.length; i++) {
                if (destBlock && this.blocks[i])
                    this.transfer(this.blocks[i], destBlock);
                this._hideBlock(i);
            }
            oldLast = this.blocks.length;
            this.blocks.length = numBlocks;
            this.blockHeights.length = numBlocks;
            //if we're expanding, set new blocks to be not there
            for (i = oldLast; i < numBlocks; i++) {
                this.blocks[i] = undefined;
                this.blockHeights[i] = 0;
            }
            this.lastAttached = Math.min(this.lastAttached, numBlocks - 1);
            if (this.firstAttached > this.lastAttached) {
                //not sure if this can happen
                this.firstAttached = null;
                this.lastAttached = null;
            }

            if( this.blocks.length != numBlocks )
                throw new Error(
                    "block number mismatch: should be "
                        + numBlocks + "; blocks.length: "
                        + this.blocks.length
                );

            for (i = 0; i < numBlocks; i++) {
                if (this.blocks[i]) {
                    //if (!this.blocks[i].style) console.log(this.blocks);
                    this.blocks[i].domNode.style.left = (i * widthPct) + "%";
                    this.blocks[i].domNode.style.width = widthPct + "%";
                }
            }
        } else {
            this.initBlocks();
        }

        this.makeTrackMenu();
    },

    fillMessage: function( blockIndex, block, message, class_ ) {
        domConstruct.empty( block.domNode );
        var msgDiv = dojo.create(
            'div', {
                className: class_ || 'message',
                innerHTML: message
            }, block.domNode );
        this.heightUpdate( domGeom.getMarginBox(msgDiv, domStyle.getComputedStyle(msgDiv)).h, blockIndex );
    },

    /**
     * Called by GenomeView when the view is scrolled: communicates the
     * new x, y, width, and height of the view.  This is needed by tracks
     * for positioning stationary things like axis labels.
     */
    updateStaticElements: function( /**Object*/ coords ) {
        this.window_info = dojo.mixin( this.window_info || {}, coords );
        if( this.fatalErrorMessageElement ) {
            this.fatalErrorMessageElement.style.width = this.window_info.width * 0.6 + 'px';
            if( 'x' in coords )
                this.fatalErrorMessageElement.style.left = coords.x+this.window_info.width * 0.2 +'px';
        }

        if( this.label && 'x' in coords )
            this.label.style.left = coords.x+'px';
    },

    /**
     * Render a dijit menu from a specification object.
     *
     * @param menuTemplate definition of the menu's structure
     * @param context {Object} optional object containing the context
     *   in which any click handlers defined in the menu should be
     *   invoked, containing thing like what feature is being operated
     *   upon, the track object that is involved, etc.
     * @param parent {dijit.Menu|...} parent menu, if this is a submenu
     */
    _renderContextMenu: function( /**Object*/ menuStructure, /** Object */ context, /** dijit.Menu */ parent ) {
        if ( !parent ) {
            parent = new dijitMenu();
            this.own( parent );
        }

        for ( var key in menuStructure ) {
            var spec = menuStructure [ key ];
            try {
                if ( spec.children ) {
                    var child = new dijitMenu();
                    parent.addChild( child );
                    parent.addChild( new dijitPopupMenuItem(
                                         {
                                             popup : child,
                                             label : spec.label
                                         }));
                    this._renderContextMenu( spec.children, context, child );
                }
                else {
                    var menuConf = dojo.clone( spec );
                    if( menuConf.action || menuConf.url || menuConf.href ) {
                        menuConf.onClick = this._makeClickHandler( spec, context );
                    }
                    // only draw other menu items if they do something when clicked.
                    // drawing menu items that do nothing when clicked
                    // would frustrate users.
                    if( menuConf.label && !menuConf.onClick )
                        menuConf.disabled = true;

                    // currently can only use preloaded types
                    var class_ = {
                        'dijit/MenuItem':        dijitMenuItem,
                        'dijit/CheckedMenuItem': dijitCheckedMenuItem,
                        'dijit/RadioMenuItem':   dijitRadioMenuItem,
                        'dijit/MenuSeparator':   dijitMenuSeparator
                    }[spec.type] || dijitMenuItem;

                    parent.addChild( new class_( menuConf ) );
                }
            } catch(e) {
                console.error('failed to render menu item '+key,e);
            }
        }
        return parent;
    },

    _makeClickHandler: function( inputSpec, context ) {
        var track  = this;

        if( typeof inputSpec == 'function' ) {
            inputSpec = { action: inputSpec };
        }
        else if( typeof inputSpec == 'undefined' ) {
            console.error("Undefined click specification, cannot make click handler");
            return function() {};
        }
        else if( inputSpec.action == 'defaultDialog' ) {
            inputSpec.action = 'contentDialog';
            inputSpec.content = dojo.hitch(this,'defaultFeatureDetail');
        }

        var handler = function ( evt ) {
            if( track.genomeView.dragging )
                return;

            var ctx = context || this;
            var spec = track._processMenuSpec( dojo.clone( inputSpec ), ctx );
            var url = spec.url || spec.href;
            spec.url = url;
            var style = dojo.clone( spec.style || {} );

            // try to understand the `action` setting
            spec.action = spec.action ||
                ( url          ? 'iframeDialog'  :
                  spec.content ? 'contentDialog' :
                                 false
                );
            spec.title = spec.title || spec.label;

            if( typeof spec.action == 'string' ) {
                // treat `action` case-insensitively
                spec.action = {
                    iframedialog:   'iframeDialog',
                    iframe:         'iframeDialog',
                    contentdialog:  'contentDialog',
                    content:        'contentDialog',
                    baredialog:     'bareDialog',
                    bare:           'bareDialog',
                    xhrdialog:      'xhrDialog',
                    xhr:            'xhrDialog',
                    newwindow:      'newWindow',
                    "_blank":       'newWindow',
                    thiswindow:     'navigateTo',
                    navigateto:     'navigateTo'
                }[(''+spec.action).toLowerCase()];

                if( spec.action == 'newWindow' )
                    window.open( url, '_blank' );
                else if( spec.action == 'navigateTo' )
                    window.location = url;
                else if( spec.action in { iframeDialog:1, contentDialog:1, xhrDialog:1, bareDialog: 1} )
                    track._openDialog( spec, evt, ctx );
            }
            else if( typeof spec.action == 'function' ) {
                spec.action.call( ctx, evt );
            }
            else {
                return;
            }
        };

        // if there is a label, set it on the handler so that it's
        // accessible for tooltips or whatever.
        if( inputSpec.label )
            handler.label = inputSpec.label;

        return handler;
    },

    /**
     * @returns {Object} DOM element containing a rendering of the
     *                   detailed metadata about this track
     */
    _trackDetailsContent: function( additional ) {
        var details = domConstruct.create('div', { className: 'detail' });
        var fmt = lang.hitch(this, 'renderDetailField', details );
        fmt( 'Name', this.key || this.name );
        var metadata = lang.clone( this.getMetadata() );
        delete metadata.key;
        delete metadata.label;
        if( typeof metadata.conf == 'object' )
            delete metadata.conf;
        if (this.browser && this.browser.config && this.browser.config.trackSelector && this.browser.config.trackSelector.renameFacets){
           var metadataCopy = {};
           for (var k in metadata){
              var key = this.browser.config.trackSelector.renameFacets[k] || k;
              metadataCopy[key] = metadata[k];
           }
           metadata = metadataCopy;
        }
        var md_keys = [];
        for( var k in metadata ){
            md_keys.push(k);
        }
        md_keys.sort(function (a, b) {
           return a.toLowerCase().localeCompare(b.toLowerCase());
        });
        for (var i = 0 ; i < md_keys.length ; i++ ){
           var k = md_keys[i];
           fmt(this.camelToTitleCase(k), metadata[k]);
        }
        for(var k in additional){
           fmt(k, additional[k]);
        }
        return details;
    },

    camelToTitleCase: function(str){
        if (str === str.toLowerCase()){
            return Util.ucFirst(str.replace(/_/g, " "));
        } else {
            return str;
        }
    },

    getMetadata: function() {
        return this.config.metadata || this.browser && this.browser.trackMetaDataStore && this.browser.trackMetaDataStore.getItem(this.name) || {};
    },

    setPinned: function( p ) {
        this.config.pinned = !!p;

        if( this.config.pinned )
            domClass.add( this.div, 'pinned' );
        else
            domClass.remove( this.div, 'pinned' );

        return this.config.pinned;
    },
    isPinned: function() {
        return !! this.config.pinned;
    },

    /**
     * @returns {Array} menu options for this track's menu (usually contains save as, etc)
     */
    _trackMenuOptions: function() {
        var that = this;
        return [
            { label: 'About this track',
              title: 'About track: '+(this.key||this.name),
              iconClass: 'jbrowseIconHelp',
              action: 'contentDialog',
              content: dojo.hitch(this,'_trackDetailsContent')
            },
            { label: 'Pin to top',
              type: 'dijit/CheckedMenuItem',
              title: "make this track always visible at the top of the view",
              checked: that.isPinned(),
              //iconClass: 'dijitIconDelete',
              onClick: function() {
                  that.browser.publish( '/jbrowse/v1/v/tracks/'+( this.checked ? 'pin' : 'unpin' ), [ that.name ] );
              }
            },
            { label: 'Edit config',
              title: "edit this track's configuration",
              iconClass: 'dijitIconConfigure',
              action: function() {
                  new TrackConfigEditor( that.config )
                      .show( function( result ) {
                          // replace this track's configuration
                          that.browser.publish( '/jbrowse/v1/v/tracks/replace', [result.conf] );
                      });
              }
            },
            { label: 'Delete track',
              title: "delete this track",
              iconClass: 'dijitIconDelete',
              action: function() {
                  new ConfirmDialog({ title: 'Delete track?', message: 'Really delete this track?' })
                     .show( function( confirmed ) {
                          if( confirmed )
                              that.browser.publish( '/jbrowse/v1/v/tracks/delete', [that.config] );
                      });
              }
            }
        ];
    },


    _processMenuSpec: function( spec, context ) {
        for( var x in spec ) {
            if( spec.hasOwnProperty(x) ) {
                if( typeof spec[x] == 'object' )
                    spec[x] = this._processMenuSpec( spec[x], context );
                else
                    spec[x] = this.template( context.feature, this._evalConf( context, spec[x], x ) );
            }
        }
        return spec;
    },

    /**
     * Get the value of a conf variable, evaluating it if it is a
     * function.  Note: does not template it, that is a separate step.
     *
     * @private
     */
    _evalConf: function( context, confVal, confKey ) {

        // list of conf vals that should not be run immediately on the
        // feature data if they are functions
        var dontRunImmediately = {
            action: 1,
            click: 1,
            content: 1
        };

        return typeof confVal == 'function' && !dontRunImmediately[confKey]
            ? confVal.apply( context, context.callbackArgs || [] )
            : confVal;
    },

    /**
     * Like getConf, but get a conf value that explicitly can vary
     * feature by feature.  Provides a uniform function signature for
     * user-defined callbacks.
     */
    getConfForFeature: function( path, feature ) {
        return this.getConf( path, [feature, path, null, null, this ] );
    },

    isFeatureHighlighted: function( feature, name ) {
        var highlight = this.browser.getHighlight();
        return highlight
            && ( highlight.objectName && highlight.objectName == name )
            && highlight.ref == this.refSeq.name
            && !( feature.get('start') > highlight.end || feature.get('end') < highlight.start );
    },

    _openDialog: function( spec, evt, context ) {
        context = context || {};
        var type = spec.action;
        type = type.replace(/Dialog/,'');
        var featureName = context.feature && (context.feature.get('name')||context.feature.get('id'));
        var dialogOpts = {
            "class": "popup-dialog popup-dialog-"+type,
            title: spec.title || spec.label || ( featureName ? featureName +' details' : "Details"),
            style: dojo.clone( spec.style || {} )
        };
        if( spec.dialog )
            declare.safeMixin( dialogOpts, spec.dialog );

        var dialog;

        function setContent( dialog, content ) {
            // content can be a promise or Deferred
            if( typeof content.then == 'function' )
                content.then( function( c ) { dialog.set( 'content', c ); } );
            // or maybe it's just a regular object
            else
                dialog.set( 'content', content );
        }

        // if dialog == xhr, open the link in a dialog
        // with the html from the URL just shoved in it
        if( type == 'xhr' || type == 'content' ) {
            if( type == 'xhr' )
                dialogOpts.href = spec.url;

            dialog = new InfoDialog( dialogOpts );
            context.dialog = dialog;

            if( type == 'content' )
                setContent( dialog, this._evalConf( context, spec.content, null ) );

            Util.removeAttribute( context, 'dialog' );
        }
        else if( type == 'bare' ) {
            dialog = new Dialog( dialogOpts );
            context.dialog = dialog;

            setContent( dialog, this._evalConf( context, spec.content, null ) );

            Util.removeAttribute( context, 'dialog' );
        }
        // open the link in a dialog with an iframe
        else if( type == 'iframe' ) {
            var iframeDims = function() {
                var d = domGeom.position( this.browser.container );
                return { h: Math.round(d.h * 0.8), w: Math.round( d.w * 0.8 ) };
            }.call(this);

            dialog = new Dialog( dialogOpts );

            var iframe = dojo.create(
                'iframe', {
                    tabindex: "0",
                    width: iframeDims.w,
                    height: iframeDims.h,
                    style: { border: 'none' },
                    src: spec.url
                });

            dialog.set( 'content', iframe );
            dojo.create( 'a', {
                             href: spec.url,
                             target: '_blank',
                             className: 'dialog-new-window',
                             title: 'open in new window',
                             onclick: dojo.hitch(dialog,'hide'),
                             innerHTML: spec.url
                         }, dialog.titleBar );
            var updateIframeSize = function() {
                // hitch a ride on the dialog box's
                // layout function, which is called on
                // initial display, and when the window
                // is resized, to keep the iframe
                // sized to fit exactly in it.
                var cDims = domGeom.position( dialog.containerNode );
                var width  = cDims.w;
                var height = cDims.h - domGeom.position(dialog.titleBar).h;
                iframe.width = width;
                iframe.height = height;
            };
            aspect.after( dialog, 'layout', updateIframeSize );
            aspect.after( dialog, 'show', updateIframeSize );
        }

        // destroy the dialog after it is hidden
        aspect.after( dialog, 'hide', function() {
                          setTimeout(function() {
                              dialog.destroyRecursive();
                          }, 500 );
        });

        // show the dialog
        dialog.show();
    },

    /**
     * Given a string with template callouts, interpolate them with
     * data from the given object.  For example, "{foo}" is replaced
     * with whatever is returned by obj.get('foo')
     */
    template: function( /** Object */ obj, /** String */ template ) {
        if( typeof template != 'string' || !obj )
            return template;

        var valid = true;
        if ( template ) {
            return template.replace(
                    /\{([^}]+)\}/g,
                    function(match, group) {
                        var val = obj ? obj.get( group.toLowerCase() ) : undefined;
                        if (val !== undefined)
                            return val;
                        else {
                            return '';
                        }
                    });
        }
        return undefined;
    },

    /**
     * Makes and installs the dropdown menu showing operations available for this track.
     * @private
     */
    makeTrackMenu: function() {
        var thisB = this;
        when( this._trackMenuOptions() )
            .then( function( options ) {
                if( options && options.length && thisB.label && thisB.labelMenuButton ) {

                    // remove our old track menu if we have one
                    if( thisB.trackMenu )
                        thisB.trackMenu.destroyRecursive();

                    // render and bind our track menu
                    var menu = thisB._renderContextMenu( options, { menuButton: thisB.labelMenuButton, track: thisB, browser: thisB.browser, refSeq: thisB.refSeq } );
                    menu.startup();
                    menu.set('leftClickToOpen', true );
                    menu.bindDomNode( thisB.labelMenuButton );
                    menu.set('leftClickToOpen',  false);
                    menu.bindDomNode( thisB.label );
                    thisB.trackMenu = menu;
                    thisB.own( thisB.trackMenu );
                }
              });
    },


    // display a rendering-timeout message
    fillBlockTimeout: function( blockIndex, block ) {
        domConstruct.empty( block.domNode );
        domClass.add( block.domNode, 'timed_out' );
        this.fillMessage( blockIndex, block,
                           'This region took too long'
                           + ' to display, possibly because'
                           + ' it contains too much data.'
                           + ' Try zooming in to show a smaller region.'
                         );
    },

    renderRegionBookmark: function( args, bookmarks, renderLabels ) {
        var thisB=this;
        if( bookmarks.then ) {
            bookmarks.then(
                function( books ) {
                    array.forEach( books.features, function( bookmark ) {
                        if( bookmark.ref != this.refSeq.name ) return;
                        var loc = new Location( bookmark.refseq+":"+bookmark.start+".."+bookmark.end );
                        this.renderRegionHighlight( args, loc, bookmark.color, renderLabels?bookmark.label:null, renderLabels?bookmark.rlabel:null );
                    }, thisB);
                },
                function(error) {
                    console.log("Couldn't get bookmarks");
                }
            );
        }
        else {
            array.forEach( bookmarks.features, function( bookmark ) {
                if( bookmark.ref != this.refSeq.name ) return;
                var loc = new Location( bookmark.refseq+":"+bookmark.start+".."+bookmark.end );
                this.renderRegionHighlight( args, loc, bookmark.color, renderLabels?bookmark.label:null, renderLabels?bookmark.rlabel:null );
            }, this);
        }
    },

    renderRegionHighlight: function( args, highlight, color, label, rlabel ) {
        // do nothing if the highlight does not overlap this region
        if( highlight.start > args.rightBase || highlight.end < args.leftBase )
            return;

        var block_span = args.rightBase - args.leftBase;

        var left = highlight.start;
        var right = highlight.end;

        // trim left and right to avoid making a huge element that can cause problems
        var trimLeft = args.leftBase - left;
        if( trimLeft > 0 ) {
            left += trimLeft;
        }
        var trimRight = right - args.rightBase;
        if( trimRight > 0 ) {
            right -= trimRight;
        }

        var width = (right-left)*100/block_span;
        left = (left - args.leftBase)*100/block_span;
        var highlight=domConstruct.create('div', {
                                className: (color?'global_highlight_mod':'global_highlight')
                                    + (trimLeft <= 0 ? ' left' : '')
                                    + (trimRight <= 0 ? ' right' : '' ),
                                style: {
                                    left: left+'%',
                                    width: width+'%',
                                    height: '100%',
                                    background: color
                                }
                            }, args.block.domNode );

        this.postRenderHighlight(highlight);

        if( label ) {
            /*
            //  vertical text, has bugs
            if( trimLeft <= 0 ) {
                domConstruct.create('div', { className:'verticaltext', style: { top: '50px', left: left+'%',transformOrigin: left+'%'+' top' }, innerHTML: label }, args.block.domNode);
            }
            if( trimRight <= 0 ) {
                domConstruct.create('div', { className:'verticaltext', style: { top: '50px', left: left+width+'%',transformOrigin: left+width+'%'+' top' }, innerHTML: rlabel }, args.block.domNode);
            }*/
            if( trimLeft <= 0 ) {
                var d1=domConstruct.create('div', { className:'horizontaltext', style: { background: 'white', zIndex: 1000, left: left+'%' }, innerHTML: label }, args.block.domNode);
            }
            if( trimRight <= 0 ) {
                var d2=domConstruct.create('div', { className:'horizontaltext', style: { background: 'white', zIndex: 1000, left: left+width+'%' }, innerHTML: rlabel }, args.block.domNode);
            }

            var textWidth = (d1.clientWidth + 1) + "px";
            d1.style.left='calc('+left+'% - '+textWidth+')';
        }
    },
    postRenderHighlight: function(node) {

    }

});
});

/*

Copyright (c) 2007-2009 The Evolutionary Software Foundation

Created by Mitchell Skinner <mitch_skinner@berkeley.edu>

This package and its accompanying libraries are free software; you can
redistribute it and/or modify it under the terms of the LGPL (either
version 2.1, or at your option, any later version) or the Artistic
License 2.0.  Refer to LICENSE for the full license text.

*/
