/*  $Id: benchmarker.js,v 1.26 2007/04/21 04:46:18 altblue Exp $
    (c) 2006-2007 Marius Feraru <altblue@n0i.net>

    This module is free software; you can redistribute it
    and/or modify it under the same terms as Perl itself.

    For details, check http://gfx.neohub.com/benchmark/
*/

Benchmarker = function() { return this.init.apply(this, arguments) };

Benchmarker.NAME     = 'Benchmarker';
Benchmarker.VERSION  = '0.5.1';
Benchmarker.REVISION = '$Revision: 1.26 $'.replace(/^[^ ]+ (.+?) \$$/,'$1');

Benchmarker.toString = function() {
  return '[' + this.NAME + ' v' + this.VERSION + '/r' + this.REVISION + ']';
};

Benchmarker.prototype = {
  init: function(m,o) {
    this.methods = m || {};
    this.mCount  = 0;
    this.options = o || {};
    this.origTitle = document.title + (this.options.summary ? ': ' + this.options.summary : '');
    document.title = '[LOADING] ' + this.origTitle;
    var css = [
      '../messages/message.css',
      '../loading/loading.css',
      '../neodom/sheet.css',
      'style.css'
    ];
    var scripts = [
      '../prototype/prototype.js',
      '../scriptaculous/effects.js',
      '../scriptaculous/controls.js',
      '../neodom/neodom.js',
      '../neodom/sheet.js',
      '../loading/loading.js',
      '../messages/message.js',
      'benchmark.js',
      'benchmark_perlish.js',
      'benchmark_htmlish.js',
      'benchmark_firebug.js'
    ];

    if (this.options.css) {
      css = css.concat(this.options.css);
      delete this.options.css;
    }
    if (this.options.scripts) {
      scripts = scripts.concat(this.options.scripts);
      delete this.options.scripts;
    }

    for (var i = 0, l = css.length; i < l; i++) {
      var prefix = css[i].indexOf("://") > 0 || css[i][0] === '/' ? '' : Benchmarker.PREFIX;
      document.write('<link rel="stylesheet" type="text/css" href="' + prefix + css[i] + '" />');
    }

    for (var i = 0, l = scripts.length; i < l; i++) {
      var s = scripts[i],
        pre = '<script type="text/javascript" src="',
        post = '"></script>';
      if (s.constructor == Array) {
        pre = '<script type="text/javascript">',
        post = '</script>',
        s = s.join('\r\n');
      } else {
        var prefix = s.indexOf("://") > 0 || s[0] === '/' ? '' : Benchmarker.PREFIX;
        pre += prefix;
      }
      document.write(pre + s + post);
    }

    var func = this.load, obj = this, observer = function() {func.apply(obj)};
    if (window.addEventListener)
      window.addEventListener('load', observer, false);
    else if (window.attachEvent)
      window.attachEvent('onload', observer);
  },

  load: function() {
    Benchmark.defaults.responders = 'HTMLish';
    Benchmark.defaults.iterations = -0.5;
    Benchmark.defaults.cooldown   = 50;

    document.title = this.origTitle;
    $T('h1', document.body, {innerHTML: this.origTitle.replace('Benchmark', '<a href=".">Benchmark</a>')});
    $T('div', document.body, {className: 'description', innerHTML: this.options.description || ''});
    new Ajax.Updater(
      $T('div', document.body).loading(),
      Benchmarker.PREFIX + 'creator_form.html?' + (new Date()).getTime(),
      {
        method: 'get',
        evalScripts: true,
        onComplete: this.start.bind(this)
      }
    );
  },

  start: function() {
    this.form = $('benchmarker');
    if (!this.form)
      return Message.error('Cannot find benchmark form!');

    var rsel = $('responders');
    $H(Benchmark.Responders).keys().each(function(val) {
      $T('option', rsel, { value: val, innerHTML: val.escapeHTML() });
    });

    this.form.observe( 'submit', this.runBench.bindAsEventListener(this) );

    $H(this.methods).each(function(m) {
      this._addMethod( m.key, this.source(m.value) );
    }.bind(this));

    $H(Benchmark.defaults).each(function(d) {
      var fld = $(d.key);
      if (!fld) return;
      var val = typeof this.options[d.key] === 'undefined' ? d.value : this.options[d.key];
      if (typeof d.value === 'function')
        val = this.source(val);
      fld.value = val;
    }.bind(this));

    if (this.options.setup)
      $('setupCode').innerHTML = this.source( this.options.setup ).escapeHTML();

    $w('clearResults addMethod toggleEditor toggleOptions toggleHandlers').each(
      function(met) {
        var button = $(met + 'Button');
        if (button)
          button.observe('click', this[met].bindAsEventListener(this));
      }.bind(this)
    );

    this.form.select('.elastic').each(function(el) {
      if (el.tagName.lc() === 'textarea') {
        new ElasticTextarea(el);
      } else if (el.tagName.lc() === 'input') {
        new ElasticInput(el);
      }
    });

    if (typeof this.options.editOptions === 'undefined')
      this.options.editOptions = true;

    this.status = {
      editor:    !!this.options.edit,
      bmoption:  !!this.options.editOptions,
      bmhandler: !!this.options.editHandlers
    };
    this.handlersShouldBeVisible = this.status.bmhandler;
    if (this.status.editor)
      this.toggleEditor();
    if (this.status.bmoption)
      this.toggleOptions();

    new Effect.Highlight( this.form.getInputs('submit')[0] );
  },

  source: function(func) {
    if (!func || typeof func !== 'function') return func;
    return func.toString()
      .replace(/^[^{]+{\s*/, '')
      .replace(/\s*}\s*$/, '')
      .replace(/\n(?:\t| {4})/g, '\n');
  },

  addMethod: function(ev) {
    if (ev) Event.stop(ev);
    this._addMethod();
  },

  _addMethod: function(title, code) {
    this.mCount++;
    var tr = $T('tr', {className: 'method'}),
      tp = $T('td', tr, {className: 'param'}),
      tv = $T('td', tr, {className: 'value'});
    $T('a', tp, {href: '#', innerHTML: 'x', title: 'remove function', className: 'delete'}
      ).observe('click', this.delMethod.bindAsEventListener(this));
    tp.appendChild($TT(' "'));
    var mname = $T('input', tp, {name: 'methodName',
      className: 'text number mname', title: 'method name',
      value: title || ('M' + this.mCount)});
    new ElasticInput(mname);

    tp.appendChild($TT('":'));
    tv.appendChild($TT('function() {'));
    $T('br', tv);
    new ElasticTextarea(
      $T('textarea', tv, {name: 'methodCode', className: 'function mcode',
        title: 'method code', rows: 2, cols: 40, innerHTML: (code || '').escapeHTML()})
    );
    $T('br', tv);
    tv.appendChild($TT('},'));
    tr.hide();
    var grip = $('methodsGrip');
    grip.parentNode.insertBefore(tr, grip);
    new Effect.Appear(tr, {afterFinish: function(){mname.activate()}});
  },

  delMethod: function(ev) {
    var el = Event.element(ev);
    Event.stop(ev);
    // this.mCount--; 
    var tr = el.up('tr');
    new Effect.Fade(tr, {duration: 0.1, afterFinish: function(){tr.remove()} });
  },

  toggle: function(ev, rowClass) {
    if (ev) Event.stop(ev);
    var elements = this.form.select('.' + rowClass);
    var el = elements[0];
    if (!el) return false;
    var isHidden = el.hasClassName('hidden');
    var effect = Effect[isHidden ? 'Appear' : 'Fade'];
    var effectOptions = {duration: 0.1, queue: {position: 'end', scope: 'bmeditor'}};
    if (isHidden) {
      effectOptions['beforeStart'] = function(ef) {
        ef.element.hide().removeClassName('hidden');
      };
    } else {
      effectOptions['afterFinish'] = function(ef) {
        ef.element.addClassName('hidden');
      };
    }
    elements.each(function(xel) { new effect(xel, effectOptions) });
    this.status[rowClass] = !!isHidden;
    return this;
  },

  isHidden: function(rowClass) {
    var elements = this.form.select('.' + rowClass);
    return elements[0] && elements[0].hasClassName('hidden');
  },

  toggleEditor: function(ev) {
    if (ev) Event.stop(ev);
    this.toggle(null, 'editor');
  },

  toggleOptions: function(ev) {
    if (ev) Event.stop(ev);
    var wasHidden = this.isHidden('bmoption');
    if (!wasHidden && this.status.bmhandler)
      this.toggle(null, 'bmhandler');
    this.toggle(null, 'bmoption');
    if (wasHidden && this.handlersShouldBeVisible)
      this.toggle(null, 'bmhandler');
  },

  toggleHandlers: function(ev) {
    if (ev) Event.stop(ev);
    this.toggle(null, 'bmhandler');
    this.handlersShouldBeVisible = this.status.bmhandler;
  },

  clearResults: function(ev) {
    if (ev) Event.stop(ev);
    $$('.tester').each(function(el){
      new Effect.Shrink(el, {afterFinish: function(){el.remove()}});
    });
  },

  runBench: function(ev) {
    if (ev) Event.stop(ev);
    var methodSets = [];

    // hand written methods
    var mnames = this.form.select('.mname'),
        mcodes = this.form.select('.mcode');
    if (mnames.length) {
      var methods = {};
      for (var i = 0, len = mnames.length; i < len; i++) {
        var method = null;
        try {
          eval('method = function(){' + $F(mcodes[i]) + '}');
        } catch (e) {
          Message.error(e.name + ':' + e.message);
          new Effect.Highlight(mcodes[i].up('tr'), {startcolor: '#ff0000', afterFinish: function(){mcodes[i].activate()}});
          return;
        }
        methods[$F(mnames[i])] = method;
      }
      methodSets.push( { name: 'default', methods: methods });
    }

    // "setup" generated methods
    var setup_code = $F('setupCode') || '';
    if (setup_code.match(/\S/)) {
      var methods = {};
      try {
        eval(setup_code);
      } catch (e) {
        Message.error(e.name + ':' + e.message);
        return;
      };
      if ( $H(methods).keys().length ) {
        methodSets.push( { name: 'auto', methods: methods });
      }
    }

    if (methodSets.length < 1) {
      Message.error('No methods? What do you want to benchmark?');
      new Effect.Highlight('addMethodButton', {startcolor: '#ff9900'});
      return;
    }

    // prepare Benchmark object(s) options
    var options = {}, opt = $H(Benchmark.defaults).keys();
    for (var i = 0, len = opt.length; i < len; i++) {
      var key = opt[i], fld = $(key);
      if (!fld) continue;
      if (typeof Benchmark.defaults[key] === 'function') {
        var method = null;
        try {
          eval('method = function(){' + fld.value + '}');
        } catch (e) {
          Message.error(e.name + ':' + e.message);
          new Effect.Highlight(fld.up('tr'),
            {startcolor: '#ff0000', afterFinish: function(){fld.activate()}});
          return;
        }
        options[key] = method;
      } else {
        options[key] = fld.value.strip();
      }
    }

    // build&run a Benchmark for each set
    if (methodSets.length > 1) {
      if (this.options.parallel) {
        for (var i = 0, l = methodSets.length; i < l; i++) {
          options.title = methodSets[i].name;
          new Benchmark(methodSets[i].methods, options);
        }
      } else {
        new Benchmarker.QRunner(methodSets, options);
      }
    }
    else {
      new Benchmark(methodSets[0].methods, options);
    }
  }
};

// run multiple benchmarks in straight order
Benchmarker.QRunner = function() {return this.init.apply(this, arguments) };
Benchmarker.QRunner.prototype = {
  init: function(sets, opt) {
    this.sets = sets, this.opt = opt, this.idx  = -1;
    // hijack "atEnd" handler
    this.atEndOrig = opt.atEnd || (function(){});
    var qe = this.next.bind(this), ae = this.atEndOrig;
    this.opt.atEnd = function() { ae.apply(this,arguments); qe(); };
    this.next();
  },
  next: function() {
    // restore the original "atEnd" handler
    if (this.bm) this.bm.atEnd = this.atEndOrig;
    this.idx += 1;
    if (this.idx >= this.sets.length)
      return this.finish();
    this.opt.title = this.sets[this.idx].name || ('benchmark #' + (i+1));
    this.bm = new Benchmark(this.sets[this.idx].methods, this.opt);
  },
  finish: function() { // paranoid cleanup?
    delete this.sets;
    delete this.opt;
  }
};

function getLastElementByTagName(node, tag) {
  if (!node) return null;
  if (node.hasChildNodes()) {
    var kids = node.childNodes, len = kids.length;
    for (var i = len - 1; i >= 0; i--) {
      var el = getLastElementByTagName(kids[i], tag);
      if (el != null) return el;
    }
  };
  if (node.nodeType === 1 && node.tagName.toLowerCase() === tag.toLowerCase())
    return node;
  return null;
}

Benchmarker.PREFIX = getLastElementByTagName(document, 'script').src.replace(/[^\/]+$/, '');
