// -*- mode: javascript -*-

var Synergizer = ( function ( $ ) {

  var SERVER_URL = '/cgi/synergizer/serv';
  var TIMEOUT = 5000;
  var ZEROTH_OPT = '-- Pick one! --';
  var ANY = '__ANY__';
  var SELECT_COLUMN_WIDTH = 400;

  var D = document;
  var CE  = function ( tag )    { return D.createElement( tag ) };
  var CTN = function ( string ) { return D.createTextNode( string ) };
  var NBSP = '\u00A0';          // &nbsp;

  var $SEL = {};

  var CACHED = {};
  var SAVE = {};
  var ABORT = 0;
  var CALLS = 0;

  var SAMPLE_INPUTS;

  var THE_ARGS;
  var THE_RESULTS;
  var THE_TABLE;

  var LOG; LOG = function ( s ) {
    var log = LOG._log || ( LOG._log = document.getElementById( 'log' ) );
    log.innerHTML = log.innerHTML + s + '\n'
  };

  var $$; $$ = {
    start: function () {
      try {
        $$._add_plugins();      // installs extensions to the jQuery object ($)

        $$._init_selectors();
        var vals = $.values( $SEL );
        $$.check_submit();
        $$.check_reset();

        $( '.required' ).change( $$.check_submit );
        $( '.id-field' ).change( $$.check_submit );

        $( '.required' ).change( $$.check_reset );
        $( '#ids' ).change( $$.check_reset );

        $( '.required' ).change( $$.reset_results );

        $( '.modal' ).jqm( { closeClass: 'modal-close', overlay: 75 } );

        $( '#sample-inputs' ).click( $$.load_sample_inputs );
        $( '#submit_form' ).click( $$.submit );
        $( '#reset_form' ).click( $$.reset );
        $( '#one-column' ).click( $$.one_column );
        $( '#two-column' ).click( $$.two_column );

        $$.do_ext_stuff();

        $$.fetch_sample_inputs();
      }
      catch ( e ) {
        alert( e.message );
        debugger;
      }
    },

    load_sample_inputs: function() {
      var si = SAMPLE_INPUTS;
      var selections = SAMPLE_INPUTS[ 'selections' ];
      var id;
      var $sel;

      if ( $$.have_ids_file() ) {
        $$.warning( 'Only the user can (re)set the ' +
                    '"File containing IDs to translate" field' );
      }

      // the try/catch blocks below are necessary to get around an IE6
      // bug
      id = 'authorities';
      $sel = $SEL[ id ];
      $$.receive_opts( SAMPLE_INPUTS[ id ], $sel );
      $$.nix_zeroth( $sel );
      try {
        $( 'option:contains("' + selections[ id ] + '")', $sel )[ 0 ].selected = 1;
      }
      catch ( e ) {}

      id = 'species';
      $sel = $SEL[ id ];
      $$.receive_opts( SAMPLE_INPUTS[ id ], $sel );
      $$.nix_zeroth( $sel );
      try {
        $( 'option:contains("' + selections[ id ] + '")', $sel )[ 0 ].selected = 1;
      }
      catch ( e ) {}

      id = 'domains';
      $sel = $SEL[ id ];
      // Note a couple of special things here: $$.receive_domains
      // instead of $$.receive_opts, and the use of $$.preprocess_namespaces
      $$.receive_domains( $$.preprocess_namespaces( SAMPLE_INPUTS[ id ] ), $sel );
      $$.nix_zeroth( $sel );
      try {
        $( 'option:contains("' + selections[ id ] + '")', $sel )[ 0 ].selected = 1;
      }
      catch ( e ) {}

      id = 'ranges';
      $sel = $SEL[ id ];
      // Back to using plain ol' $$.receive_opts, but here we still
      // need to use $$.preprocess_namespaces
      $$.receive_opts( $$.preprocess_namespaces( SAMPLE_INPUTS[ id ] ), $sel );
      $$.nix_zeroth( $sel );
      try {
        $( 'option:contains("' + selections[ id ] + '")', $sel )[ 0 ].selected = 1;
      }
      catch ( e ) {}

      $( '#ids' )[ 0 ].value = SAMPLE_INPUTS[ 'ids' ].join( '\n' );

//       $( '#submit_form' )[ 0 ].disabled = false;
//       $( '#reset_form' )[ 0 ].disabled = false;

      $$.hide_results();

      $$.clear_message();
      $$.hide_message();

      return false;
    },

    want_spreadsheet: function () {
      return $( '#spreadsheet-output' )[ 0 ].checked;
    },

    one_column: function () {
//       $( '.domains-column' ).hide();
      $$.show_results( 1 );
      $( '#one-column' ).hide();
      $( '#two-column' ).show();
      return false;
    },

    two_column: function () {
      $$.show_results();
//       $( '.domains-column' ).show();
      $( '#two-column' ).hide();
      $( '#one-column' ).show();
      return false;
    },

    back: function () {
      $( '#results-panel' ).hide();
      $( '#inputs-panel' ).show();
    },

    submit: function () {
      if ( $$.results() ) {
        var inputs_form = $( '#inputs-panel' );
        var results = $( '#results' );
        inputs_form.css( 'display', 'none' );
        results.css( 'display', 'block' );
      }
      return false;
    },

    reset: function () {
      $$.reset_inputs();
      $$.reset_results();
      $( '#results-panel' ).hide();

      $$.clear_message();
      $$.hide_message();

//       $( '#reset_form' )[ 0 ].disabled = true;
//       $( '#submit_form' )[ 0 ].disabled = true;
      return false;
    },

    reset_inputs: function () {
      $( '#inputs' )[ 0 ].reset();

      for ( var k in $SEL ) $$.clear_opts( $SEL[ k ] );

      var r = SAMPLE_INPUTS[ 'authorities' ];
      if ( r !== undefined ) {
        var $sel = $SEL[ 'authorities' ];
        $$.receive_opts( r, $sel );

        $( 'option', $SEL[ 'authorities' ] )[ 0 ].selected = 1;
      }

      return
    },

    reset_results: function () {
      $( '#results tr' ).remove();
      $( '#two-column' ).hide();
      $( '#one-column' ).show();
    },

    results: function () {
      if ( ! $$.validate() ) return false;
      
      var args = {};

      var sel_keys =
        'authorities species domains ranges'.split( ' ' );

      for ( var i = 0; i < sel_keys.length; ++i ) {
        var k = sel_keys[ i ];
        if      ( k == 'authorities' ) j = 'authority';
        else if ( k == 'domains' )     j = 'domain';
        else if ( k == 'ranges' )      j = 'range';
        else                           j = k;
        
        args[ j ] = $( '#' + k ).find( ':selected' ).text();
        if ( k == 'domains' || k == 'ranges' ) {
          args[ j ] = args[ j ].replace( /\s.*$/, '' );
        }
      }

      if ( args[ 'domain' ] == ANY ) {
        delete args[ 'domain' ];
      }

      var ids = $.grep(
                        $( '#ids' )[ 0 ].value.split( /\s+/ ),
                        function ( n, i ) {
                          return n.match( /^\S+$/ );
                        }
                      );

      if ( ids.length ) args[ 'ids' ] = ids;

      if ( $$.have_ids_file() ) {
        $( '#cache-id' ).load( $$.complete_submission( args ) );
        $( '#inputs' )[ 0 ].submit();
        return;
      }
      else {
        $$.fetch_results( args );
      }
    },

    have_ids_file: function () {
      return $( '#ids-file' )[ 0 ].value.match( /\S/ );
    },

    complete_submission: function ( args ) {
      var callback = function ( e ) {
        $( '#cache-id' ).unbind( 'load', callback );
        // sic! the hash key for the argument is 'cache_id', even
        // though the iframe's id is 'cache-id'
        args[ 'cache_id' ] =
          $( '#cache-id' )[ 0 ].contentDocument.body.textContent;
        
        $$.fetch_results( args );
      }
      return callback;
    },

    make_call: function ( method, params, id ) {
      var call = {};
      call.method = 'sample_inputs';
      call.params = [];
      call.id = id || 0;
      return call;
    },

    fetch_sample_inputs: function () {
//       var json = JSON.stringify( [ 'sample_inputs' ] );

      var call = $$.make_call( 'sample_inputs', [] );
      var json = JSON.stringify( call );

      var callback =
        function ( rr, text_status ) {

          var is_error = $$.is_service_error( rr );

          // CLEAN THIS UP! [080609Mon]
          if ( !is_error && 'result' in rr ) rr = rr.result;

          if ( !is_error ) {
            SAMPLE_INPUTS = rr;
            var r = SAMPLE_INPUTS[ 'authorities' ];
            var n = r && r.length;

            var $sel = $SEL[ 'authorities' ];
            if ( n ) {            // fetch was successful
              //$sel._last_json = $$.to_json( [ 'available_authorities' ] );
              $sel._last_json = JSON.stringify( [ 'available_authorities' ] );
              $$.receive_opts( r, $sel );
              $$.hide_processing_indicator();
              return; // it's important to have this return here: the
                      // remaining code in this method handles all
                      // unsuccessful fetches
            }
          }

          $$.hide_processing_indicator();

          var msg =
            ( is_error && rr.error ) ||
            'unknown server error (' + text_status + ') [' + rr + ']';


          // CLEAN THIS UP! [080609Mon]
          if ( msg.message ) msg = msg.message;

          $$.server_error( msg );
        };

      $$.show_processing_indicator();
      var xhr      = $.post_json( SERVER_URL, json, callback );
      setTimeout( $$.abort_xhr( xhr ), TIMEOUT );
    },

    fetch_results: function ( args ) {
      //var json = JSON.stringify( [ 'translate', args ] );
      var call = {};
      call.method = 'translate';
      call.params = [ args ];
      call.id = 0;
      var json = JSON.stringify( call );

      var callback = function ( p, status ) {
        if ( status == 'success' ) {
          if ( p.error ) {
            // CLEAN THIS UP! [080609Mon]
            var msg = p.error;
            if ( msg.message ) msg = msg.message;
            $$.server_error( msg );
          }
          else {

            // CLEAN THIS UP! [080609Mon]
            var results = p;
            if ( results.result ) results = results.result

            THE_RESULTS = results;
            THE_ARGS = args;
            $$.show_results();
          }
        }
        else {
          $$.server_error( 'unknown server error (' + status + ') [' + p + ']' );
        }
      };

      $$.show_processing_indicator();
      var xhr = $.post_json( SERVER_URL, json, callback );

      setTimeout( $$.abort_xhr( xhr ), TIMEOUT );
    },

    show_results: function ( one_column ) {
      if ( $$.want_spreadsheet() ) {
        $$.show_results_as_spreadsheet();
        return;
      }

      $$.reset_results();

      $( '#results-panel' ).show();

      var thead = $( '#results thead' );
      var tr = $( '<tr></tr>' );
      thead.append( tr );

      if ( !one_column )
        tr.append( $( '<th>' + THE_ARGS[ 'domain' ] + '</th>' ) );

      tr.append( $( '<th>' + THE_ARGS[ 'range' ]  + '</th>' ) );

      var table = $( '#results tbody' );
      var r = THE_RESULTS;
      var all_unknown = ( r.length > 0 );
      var have_unknowns = false;

      for ( var i = 0; i < r.length; ++i ) {
        var tr = $( '<tr></tr>' );
        table.append( tr );

        var v;
        var unknown_id = false;
        if ( r[ i ].length == 1 || ( unknown_id = ( r[ i ][ 1 ] == null ) ) ) {

          if ( unknown_id ) {
            have_unknowns = true;
          }
          else {
            all_unknown = false;
          }

          // the following ensures that all rows, even those that
          // should be (or at least *look*) empty, have the same
          // height

          // TODO: there ought to be a strictly CSS-based way to
          // achieve this goal
          if ( one_column ) v = '&nbsp;';
          else v = '';
        }
        else {
          all_unknown = false;
          v = r[ i ].slice( 1 ).join( '<br/>' );
        }

        if ( !one_column ) {
          var dd = $( '<td class="from-col"></td>' );
          if ( unknown_id ) dd.addClass( 'unknown-id' );
          dd.html( r[ i ][ 0 ] );
          tr.append( dd );
        }

        var rd = $( '<td class="to-col"></td>' );
        rd.html( v );
        tr.append( rd );
      }

      if ( have_unknowns || all_unknown ) {
        var auth = $$.selected_authority();
        var domain = $$.selected_domain();
        var species = $$.selected_species();
        if ( all_unknown ) {
          if ( domain == ANY ) {
            var msg = 'According to authority "' + auth +
                      '" none of the input IDs belong to any namespace ' +
                      'for species ' + species + '.';
          }
          else {
            var msg = 'According to authority "' + auth +
                      '" none of the input IDs belong to namespace "' +
                      domain + '" for species ' + species + '.';
          }
        }
        else if ( have_unknowns ) {
          if ( domain == ANY ) {
            var msg = 'Shown in <span class="unknown-id">red</span> below ' +
                      'are IDs that do not belong to any namespace ' +
                      'for species ' + species +
                      ' (according to authority "' + auth + '").';
          }
          else {
            var msg = 'Shown in <span class="unknown-id">red</span> below ' +
                      'are IDs that do not belong to namespace "' +
                      domain + '" for species ' + species +
                      ' (according to authority "' + auth + '").';
          }
        }

        $$.show_message( msg );
      }

      $$.hide_processing_indicator();
    },

    show_results_as_spreadsheet: function () {
      $$.reset_results();

      var tsv = '';
      tsv += THE_ARGS[ 'domain' ] + '\t' + THE_ARGS[ 'range' ] + '\n';

      var r = THE_RESULTS;

      var all_unknown = ( r.length > 0 );
      var have_unknowns = false;

      for ( var i = 0; i < r.length; ++i ) {
        var v;
        var unknown_id = false;
        if ( r[ i ].length == 1 || ( unknown_id = ( r[ i ][ 1 ] == null ) ) ) {

          if ( unknown_id ) {
            have_unknowns = true;
            v = '!?';
          }
          else {
            all_unknown = false;
            v = '';
          }

        }
        else {
          all_unknown = false;
          v = r[ i ].slice( 1 ).join( ' ' );
        }

//         if ( !one_column ) {
//           var the_class =
//             unknown_id ? 'class="unknown-id" ' : '';
//           var dd =
//             $( '<td ' + the_class + 'valign="top"></td>' );
//           dd.html( r[ i ][ 0 ] );
//           tr.append( dd );
//         }

        tsv += r[ i ][ 0 ] + '\t' + v + '\n';
      }

//       if ( have_unknowns || all_unknown ) {
//         var auth = $$.selected_authority();
//         var domain = $$.selected_domain();
//         var species = $$.selected_species();
//         if ( all_unknown ) {
//           var msg = 'According to authority "' + auth +
//                     '" none of the input IDs belong to namespace "' +
//                     domain + '" for species ' + species + '.';
//         }
//         else if ( have_unknowns ) {
//           var msg = 'Shown in <span class="unknown-id">red</span> in the ' +
//                     'results are IDs that do not belong to namespace "' +
//                     domain + '" for species ' + species +
//                     ' (according to authority "' + auth + '").';
//         }

//         $$.show_message( msg );
//       }

      $( '#table-to-bounce' )[ 0 ].value = tsv;
      $( '#ttb-form' )[ 0 ].submit();

      $$.hide_processing_indicator();
    },

    hide_results: function ( one_column ) {
      $( '#results-panel' ).hide();
      $$.reset_results();
    },

    dump_test: function ( b ) {
      LOG( Dumper.dump( b ) );
      return false;
    },

    updater: function ( $sel, $next ) {
      return function ( e ) {

        // we need to keep track of the last selection to make sure
        // that the event really represents a change in the selected
        // value; Safari 3 tends to generate spurious change events
        // which otherwise would result in unwanted updates to some
        // select elements

        var selection = $sel._selection();
        if ( $sel._last_selection != selection ) {
          $$.hide_results();
          $$.clear_message();
          $$.hide_message();

          $sel._last_selection = selection;
          $$.nix_zeroth( $sel );

          $$.check_submit();
          $$.check_reset();

          if ( $next ) $$.fetch( $next, 1 );
        }
      }
    },

    nix_zeroth: function ( $sel ) {
      $sel.find( ':contains(' + ZEROTH_OPT + ')' ).remove();
    },

    fetch: function ( $sel, no_cache ) {
      var do_cache = !no_cache;

      if ( do_cache ) $$.cache_last( $sel );
      
      var json     = $$.build_json( $sel._service_call );

      var cached;
      if ( do_cache && ( cached = CACHED[ json ] ) ) {

        $sel.each( function ( i ) {
            var j = 2 * i;
            $$.install_opts( $( this ),
                             cached[ j ],
                             cached[ j + 1 ] );
          } );

        $sel._last_json = json;
      }
      else {
        var callback = $$.callback( $sel, json );
        $$.show_processing_indicator();
        var xhr      = $.post_json( SERVER_URL, json, callback );
        setTimeout( $$.abort_xhr( xhr ), TIMEOUT );
      }

      var next = $sel._next;
      while ( next ) {
        $$.clear_opts( next );
        next = next._next;
      }

    },

    cache_last: function ( $sel ) {
      var last_json = $sel._last_json;

      if ( last_json ) {
        var to_cache = [];

        $sel.each(
          function ( i ) {
            var opts = $( this ).find( 'option' ).get();
            var s = 0;
            $.each( opts,
              function ( i, e ) {
                if ( e.selected ) { s = i; return false }
              }
            );
            to_cache.push( opts, s );
          }
        );
        CACHED[ last_json ] = to_cache;
      }
    },

    build_json: function ( sc ) {
      var call = {};
      call.method = sc[ 0 ];
      call.params =
        $.map( sc.slice( 1 ),
               function ( e ) {
                 return typeof e == 'function' ? e() : e;
               }
             );
      call.id = 0;
      var json = JSON.stringify( call );

      return json;
    },

    namespace_opt: function ( d ) {
      var ns  = d[ 0 ];
      var rep = d[ 1 ];
      return ns + ' [' + rep + ']';
    },

    cached: function () { return CACHED },

    callback: function ( $sel, json ) {

      var preprocess = $sel._preprocess || $$.identity;
      var receive = $sel._receive || $$.receive_opts;

      // the callback is a closure whose scope includes a (jQuerified)
      // SELECT element ($sel), a JSON string (json), and two secondary
      // callbacks (preprocess and receive).

      var the_callback =

        function ( r, text_status ) {

          try {
            // CLEAN THIS UP! [080609Mon]
            if ( $$.is_service_error( r ) ) {
              var msg = r.error;
              if ( msg.message ) msg = msg.message;
              throw new Error( msg );
            }

            // CLEAN THIS UP! [080609Mon]
            if ( 'result' in r ) r = r.result;

            if ( !( 'length' in r ) )
              throw new Error( 'unknown server error' );

            if ( !( r && r.length ) )
              throw new Error( 'server returned empty response' );

            r = preprocess( r );

            if ( !( r && r.length ) )
                throw new Error( 'server returned unusable response' );

          }
          catch ( e ) {
            $$.server_error( e.message );
            return;
          }

          $sel._last_json = json;
          receive( r, $sel );
          $$.hide_processing_indicator();
          return;
        };

      return the_callback;
    },

    receive_opts: function ( r, $sel ) {

      // generic callback to install newly fetched options

      var data = r.length == 1 ? r : [ [ 0, ZEROTH_OPT ] ].concat( r );
      var opts = $$.make_new_opts( data );
      $$.install_opts( $sel, opts );
    },

    receive_domains: function ( r, $sel ) {
      var selected_authority = $$.selected_authority();
      // GROSS KLUGE!
      if ( selected_authority == 'ncbi' ||
           selected_authority == 'ensembl' ||
           selected_authority == 'wormbase' ) {
        $$.receive_opts( r, $sel.filter( '#domains' ) );
      }
      else {
        $$.receive_opts( r.concat( [ [ -1, ANY ] ] ),
                         $sel.filter( '#domains' ) );
      }
    },

//     receive_ranges: function ( r, $sel ) {
//       $$.receive_opts( r, $sel.filter( '#ranges' ) );
//     },

    receive_namespaces: function ( r, $sel ) {
      $$.receive_opts( r.concat( [ [ -1, ANY ] ] ),
                       $sel.filter( '#domains' ) );
      $$.receive_opts( r, $sel.filter( '#ranges' ) );
    },

    install_opts: function ( $sel, opts, index ) {

      // we use apply here to pass the arguments list directly to
      // update_select; this is important because update_select's
      // depends on the number of arguments it receives (the last
      // argument--index--is optional)
      $$.update_select.apply( $$, arguments );

      if ( opts.length == 1 ) $sel.trigger( 'change' );
    },

    clear_opts: function ( $sel ) {
      $( 'option', $sel ).remove();
    },

    update_select: function ( $select, opts, index ) {
      $( 'option', $select ).remove();
      $select.append( opts );

      var selection;
      if ( arguments.length == 3 ) {
        selection = $( 'option', $select ).get( index );
      }
      else {
        selection =
          $select.find( 'option:selected', 'option:first' ).get( 0 );
      }

      selection.selected = 'true';
      $select._last_selected = $( selection ).text();
    },

    make_new_opts: function ( data ) {
      var os = $.repeat( data.length,
                         function () { return CE( 'option' ) } );
      $( os ).
        each(
          function ( i ) {
            var o = $( this );
            var d = data[ i ];
            if ( $$.is_array( d ) ) {
              o.val ( d[ 0 ] );
              o.text( d[ 1 ] );
            }
            else {
              o.text( d );
              o.val( d == ZEROTH_OPT ? 0 : d ); 
            }
          }
        );
      return os;
    },

    // I'm disabling check_reset and check_submit for now, because
    // they're not working; they don't get triggered at the right
    // time, so the submit button remains enabled even after the user
    // changes one of the choices in a way that renders some other
    // options obsolete...

    check_reset: function () {
      return;                   // this is not working
      $( '#reset_form' )[ 0 ].disabled = !$$.check_any_input();
    },

    check_submit: function () {
      return; // this is not working
      $( '#submit_form' )[ 0 ].disabled = !$$.check_all_inputs();
    },

    check_any_input: function () {
      return $$.check_any_select() || $$.check_ids_field();
    },

    check_any_select: function () {
      var selects = $( 'select.required' );
      for ( var i = 0; i < selects.length; ++i ) {
        var s = $( selects[ i ] );
        var t = $( 'option:selected', s ).text();
        if ( t != NBSP && t != ZEROTH_OPT ) return true;
      }

      return false;
    },

    check_ids_field: function () {
      return $( '#ids' )[ 0 ].value != '';
    },

    check_any_field: function () {
      return ( $( '#ids' )[ 0 ].value + $( '#ids-file' )[ 0 ].value ) != '';
    },

    check_all_inputs: function () {
      return $$.check_all_selects() && $$.check_any_field();
    },


    check_all_selects: function () {
      var all = true;

      $( 'select.required' ).each( 
        function () {
          var s = $( 'option:selected', this ).text();
          return ( all = ( all && s &&
                           s != NBSP &&
                           s != ZEROTH_OPT ) );
        }
      );

      return !!all;
    },

    validate: function () {
      var ret = $$.check_all_inputs();
      if ( !ret ) {
        $$.error( 'some required parameters are missing' );
      }
      return !!ret;             // make sure the returned value is boolean
    },

    report_server_error: function ( e ) {
      try { alert( e.error ) } catch ( x ) {}
    },

    abort_xhr: function ( x ) {
      return x ? function () { x.abort() } : $$.noop;
    },

    is_array: function ( a ) {
      // I think this fails in IE...
      return ( typeof a === 'object' &&
               typeof a.length === 'number' &&
               !( a.propertyIsEnumerable( 'length' ) ) );
    },

    is_service_error: function ( o ) {
      return ( typeof o === 'object' && o.error );
    },

    server_error: function ( e ) { $$._error( 'server', e ); },
    error: function ( e ) { $$._error( null, e ); },
    _error: function ( type, e ) {
      var pfx = '#' + ( type ? type + '-' : '' ) + 'error';
      $( pfx + '-message' ).html( '<p>' + e + '</p>' );
      $$.hide_processing_indicator();
      $( pfx ).jqmShow();
    },

    warning: function ( w ) {
      var pfx = '#warning';
      $( pfx + '-message' ).html( '<p>' + w + '</p>' );
      $( pfx ).jqmShow();
    },

    show_message: function ( m ) {
      $$.clear_message();
      var div = $( '#messages' );
      div.html( '<p>' + m + '</p>' );
      div.show();
    },

    hide_message: function () {
      $( '#messages' ).hide();
    },

    clear_message: function (){
      $( '#messages p' ).remove();
    },

    show_processing_indicator: function () {
//       $( '#processing-indicator-window' ).jqmShow();
      $( '#loading' ).show();
    },

    hide_processing_indicator: function () {
      $( '#loading' ).hide();
    },

    // barebones conversion to JSON; handles *only* arrays of strings
    // that do not contain double quotes; if the first argument is an
    // array it will use it as the argument; otherwise it will use the
    // builtin arguments array as argument; the members of this array
    // will be converted to strings (if necessary), surrounded by
    // double-quotes, joined by commas, flanked by square brackets
    to_json: function ( /* variable args */ ) {
      var a = arguments;
      if ( $$.is_array( a[ 0 ] ) ) a = a[ 0 ];
      var q = $.map( a, function ( e, i ) { return '"' + e + '"' } );
      return '[ ' + q.join( ', ' ) + ' ]';
    },

    assert: function ( b, msg ) {
      var na = arguments.length;
      if ( !na ) throw 'internal error';
      if ( b ) return;
      var s = 'failed assertion';
      if ( na > 1 ) s += ': ' + msg;
      throw s;
    },

    dump_event: function ( e ) {
      var s = $( '#dump' ).html() + '\n\n\n';
      s += '__CALLS__: ' + ++CALLS + '\n';
      for ( var i in e ) {
        s += i + ': ';
        var v = e[ i ];
        var t = typeof v;
        if ( i == 'currentTarget' ||
             i == 'target' ||
             i == 'explicitOriginalTarget' ||
             i == 'srcElement' ) {
          s += v.id;
        }
        else if ( t == 'function' ) s += '[function]';
        else {
          s += v;
        }
        s += '\n';
      }
      $( '#dump' ).html( s );
    },

    dump_object: function ( o ) {
      var s;
      if ( typeof o != 'object' ) {
        s = 'not an object:\n' + o;
      }
      else {
        CACHED[ 'objects' ] = {};
        s = 'calls: ' + ++CALLS + '\n' + $$._dump_object( o, 0, 3 );
        delete CACHED[ 'objects' ];
      }
      $( '#dump' ).html( s );
    },

    _hash_code: function ( o ) {
      var s = '';
      for ( var i in o ) {
        try { s += i + o[ i ] } catch ( e ) { s += e }
      };
      return b64_sha1( s );
    },

    _dump_object: function ( o, l, x ) {
      var hc = $$._hash_code( o );
      var k = CACHED;
      if ( CACHED.objects[ hc ] ) return '[REUSED: ' + hc + ']';
      CACHED.objects[ hc ] = 1;

      var indent = $$._indent( '  ', l );

      var s =
        indent + '__CONSTRUCTOR__: ' + o.constructor + '\n' +
        indent + '__HASH_CODE__: '   + hc + '\n';
      try {
        for ( var i in o ) {
          if ( ABORT ) {
            s += indent + '== ABORTED ==' + '\n'; break;
          }

          if ( i == 'ownerDocument' ) continue;

          if ( v instanceof HTMLDocument ) {
            alert( I );
            return 'ABORTED: [' + i + ': ' + v + ']';
          }

          s += indent + i + ':';
          var v = o[ i ];

          var t = typeof v;
          var s2;
          if ( l < x && t == 'object' &&
               ( i == 'srcElement' ||
                 i.match( /target$/i ) ||
                 ! v instanceof Node ) &&
               ( s2 = $$._dump_object( v, l + 1, x ) ) !== undefined ) {
            s += ( s2.match( /^\[REUSED/ ) ? ' ' : '\n' ) + s2;
          }
          else {
            s += ' ';
            if ( t == 'function' ) {
              s += '[function]';
            }
            else {
              s += v;
            }
          }
          s += '\n';
        }
        return s;
      }
      catch ( e ) { return undefined }
    },

    _indent: function ( s, n ) {
      var a = '';
      var i = 0;
      while ( i++ < n ) a += s;
      return a;
    },

    _add_plugins: function () {

      // this one-time initialization method installs some useful
      // extensions in the jQuery object ($)

      // returns the array consisting of the integers in [ i, j )
      // NB: $.ranges( i, i ) is empty!
      $.ranges = function ( i, j ) {
        var s = Math.max( j - i, 0 );
        var ret = new Array( s );
        for ( var k = 0; k < s; k++ ) ret[ k ] = i + k;
        return ret;
      };

      $.repeat = function ( n, callback ) {
        var ret = [];

        // Go through the array, translating each of the items to their
        // new value (or values).
        for ( var i = 0; i < n; i++ ) {
          var value = callback( i );
          if ( value !== undefined ) {
            if ( $$.is_array( value ) ) value = [ value ];
            ret = ret.concat( value );
          }
        }

        return ret;
      };

      $.post_json = function ( url, data, callback ) {
        return $.ajax( {
          type: "POST",
          url: url,
          data: data,
          success: callback,
          dataType: 'json',
          contentType: 'application/json'
        } );
      };

      $.keys = function ( o ) {
        var keys = [];
        try { // sometimes iterating over properties triggers an exception
          for ( var k in o ) keys.push( k );
        }
        catch ( toss ) {}
        return keys;
      }

      $.values = function ( o ) {
        var values = [];
        try { // sometimes iterating over properties triggers an exception
          for ( var k in o )
            try { // sometimes accessing a property triggers an exception
              values.push( o[ k ] );
            }
            catch ( toss ) {}
        }
        catch ( toss ) {}
        return values;
      }
    },

    _add_buttons: function () {
      var bt = $( '#buttons > table > tbody' )[ 0 ];
      for ( var t in $$ ) {
        var m;
        if ( !( m = t.match( /^_(test_.*)/ ) ) ) continue;
        var tr = CE( 'tr' );
        bt.appendChild( tr );
        var td = CE( 'td' );
        tr.appendChild( td );
        var bu = CE( 'button' );
        td.appendChild( bu );
        bu.appendChild( CTN( m[ 1 ] ) );
        bu.onclick = $$[ m[ 0 ] ];
      }
    },

    preprocess_namespaces: function ( r ) {
      try {
        return $.map( r,
                      function ( d, i ) {
                        return [ [ d[ 0 ], $$.namespace_opt( d ) ] ]
                      }
                    );
      }
      catch ( e ) {
        // TODO this should be an internal error
        return r;
      }
    },

    noop: function () {},
    identity: function ( x ) { return x },

    selected_authority: function () {
      return $$.selected_item( $( '#authorities' ) );
    },

    selected_species: function () {
      return $$.selected_item( $( '#species' ) );
    },

    selected_domain: function () {
      return $$.selected_item( $( '#domains' ) ).replace( /\s.*/, '' );
    },

    selected_range: function () {
      return $$.selected_item( $( '#ranges' ) ).replace( /\s.*/, '' );
    },

    selected_item: function ( $sel ) {
      try {
        var selected_item = $sel.find( ':selected' ).text();
      }
      catch ( e ) {}
      if ( selected_item === undefined ||
           !selected_item.match( /\S/ ) ||
           selected_item == ZEROTH_OPT ) {
        return undefined;
      }
      else {
        return selected_item;
      }
    },

    the_selection: function ( $sel ) {
      return function () {
        var ret = $sel.find( ':selected' ).text();
        $$.assert( ret && ret != ZEROTH_OPT );
        return ret;
      }
    },

    _init_selectors: function () {
      var ids  = [];
      var selection = [];

      var required = $( 'select.required' );

      var $s;

      $s = $SEL[ 'authorities' ] = $( '#authorities' );
      var selected_authority =
        $SEL[ 'authorities' ]._selection =
          $$.the_selection( $SEL[ 'authorities' ] );

      $s = $SEL[ 'species' ] = $( '#species' );
      var selected_species =
        $SEL[ 'species' ]._selection =
          $$.the_selection( $SEL[ 'species' ] );

      $s = $SEL[ 'domains' ] = $( '#domains' );
      var selected_domain =
        $SEL[ 'domains' ]._selection =
          function () {
            var f = $$.the_selection( $SEL[ 'domains' ] );
            return f().replace( /\s.*/, '' );
          };

      $s = $SEL[ 'ranges' ] = $( '#ranges' );
      // do we really need the following???
      var selected_range =
        $SEL[ 'ranges' ]._selection =
          function () {
            var f = $$.the_selection( $SEL[ 'ranges' ] );
            return f().replace( /\s.*/, '' );
          };
      
      var id, $sel, $next;

      $sel = $SEL[ 'authorities' ];
      $next = $sel._next = $SEL[ 'species' ];
      $sel.change( $$.updater( $sel, $next ) );
      $sel._last_selection = '';
      $sel._service_call = [ 'available_authorities' ];

      $sel = $SEL[ 'species' ];
      $next = $sel._next = $SEL[ 'domains' ];
      $sel.change( $$.updater( $sel, $next ) );
      $sel._last_selection = '';

      // an additional parameter needed for available_species, so
      // that it sends internal names too
      $sel._service_call = [ 'available_species', selected_authority, 1 ];


      // an additional parameter needed for available_domains and
      // available_ranges, so that it sends representatives too
      var with_reps = 1;

      $sel = $SEL[ 'domains' ];
      $next = $sel._next = $SEL[ 'ranges' ];
      $sel.change( $$.updater( $sel, $next ) );
      $sel._last_selection = '';
      $sel._service_call = [
                             'available_domains',
                             selected_authority,
                             selected_species,
                             with_reps
                           ];
      $sel._receive = $$.receive_domains;
      //$sel._preprocess = $$.preprocess_domains;
      $sel._preprocess = $$.preprocess_namespaces;

      $sel = $SEL[ 'ranges' ];
      $next = $sel._next = undefined;
      $sel.change( $$.updater( $sel, $next ) );
      $sel._last_selection = '';
      $sel._service_call = [
                             'available_ranges',
                             selected_authority,
                             selected_species,
                             selected_domain,
                             with_reps
                           ];

      //$sel._receive = $$.receive_ranges;
      $sel._preprocess = $$.preprocess_namespaces;

      return;
    },

    __init_selectors: function () {
      var ids  = [];
      var selection = [];

      var required = $( 'select.required' );

      for ( var i = 0; i < required.length; ++i ) {
        var id = required[ i ].id;
        ids[ i ] = id;
        var $s = $SEL[ id ] = $( required[ i ] );
        $s._selection = selection[ i ] = $$.the_selection( $s );
      }

      $SEL[ 'namespaces' ] = $( [ $SEL[ 'domains' ][ 0 ], $SEL[ 'ranges' ][ 0 ] ] );

      var l = ids.length = ids.length - 1;
      ids[ l - 1 ] = 'namespaces';

      var id, $sel, $next;

      $sel = $SEL[ 'authorities' ];
      $next = $sel._next = $SEL[ 'species' ];
      $sel.change( $$.updater( $sel, $next ) );
      $sel._last_selection = '';
      $sel._service_call = [ 'available_authorities' ];

      $sel = $SEL[ 'species' ];
      $next = $sel._next = $SEL[ 'namespaces' ];
      $sel.change( $$.updater( $sel, $next ) );
      $sel._last_selection = '';

      // an additional parameter needed for available_species, so
      // that it sends abbreviations too
//       $sel._service_call = [ 'available_species', selection[ 0 ], 1 ];
       $sel._service_call = [ 'available_species', selection[ 0 ] ];

      $sel = $SEL[ 'namespaces' ];
      $next = $sel._next = undefined;

      $SEL[ 'domains' ]._last_selection = '';

      $sel.change( $$.updater( $SEL[ 'ranges' ], undefined ) );
      $SEL[ 'ranges' ]._last_selection = '';

      $sel._receive = $$.receive_namespaces;

      // an additional parameter needed for available_namespaces, so
      // that it sends representatives too

      $sel._service_call = [ 'available_namespaces',
                             selection[ 0 ],
                             selection[ 1 ],
                             1 ];

      $SEL[ 'namespaces'  ]._preprocess = $$.preprocess_namespaces;

      return;
    },

    /* tests **************************************************/

    _test_update_select_0: function () {
      var select = $( '#species' )[ 0 ];
      var opts = [ [ 0, ZEROTH_OPT ],
                   [ 'FEE', 'fee' ],
                   [ 'FIE', 'fie' ],
                   [ 'FOE', 'foe' ] ];
      $$.update_select( select, opts );
    },

    _test_update_select_1: function () {
      var select = $( '#species' )[ 0 ];
      var opts = [ [ 0, ZEROTH_OPT ],
                   [ 'alpha', 'ALPHA' ],
                   [ 'beta', 'BETA' ],
                   [ 'gamma', 'GAMMA' ],
                   [ 'delta', 'DELTA' ],
                   [ 'epsilon', 'EPSILON' ],
                   [ 'zeta', 'ZETA' ],
                   [ 'eta', 'ETA' ],
                   [ 'theta', 'THETA' ],
                   [ 'iota', 'IOTA' ],
                   [ 'kappa', 'KAPPA' ],
                   [ 'lambda', 'LAMBDA' ],
                   [ 'mu', 'MU' ]
                 ];
      $$.update_select( select, opts );
    },

    _test_update_select_2: function () {
      var authority = 'ensembl';
      $$.ask( $$.to_json( [ 'available_species', authority ] ),
              $$.receive_species );
    },

    _test_error_popup_0: function () {
      $$.server_error( 'an error occurred!!!' );
      $( '#server-error' ).jqmShow();
    },

    _test_error_popup_1: function () {
      $$.ask( '[ "foobar" ]',
              function ( r ) {
                var e = r.error;
                if ( e ) {
                  $$.error( e )
                }
                else {
                  alert( 'test_failed' );
                }
              }
            );
    },

    _test_env: function () {
      $$.ask( '[ "env_as_string" ]',
              function ( r ) {
                if ( r.error ) {
                  alert( 'test_failed' );
                  return;
                }
                // we do it like this for now...
                $$.error( '<pre>' + r[ 0 ] + '</pre>' );
              }
            );
    },

    _test_validate: function () {
      $( 'form:first' ).submit();
    },

    _test_xserver_ajax: function () {
      var on_error = function ( x, status, error ) {
        alert( 'GET failed (' + status + '): ' + error );
      };
      try {
        var xhr =
          $.ajax( {
                    url: 'http://llama.med.harvard.edu',
                    cache: false,
                    timeout: 5000,
                    success: function ( data, status ) {
                      alert( 'GET succeeded (' + status + ')' );
                    },
                    error: on_error
                  }
              );
      }
      catch ( e ) { on_error( xhr, undefined, e ) }
    },

    do_ext_stuff: function () {
      // disabled
      return;
      // to re-enable, make sure to uncomment the Ext-related
      // directives in the head of the html file

      var ids_box = new Ext.form.TextArea( { applyTo: 'ids' } );
      var inputs_panel = new Ext.Panel( {
        applyTo: 'inputs-panel',
        region: 'center',
        frame: 'true'
      } );
      var results_panel = new Ext.Panel( { applyTo: 'results-panel',
                                           region: 'east', width: "40%" } );
      var viewport = new Ext.Viewport({
                       layout: 'border',
                       items: [
                                inputs_panel,
                                results_panel
                              ]
                     });
      viewport.doLayout();
    },


    __end: null

  };

  var no_op = function ( msg ) {};
  $$.console = {
    info: no_op,
    warning: no_op,
    error: function ( msg ) { throw new Error( msg ) }
  };

  return $$;
} )( jQuery );

if ( this.console === undefined )
  this.console = Synergizer.console;

// we use this longer expression:
jQuery( function () {
          Synergizer.start()
        }
);
// ...rather than the more succinct:
// jQuery( Synergizer.start );
// so that we can use "this" within Synergizer.start to refer to the
// Synergizer global object
