/* $Id: game.js,v 1.77 2008/03/30 19:33:58 altblue Exp $
 *
 * Copyright 2006, 2007 Marius Feraru <altblue@n0i.net>
 * All rights reserved.
 *
 * This file is part of TGC (Triburile.ro Game Controller)
 *
 * TGC is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details:
 * <http://www.gnu.org/licenses/>
 *
 */

if (typeof console == 'undefined') { // if you didn't enable Firebug... be silent ;-)
  console = { _time: {} };
  ['log','info','debug','warn','error','assert','dir','dirxml','count',
    'trace','group','groupEnd','time','timeEnd','profile','profileEnd'
  ].forEach(function(method) {
      window.console[method] = function(){};
  });
  console.debug = function(){
    if (!TGC.debug) return;
    var out = $('debug-console');
    if (!out) {
      out = new Element('div', {id:'debug-console'});
      var win = TGC.createWindow({
        title: 'Console',
        width:  400,
        height: 150,
        bottom: 0,
        right:  0
      });
      win.getContent().update(out);
      win.show();
    }
    var line = new Element('p');
    line.update($A(arguments).join(' '));
    out.insert(line);
    line.scrollIntoView(false);
  };
  console.time = function(id) {
    if (!TGC.debug) return;
    this._time[id] = new Date();
  };
  console.timeEnd = function(id) {
    if (!TGC.debug) return;
    if (!this._time[id]) return;
    console.debug(id + ' completed in ' + ((new Date())-this._time[id])/1000 + ' seconds.');
    delete this._time[id];
  };
}
// console.profile('TGC initialization');

// script.js, #134
function tick() {
  tickTime();
  for (var res in resis)
    tickRes(resis[res]);

  var i = timers.length;
  while (i--)
    if ( tickTimer(timers[i]) )
      timers.splice(i, 1);
}

// script.js, #570
function insertUnit(input, max) {
  var val = parseInt(input.value,10) || 0;
  if (!max || val >= max) {
    input.value = '';
    return;
  }
  // try to round-down numbers by the thousands/hundreds/tens
  var list = [max];
  for (var i = 10; i <= 1000; i *= 10) {
    if (max > i) list.unshift( Math.floor(max/i) * i );
  }
  for (var i = 0, l = list.length -1; i < l; i++) {
    if (val < list[i]) {
      input.value = list[i];
      return;
    }
  }
  input.value = max;
}

// script.js, #577
insertNumber = insertUnit;

// script.js, #613
function overviewShowLevel() {
  var labels = overviewGetLabels();
  for (var i = 0, l = labels.length; i < l; i++) {
    var label = labels[i];
    if(!label) continue;
    label.style.display = 'inline';
  }
}

// script.js, #622
function overviewHideLevel() {
  var labels = overviewGetLabels();
  for (var i = 0, l = labels.length; i < l; i++) {
    var label = labels[i];
    if(!label) continue;
    label.style.display = 'none';
  }
}

// script.js, #55
function popup(url, width, height) {
  var win = TGC.createWindow({width: (width||400), height: 200});
  new Ajax.Request(url, {
       method: 'GET',
    onSuccess: popupLoaded.curry(win, url),
    onFailure: function(){
      win.setHTMLContent('<p class="error">FAILED!</p>');
      win.close();
    }
  });
  win.showCenter();
}

function popupLoaded(win, url, t) {
  var html = new Element('div').update(t.responseText.scrubHTML());
  TGC._testContent.appendChild(html);
  html.xselect('.//link').invoke('remove');
  TGC.filterPopupContent(url, html);
  var title = html.down('title');
  if (title && title.textContent) {
    win.setTitle(title.textContent.clean().escapeHTML());
    title.remove();
  } else {
    win.setTitle('');
  }
  var main = html.down('table.main');
  if (main) {
    var h2 = main.down('h2');
    if (h2) {
      win.setTitle(h2.textContent.clean().escapeHTML());
      h2.remove();
    }
    TGC.cleanupMain(main);
  }
  win.setHTMLContent( (main || html).innerHTML );
  html.remove();
  win.updateSize();
}

// script.js, #60
popup_scroll = popup;

// script.js, #578
function selectTarget(x, y) {
  var form = TGC.body.down('form[name="units"]');
  if (!form) return;
  form.down('input[name="x"]').value = x;
  form.down('input[name="y"]').value = y;
  Windows.getFocusedWindow().close();
}

// ----------------------------------------------------------------------------

// our "controller"
var TGC = {
  debug: false,

  gameLoaded: function() { // game frame is loaded... do this...
    console.time('TGC::gameLoaded');
    if (this.isHumanRequired()) return; // fast way out

    // yes, bail out if you cannot *really* decide what's what!
    console.time('TGC::gameInit');
    Object.extend(this, this.parseDocument(this.body));
    if (!this.menu || !this.toolbar || !this.main || !this.initGameData()) return;
    console.timeEnd('TGC::gameInit');

    console.time('TGC::cleanupDocument');
    this.cleanupDocument();
    console.timeEnd('TGC::cleanupDocument');

    this.enhanceWindowTitle();

    if (this.screen) { // customizations specific for each "screen" (+ "mode")
      var queueEnhancer = ['enhance','queue',this.screen.dasherize()].join('-').camelize();
      if (this[queueEnhancer]) {
        console.time('TGC::' + queueEnhancer);
        this[queueEnhancer](this.main);
        console.timeEnd('TGC::' + queueEnhancer);
      }
      var enhancer = ['enhance', 'screen', this.screen.dasherize()];
      if (this.mode) enhancer.push(this.mode.dasherize());
      enhancer = enhancer.join('-').camelize();
      if (this[enhancer]) {
        console.time('TGC::' + enhancer);
        this[enhancer](this.main);
        console.timeEnd('TGC::' + enhancer);
      }
    }


    // yes, *after* "screen" (maybe they need JIT gathered data)
    console.time('TGC::enhanceGameMenu');
    this.enhanceGameMenu();
    console.timeEnd('TGC::enhanceGameMenu');
    console.time('TGC::enhanceGameToolbar');
    this.enhanceGameToolbar();
    console.timeEnd('TGC::enhanceGameToolbar');
    console.timeEnd('TGC::gameLoaded');
  },

  parseDocument: function(root) { // A) extract page "sections"
    return {
         menu: root.xselect('.//a[@href="help.php"]/ancestor::table')[0],
      toolbar: root.xselect('.//*[@id="wood"]/ancestor::table')[0],
         main: root.select('table.main').reverse()[0]
    };
  },

  initGameData: function() { // B) initialize page(village) params
    this.args = this.window.location.href.toQueryParams();

    if (!this.args.village) {
      var a = this.body.xselect('.//a[contains(@href, "village=")]')[0];
      if (a) this.args.village = a.href.replace(/^.*?village=(\d+).*$/, '$1');
    }
    if (!this.args.village) return; // could we do better?!

    if (this.args.screen)
      this.screen = this.args.screen;
    if (this.args.mode)
      this.mode = this.args.mode;

    var vnfo = {};

    var va = this.body.down('a[href$="screen=overview"]');
    if (va) {
      vnfo.name = va.textContent.clean();
      var loc = va.next('b'), match;
      if (loc && (match = loc.textContent.match(/\((\d+)\|(\d+)\)\s+(K\d+)/)))
        Object.extend(vnfo, {x: match[1], y: match[2], continent: match[3]});
    }

    ['wood','stone','iron','storage'].each(function(res){
      var span = $(res);
      if (span) vnfo[res] = span.textContent.toInteger();
    },this);

    // var pop = this.toolbar.down('img[src="graphic/face.png"]').up('td').next('td');
    var pop = this.toolbar.xselect('.//img[@src="graphic/face.png"]/ancestor::td[1]/following-sibling::td[1]')[0];
    if (pop) {
      var res = pop.textContent.clean().split('/').map(function(s){return s.toInteger()});
      vnfo.population    = res[0];
      vnfo.maxPopulation = res[1];
    }

    this.village = this.createOrUpdateVillage(this.args.village, vnfo);

    var building = this.getBuilding(this.screen);
    if (building) {
      var hdr = this.main.down('h2'), match;
      if (hdr && (match = hdr.textContent.match(/nivelul\s+(\d+)/i)))
        this.village.buildings[this.screen] = match[1].toInteger();
    }

    return true; // initialization was successful, TGC properties were set
  },

  cleanupDocument: function() { // C) quick and shallow document cleanup!
    // no ads please
    this.body.xselect('.//h4[contains(text(), "Cont-Premium")]')
      .concat(this.main.xselect('.//td[contains(text(), "ca să poţi da mai mult de două comenzi")]'))
      .each(function(el){
        var ad = (el.up('table') || el), next = ad.next();
        if (next && next.tagName.lc() === 'br') next.remove();
        ad.remove();
      });

    // annoying little buggers
    ['body', 'menu', 'toolbar'].each(function(section){
      ['align','width','style'].each(function(attribute){
        if (this[section].hasAttribute(attribute))
          this[section].removeAttribute(attribute);
      },this);
    },this);

    // be brief.
    this.menu.xselect('.//a[text() = "Lista de poziţionare"]')[0].innerHTML = 'Top';

    // cleanup "main" section
    this.cleanupMain();

    // eliminate "mouse watch" annoyance...
    if (!this.screen || this.screen !== 'map')
      this.document.removeEventListener('mousemove', this.window.watchMouse, true);

    // drop more bollocks
    this.body.select('hr, table + br').invoke('remove');

    // D) add (more) CSS hooks
    this.body.addClassName(this.screen || 'unknown');
    this.menu.addClassName('menu').setAttribute('id', 'menu');
    this.toolbar.setAttribute('id', 'toolbar');
    this.main.setAttribute('id', 'main');
  },

  cleanupMain: function(root) {
    var scrub = 'scrubbed-';
    if (!root) {
      root = this.main;
      scrub = '';
    }

    // annoying little buggers
    ['align','width','style'].each(function(attribute){
      if (root.hasAttribute(attribute))
        root.removeAttribute(attribute);
    });

    // be brief.
    root.select('th')
        .filter(function(n){ return n.innerHTML.indexOf('(hh:mm:ss)') > -1 })
        .concat(root.xselect('.//th[contains(text(),"Timp de construcţie")]'))
        .uniq().each(function(th) {
          th.title = th.textContent.strip();
          th.textContent = 'Durată';
        });
    root.xselect('.//td[contains(text(),"astăzi la ora")]').each(function(el) {
      el.innerHTML = el.innerHTML.strip().replace('astăzi ','');
    });
    root.xselect('.//th[contains(text(),"În sat")]').each(function(el) {
      el.up('table').xselect('.//td[' + (el.realCellIndex() + 1) + ']')
          .invoke('setStyle', 'text-align:center');
      el.innerHTML = 'În sat/total';
    });
    root.xselect('.//th[contains(text(),"Întrerupere")]').each(function(el) {
      el.innerHTML = el.innerHTML.replace(/Întrerupere\s*\*?/,
        '<img src="' + this.icons.abort + '" alt="Întrerupere" title="Întrerupere" />');
    }, this);
    root.xselect('.//a[contains(text(),"întrerupe")]').each(function(el) {
      el.innerHTML = el.innerHTML.replace('întrerupe',
        '<img src="' + this.icons.abort + '" alt="întrerupe" title="întrerupe" />');
    }, this);

    // turn "info" links into "popups"
    root.xselect('.//a['
      +     'contains(@href,"screen=info_player")'
      + ' or contains(@href,"screen=info_village")'
      + ' or contains(@href,"screen=info_ally")'
      + ' or contains(@href,"screen=info_command")'
      + ']'
    ).each(function(a){
      a.href = 'javascript:popup("' + a.href + '", 600)';
    });

    // move "server time" out of "main"
    var st = root.down('#' + scrub + 'serverTime');
    if (st) root.insert({ after: st.up('p').addClassName('server-time').remove() });

    // replace vertical sidebar with an horizontal one
    this.replaceSidebar(root);
  },

  // show more details in window's title
  enhanceWindowTitle: function() {
    var details = [ this.village.name.escapeHTML() ];
    if (this.village.x || this.village.y)
      details[0] += ' (' + this.village.x + '|' + this.village.y + ')';
    if (this.screen) {
      var building = this.getBuilding(this.screen);
      details.push( building ? building.name
          : this.screen.split(/[^a-z]+/).invoke('capitalize').join(' ')
      );
    }
    if (this.mode)
      details.push(this.mode.split(/[^a-z]+/).invoke('capitalize').join(' '));
    if (!details.length)
      details[0] = 'Triburile.ro';
    window.top.document.title = details.join(' - ');
  },

  enhanceGameMenu: function() {
    var menu = this.menu, lTD, rTD;
    // all worlds seem to have been updated to this new creepy layout :(
    lTD = menu.down('td').addClassName('left');
    var cols = lTD.nextSiblings();
    rTD = cols.pop();
    rTD.addClassName('right');
    var goRight = false;
    while (!goRight) {
      col = cols.shift();
      lTD.insert(' - ' + col.innerHTML);
      if (col.innerHTML.indexOf('>Top<') > 0) goRight = true;
      col.remove();
    }
    cols.reverse().each(function(col) {
      rTD.insert({top: col.innerHTML + ' - '});
      col.remove();
    });
    // skip useless links
    $w('Logout Premium Chat').each(function(label) {
      var a = menu.xselect('.//a[text() = "' + label + '"]')[0];
      if (a) a.remove();
    });
    lTD.innerHTML = lTD.innerHTML.replace('-  -', '-', 'g');
    // enhanced "overview" link/menu
    var hv = new Element('a', {id:'hoverview', accesskey:'o',
      href: 'game.php?village=' + this.village.id + '&screen=overview_villages'
    });
    hv.update('Overview').observe('click', this.hoverviewListener.bindAsEventListener(this, 'General'));
    lTD.insert({top:hv}).insert({top:'<a href="http://www.triburile.ro/" accesskey="m"><img src="/favicon.ico" width="16" height="16" alt="Triburile" /></a> '});

    var menu = new Element('ul', {'class': 'menu'});
    // [ 'Dezvoltare', 'Resurse', 'Clădiri', 'Populaţie', 'Comerţ', 'Război' ].each(
    [ 'Dezvoltare', 'Resurse', 'Clădiri' ].each(
      function(item){
        var li = new Element('li');
        li.update(item)
          .observe('click', this.hoverviewListener.bindAsEventListener(this, item));
        menu.insert(li);
      }, this
    );
    new Tip(hv, menu,
      { className: 'menu',
        hook:      {target: 'bottomLeft', tip: 'topLeft'},
        hideOn:    false,
        delay:     0.3,
        hideAfter: 0.1
      }
    );
  },

  enhanceGameToolbar: function() {
    var tb = this.toolbar;

    // simpler but more functional "header"
    var hh = tb.down('td');
    hh.setAttribute('id', 'header');
    hh.innerHTML = hh.select('td').reverse().map(function(td) {
      return td.innerHTML.clean();
    }).grep(/\S/).join(' - ');
    hh.innerHTML = hh.innerHTML.clean().replace(/^(<a.+?<\/a>)\s+<b>([^<]+)<\/b>.+?(<a[^>]+>).+$/, '$1 $3$2</a>').replace('_villages', '');

    // add villages "plotter" switch
    this.setPlotterSwitch.bind(this,hh.select('a').reverse()[0]).defer();

    // "Resources toolbar"
    var rtr = tb.down('table.box tr');

    // place "workers" at the same "table" ;-)
    rtr.down('td').nextSiblings('td').reverse()[0].setStyle({borderRight: 'dotted 1px'});
    var wt = tb.down('table.box', 1)
    wt.select('td').each(function(td){ rtr.insert({bottom: td.remove()}); });
    wt.remove();

    // add more tooltips to "resources": building name + level (if available)
    rtr.select('a').each(function(a) {
      var m = a.href.match(/screen=(\w+)/);
      if (!m) return;
      var b = this.getBuilding(m[1]);
      if (!b) return;
      var l = this.village.buildings[b.id] || 0;
      if (l) {
        a.title = b.name + ' (Nivelul ' + l + ')';
        a.select('img').invoke('removeAttribute','title');
      }
    }, this);

    // add links to all "important" buildings
    rtr.down('td').setStyle({borderLeft: 'dotted 1px', paddingLeft: '1ex'});
    this.addToolbarBuildings(rtr, this.village.id, false);

    // add "village (quick)switch"
    var sw = new Element('a', {href: '#', title: 'celelalte sate'});
    sw.addClassName('village-switch').update('▼').observe('click',
      this.toggleVillageSwitch.bindAsEventListener(this)
    );
    hh.insert({top:sw});
  },

  /* BEGIN Screen Enhancers */

  // "Cazarmă"
  enhanceScreenBarracks: function(root) {
    var mt = root.select('table.vis').reverse()[0];
    if (!mt) return;
    mt.addClassName('grid');
    root.xselect('.//th[text()="Necesitate"]').each(this.enhanceNecesitate.bind(this));
  },

  // "Atelier"
  enhanceScreenGarage: function(root) {
    var mt = root.select('table.vis').reverse()[0];
    if (!mt) return;
    mt.addClassName('grid');
    root.xselect('.//th[text()="Necesitate"]').each(this.enhanceNecesitate.bind(this));
  },

  // "Clădirea principală" - "Construire"
  enhanceScreenMainBuild: function(root, destroyMode) {
    var mt = root.select('table.vis').reverse()[0];
    if (!mt) return;
    mt.addClassName('grid');
    mt.up('table').addClassName('elastic');
    mt.select('th').invoke('removeAttribute', 'width');
    if (!destroyMode) {
      mt.select('td.inactive').each(function(td){
        td.innerHTML = td.innerHTML
                .replace('Materii prime disponibile', 'Revino')
                .replace('Materie primă suficientă disponibilă','Stai la coadă');
      });
    }
    var ridx = 1;
    mt.xselect('.//td/a/img').each(function(img) {
      var a = img.parentNode, s = a.href.toQueryParams().screen;
      if (!s) return;
      var b = this.getBuilding(s);
      if (!b) return;
      var na = new Element('a', {
        href:  'javascript:popup("' + b.help + '", 500)',
        title: 'Building Info'
      });
      na.insert(img.remove());
      a.innerHTML = a.textContent.strip();
      a.insert({before: na}).insert({before: ' '});
      a.up('tr').setAttribute('class', 'building-' + s + ' ' + (ridx++ % 2 ? 'row_a' : 'row_b'));
    }, this);

    // CSS hooks
    mt.xselect('.//tr/th[1]')[0].className = 'building';
    if (!destroyMode) {
      mt.xselect('.//tr/th[2]')[0].className = 'materials';
      mt.xselect('.//tr/th[3]')[0].className = 'duration tiny';
      mt.xselect('.//tr/th[4]')[0].className = 'construction';
      mt.xselect('.//tr/td[1]').invoke('addClassName', 'building');
      mt.xselect('.//tr/td[2]').filter(function(td){return !td.hasClassName('inactive')}).invoke('addClassName', 'wood');
      mt.xselect('.//tr/td[3]').invoke('addClassName', 'stone');
      mt.xselect('.//tr/td[4]').invoke('addClassName', 'iron');
      mt.xselect('.//tr/td[5]').invoke('addClassName', 'population');
      mt.xselect('.//tr/td[6]').invoke('addClassName', 'duration');
      mt.xselect('.//tr/td[7]').invoke('addClassName', 'construction');
    } else {
      mt.xselect('.//tr/th[2]')[0].className = 'duration tiny';
      mt.xselect('.//tr/th[3]')[0].className = 'construction';
      mt.xselect('.//tr/td[2]').filter(function(td){return !td.hasClassName('inactive')}).invoke('addClassName', 'duration');
      mt.xselect('.//tr/td[3]').invoke('addClassName', 'construction');
    }
    if (!destroyMode) {
      // drop "necesitate" images
      mt.xselect('.//tr/td/img').invoke('remove');
    }
    // add "index" column
    mt.xselect('.//tr/th')[0].insert({before:'<th class="idx">#</th>'});
    var ridx = 1;
    // FIXME: bottleneck!
    mt.xselect('.//tr/td[1]').each(function(td) {
      td.addClassName('nowrap');
      var tr = td.up('tr');
      tr.insert({top: '<td class="idx">' + (ridx++) + '</td>'});
      var nivel = 0;
      var m = td.innerHTML.clean().match(/\(([^)]+)\)$/);
      if (m) {
        td.innerHTML = td.innerHTML.clean().replace(/\s+\([^)]+\)$/,'');
        m = m[1].match(/^Nivelul\s+(\d+)/i);
        if (m) {
          nivel = m[1].toInteger();
          td.insert(' <b>' + nivel + '</b>');
        }
      }
      var bm = td.up('tr').className.match(/(?:\s|^)building-(\S+)(?:\s|$)/);
      var bld;
      if (bm && (bld = this.getBuilding(bm[1]))) {
        var bi  = bm[1];
        this.village.buildings[bi] = nivel;
        this.addBuildingTip(td,bi,nivel);
        var nextLevel = this.getQueueBuildingLevel(bld) + (destroyMode ? -1 : 1);
        if (bld.levels[nextLevel]) {
          var lastCol = td.nextSiblings().reverse()[0];
          if ( lastCol.textContent.indexOf('Statuia există deja în alt oraş') < 0
              && lastCol.textContent.indexOf('Clădirea complet construită') < 0
              && lastCol.textContent.indexOf('Clădirea nu poate fi demolată mai departe') < 0
          ) {
            this.addBuildingTip(lastCol,bi,nivel,nextLevel);
          }
        }
      }
    }, this);

    if (destroyMode) return;

    // "commify" numbers and highlight "missing" resources
    var status = { population: this.village.maxPopulation - this.village.population };
    $w('wood stone iron storage').each(function(attr) {
      status[attr] = this.village[attr];
    }, this);
    // FIXME: bottleneck!
    $w('wood stone iron population').each(function(id) {
      mt.select('td.' + id).each(function(td){
        var value = td.textContent.toInteger();
        if (!value) {
          td.className = '';
          return;
        }
        var delta = status[id] - value;
        td.innerHTML = value.toLocaleString();
        td.setAttribute('title', delta.toLocaleString());
        if (delta < 0)
          td.addClassName('warning');
        if (id !== 'population' && status.storage <= value)
          td.addClassName('storage-overflow');
      });
    }, this);
  },

  // "Clădirea principală" - "Demolare"
  enhanceScreenMainDestroy: function(root) {
    this.enhanceScreenMainBuild(root,true);
  },

  // "Târg" - "Trimitere de materii prime"
  enhanceScreenMarketSend: function(root) {
    // let transports list flow
    root.select('th[width], td[width], table[width]').invoke('removeAttribute','width');

    // drop lame row breaks
    root.xselect('.//table[@class="vis"]//tr/td[1]//br').invoke('insert',{before:' '}).invoke('remove');

    // enhance "Transports tables"
    var out = root.xselect('.//h3[text() = "Transporturile proprii"]/following-sibling::table[1]')[0];
    if (out) this.enhanceTransportsTable(out, true);
    var inc = root.xselect('.//h3[text() = "Sosesc transporturi"]/following-sibling::table[1]')[0];
    if (inc) this.enhanceTransportsTable(inc, false);
  },

  // "Târg" - "Oferte proprii"
  enhanceScreenMarketOwnOffer: function(root) {
  },

  // "Târg" - "Oferte străine"
  enhanceScreenMarketOtherOffer: function(root) {
  },

  // "Târg" - "Statutul negustorilor"
  enhanceScreenMarketTraders: function(root) {
  },

  // villages overview
  enhanceScreenOverviewVillages: function(root) {
    var t = root.down('table');
    t.hide();
    t.insert({before: this.drawOverview('General', this.parseOverview(t))});
    t.remove();
  },

  // "Piaţa centrală" - "Comenzi"
  enhanceScreenPlaceCommand: function(root) {
    root.select('th[width], td[width], table[width]').each(
      function(el) { el.removeAttribute('width'); }, this
    );
    // add CSS hooks to "Mişcări de trupe"
    root.xselect('.//h3[text() = "Mişcări de trupe"]/following-sibling::table')
        .invoke('addClassName', 'miscari-trupe');
    var no = 1;
    root.select('table.miscari-trupe tr').each(function(tr) {
      // add row "type"
      var img = tr.down('td img');
      if (img) {
        var m = /([^\/]+)\.\w+$/.exec(img.src);
        if (m) tr.addClassName(m[1]);
      }
      // add row numbers
      var ftd = tr.down('td');
      if (ftd) {
        ftd.insert({before: '<td class="idx">' + no.toPaddedString(2) +'</td>'});
        no++;
      } else {
        var fth = tr.down('th');
        if (fth) fth.insert({before: '<th class="idx" width="1">#</th>'});
      }
    }, this);
  },

  // "Piaţa centrală" - "Trupe"
  enhanceScreenPlaceUnits: function(root) {
  },

  // "Piaţa centrală" - "Simulator"
  enhanceScreenPlaceSim: function(root) {
  },

  // Rapoarte -> Toate rapoartele
  enhanceScreenReportAll: function(root) {
    return this.enhanceScreenReportAttack(root);
  },

  // Rapoarte -> Atacuri
  enhanceScreenReportAttack: function(root) {
    if (!this.args.view) return;
    this.enhanceNavigation(root);
    this.enhanceSpyStats(root);
    var myself = this.parseAttackTable(
          root.xselect('.//table//th[text()="Agresor:"]/ancestor::table[1]')[0]
        ),
        enemy  = this.parseAttackTable(
          root.xselect('.//table//th[text()="Apărător:"]/ancestor::table[1]')[0]
        );
    this.addLossCost(myself);
    this.addLossCost(enemy);
    this.addAttackLinks(myself, enemy);

    // proper "booty" display
    var btd = root.xselect('.//table//th[text()="Pradă:"]/following-sibling::td[1]')[0];
    if (btd) {
      var booty = btd.textContent.clean().split(' ').map(function(s){return s.toInteger()});
      if (booty.length == 3) {
        var idx = 0;
        $w('wood stone iron').each(function(res) {
          btd.insert({before:
            ' <td class="' + res + '">'
            + booty[idx++].toLocaleString()
            + '</td>'
          });
        });
        btd.up('table').addClassName('grid');
        btd.remove();
      }
    }
  },

  // Rapoarte -> Apărare
  enhanceScreenReportDefense: function(root) {
    if (!this.args.view) return;
    this.enhanceNavigation(root);
    var myself = this.parseAttackTable(
          root.xselect('.//table//th[text()="Agresor:"]/ancestor::table[1]')[0]
        ),
        enemy  = this.parseAttackTable(
          root.xselect('.//table//th[text()="Apărător:"]/ancestor::table[1]')[0]
        );
    this.addLossCost(myself);
    this.addLossCost(enemy);
  },

  // Rapoarte -> Sprijin
  enhanceScreenReportSupport: function(root) {
    if (!this.args.view) return;
    this.enhanceNavigation(root);
  },

  // Rapoarte -> Comerţ
  enhanceScreenReportTrade: function(root) {
    if (!this.args.view) return;
    this.enhanceNavigation(root);
  },

  // Rapoarte -> Altele
  enhanceScreenReportOther: function(root) {
    if (!this.args.view) return;
    this.enhanceNavigation(root);
  },

  // Rapoarte -> Trimis mai departe
  enhanceScreenReportForwarded: function(root) {
    if (!this.args.view) return;
    this.enhanceNavigation(root);
  },

  // Rapoarte -> Filtru
  enhanceScreenReportFilter: function(root) {
  },

  // Rapoarte -> Blochează expeditorul
  enhanceScreenReportBlock: function(root) {
  },

  // incoming mail
  enhanceScreenMailIn: function(root) {
    if (this.args.view) {
      var units   = $H(this.getUnits()),
          pre     = '<table class="vis"><tr>',
          post    = '</tr></table>',
          unitsRE = new RegExp( '(^|>|\\n)'
                                + ('(\\d+)\\s+').times(units.size()-1)
                                + '(\\d+)($|\\s*<|\\n)',
                                'g'
                              );
      units.each(function(u){
        pre += '<th align="center" width="35"><img src="'
            + u.value.icon + '" title="'
            + u.value.name + '" /></th>';
      });
      pre += '</tr><tr>';
      root.select('div.post').each(function(div){
        div.innerHTML = div.innerHTML.replace(unitsRE,function(){
            var numbers = Array.splice(arguments, 1, units.size() + 2),
                suffix  = numbers.pop(),
                ret     = numbers.shift();
            for (var i = 0; i < units.size(); i++)
              ret += '<td>'
                  + parseInt(numbers[i] || 0).toLocaleString()
                  + '</td>';
            return pre + ret + post + suffix;
          });
      });
    }
  },

  // "Fierărie"
  enhanceScreenSmith: function(root) {
    var mt = root.select('table.vis').reverse()[0];
    if (!mt) return;
    mt.addClassName('grid');
  },

  // "Curte nobilă"
  enhanceScreenSnob: function(root) {
    root.select('table.vis').invoke('addClassName', 'grid');
    root.xselect('.//th[text()="Necesitate"]').each(this.enhanceNecesitate.bind(this));
  },

  // "Grajd"
  enhanceScreenStable: function(root) {
    var mt = root.select('table.vis').reverse()[0];
    if (!mt) return;
    mt.addClassName('grid');
    root.xselect('.//th[text()="Necesitate"]').each(this.enhanceNecesitate.bind(this));
  },

  // "Magazie"
  enhanceScreenStorage: function(root) {
  },

  /* END Screen Enhancers */

  /* BEGIN Queue Enhancers */

  enhanceQueueContainer: function(table) {
    table.addClassName('queue grid').select('th[width]').invoke('removeAttribute', 'width');
    table.xselect('.//tr/th[1]').invoke('addClassName','command');
    table.xselect('.//tr/th[2]').invoke('addClassName','duration tiny');
    table.xselect('.//tr/th[3]').invoke('addClassName','eta');
    table.xselect('.//tr/th[4]').invoke('addClassName','abort tiny');
    var notice = table.next('div[style]');
    if (notice) {
      var nRow = new Element('tr'),
          nCell = new Element('td', {
            'style': 'text-align:right;' + notice.getAttribute('style'),
            colspan: table.tBodies[0].rows[0].cells.length
          });
      nCell.update(notice.innerHTML);
      nRow.insert(nCell);
      table.insert(nRow);
      notice.remove();
    }
  },

  enhanceQueueBarracks: function(root) {
    var t = root.xselect('.//th[text()="Instrucţie"]/ancestor::table[1]')[0];
    if (!t) return;
    this.enhanceQueueContainer(t);
    var q = this.village.queue[this.screen] = [];
    var d = new Date();
    for (var i = 1, l = t.tBodies[0].rows.length; i < l; i++) {
      var r = t.tBodies[0].rows[i],
          m = r.cells[0].textContent.clean().match(/^(\d+)\s+(.+?)$/);
      if (!m) continue;
      var u = this.getUnitByName(m[2]);
      if (!u) continue;
      d.setSeconds( r.cells[1].textContent.clean().toSeconds() );
      q.push({
        ref: u,
        num: m[1].toInteger(),
        end: d.getTime()
      });
      r.cells[0].insert({top: '<img src="' + u.icon + '" /> '});
    }
  },

  enhanceQueueMain: function(root) {
    // take care, may contain "destroy" commands!
    var t = root.xselect('.//th[text()="Comandă de construcţie"]/ancestor::table[1]')[0];
    if (!t) return;
    this.enhanceQueueContainer(t);
    var q = this.village.queue[this.screen] = [];
    var d = new Date();
    for (var i = 1, l = t.tBodies[0].rows.length; i < l; i++) {
      var r = t.tBodies[0].rows[i], text = r.cells[0].textContent.clean(),
          building, level;
      var m = text.match(/^(.+?)\s+\(nivelul\s+(\d+)\)$/i);
      if (m) {
        building = this.getBuildingByName(m[1]);
        level    = m[2].toInteger();
      } else {
        m = text.match(/^Demolare\s+(.+?)$/i);
        if (m)        building = this.getBuildingByName(m[1]);
        if (building) level    = this.getDestroyBuildingLevel(building);
      }
      if (!building) continue;
      d.setSeconds( r.cells[1].textContent.clean().toSeconds() );
      q.push({ ref: building, num: level, end: d.getTime() });
      r.cells[0].insert({top: '<img src="' + building.icon + '" /> '});
      this.addBuildingTip(
        $(r.cells[0]),
        building.id,
        this.village.buildings[building.id],
        level
      );
    }
  },

  enhanceQueueSmith: function(root) {
    var t = root.xselect('.//td[text()="Comandă de cercetare"]/ancestor::table[1]')[0];
    if (!t) return;
    this.enhanceQueueContainer(t);
    var q = this.village.queue[this.screen] = [];
    var d = new Date();
    var aliases = {
      'Lance': 'spear',
      'Spadă': 'sword',
      'Topor': 'axe',
      'Arc':   'archer'
    };
    for (var i = 1, l = t.tBodies[0].rows.length; i < l; i++) {
      var r = t.tBodies[0].rows[i];
      d.setSeconds( r.cells[1].textContent.clean().toSeconds() );
      var n = r.cells[0].textContent.clean();
      var u = aliases[n] ? this.getUnit(n) : this.getUnitByName(n);
      if (!u) continue;
      q.push({
        ref: u,
        num: 1,
        end: d.getTime()
      });
      r.cells[0].insert({top: '<img src="' + u.icon + '" /> '});
    }
  },

  /* END Queue Enhancers */

  replaceSidebar: function(root) {
    // go slow (step by step) in order to avoid replacing "deeper" structures
    var h2 = root.down('h2');
    if (!h2) return;
    var table = h2.next('table');
    if (!table) return;
    // var vis = table.down('table[width="100"]');
    var vis = table.down('table.vis');
    if (!vis || vis.up('tr').cells.length != 2) return;

    var barTable = new Element('table', {'class':'vis toolbar'}),
        bar = new Element('tr');
    barTable.insert(bar);
    var cont = new Element('div', {'class':'content'});
    table.insert({before: barTable}).insert({after: cont});

    vis.up('td').next('td').immediateDescendants().each(function(el){
      el.removeAttribute('width');
      var xt = [];
      switch (el.tagName.uc()) {
        case 'FORM':
          el.immediateDescendants().each(function(t){
            if (t.tagName.uc() === 'TABLE') xt.push(t);
          });
          break;
        case 'TABLE':
          xt.push(el);
          break;
      }
      if (xt.length > 0) {
        xt.invoke('removeAttribute', 'width');
        xt.invoke('addClassName', 'elastic');
        xt.each(function(t) {
          $A(t.down('tr').cells).invoke('removeAttribute','width');
        });
      }
      cont.insert(el.remove());
    });

    vis.select('a').each(function(el){
      var td = el.up('td');
      td.removeAttribute('width');
      bar.insert(td.remove());
    });

    table.remove();

    if (h2.textContent == 'Rapoarte' || h2.textContent == 'Mesaje')
      h2.remove();

  },

  enhanceNavigation: function(root) {
    root.xselect('.//a[text()="<<"]').invoke('setAttribute', 'accesskey', ',');
    root.xselect('.//a[text()=">>"]').invoke('setAttribute', 'accesskey', '.');
  },

  addAttackLinks: function(myself, enemy) {
    if (!myself || !enemy) return;
    var iAmDefending = this.isMyVillage(enemy.village.id);

    if (iAmDefending) {
      var swap = myself;
      myself = enemy; // truth thou said ;-)
      enemy = swap;
    }

    var tr = new Element('tr');
    tr.addClassName('reactions');
    tr.insert('<td>(Re)acționează:</td>');
    var td = new Element('td');
    tr.insert(td);
    (enemy.table.down('tbody') || enemy.table).insert(tr);

    var note = new Element('b', {'class':'attack-note'});

    var anewArgs = new Hash({
       screen: 'place',
         mode: 'command',
      village: myself.village.id,
       target: enemy.village.id
    });
    var anew = new Element('a', {
      'class': 'attack-custom',
        title: 'atacă ' + enemy.village.name.escapeHTML(),
         href: 'game.php?' + anewArgs.toQueryString()
    });
    anew.update('atacă').observe('click', this.triggerAttack.bindAsEventListener(this));
    td.insert(anew);
    if (iAmDefending) {
      td.insert(' pe <a href="game.php?village='
        + myself.village.id
        + '&amp;screen=info_player&amp;id='
        + enemy.player.id
        + '">' + enemy.player.name.escapeHTML() + '</a>  '
      ).insert(note);
      anew.focus();
      return;
    }

    var confirmAL = this.confirmAttack.bindAsEventListener(this);

    var asameArgs = new Hash({
       screen: 'place',
      village: myself.village.id,
       target: enemy.village.id,
        'try': 'confirm',
            x: enemy.village.x,
            y: enemy.village.y,
       attack: 'Atac'
    });
    var asame = new Element('a', {
      'class': 'attack-same',
      title: 'atacă ' + enemy.village.name.escapeHTML() + ' cu aceleași trupe ca și data trecută',
      href:  'game.php?' + asameArgs.merge(myself.units).toQueryString()
    });
    asame.update('cu aceleași trupe').observe('click', confirmAL);
    td.insert(' … ').insert(asame);

    asameArgs.set('village',this.village.id);
    var asamehere = new Element('a', {
      'class': 'attack-same-here',
      title: 'atacă ' + enemy.village.name.escapeHTML() + ' cu aceleași trupe ca și data trecută DIN satul CURENT',
      href:  'game.php?' + asameArgs.merge(myself.units).toQueryString()
    });
    asamehere.update('de *aici*').observe('click', confirmAL);
    td.insert(' … ').insert(asamehere);

    td.insert('  ').insert(note);
    asame.focus();
  },

  triggerAttack: function(ev) {
    var a = Event.element(ev);
    Event.stop(ev);
    a.blur();
    this.body.down('b.attack-note').update('');
    var win = this.createWindow({title: 'Atac'});
    new Ajax.Request(a.href, {
            method: 'GET',
         onSuccess: this.attackFormLoaded.bind(this,win),
         onFailure: function(){
            win.setHTMLContent('<p class="error">FAILED!</p>');
            win.close();
         }
      }
    );
    win.showCenter();
  },
  attackFormLoaded: function(win, t) {
    var html = new Element('div').update(t.responseText.scrubHTMLBody());
    this._testContent.appendChild(html);
    var form = html.down('form[name="units"]');
    if (!form) {
      win.close();
      alert('request failed');
      return;
    }
    form.observe('submit', this.attackFormSubmitted.bindAsEventListener(this,win));
    form.select('a[href*="targets.php"], input.support').invoke('remove');
    win.getContent().update(form);
    html.remove();
    win.updateSize();
    win.getContent().down('input[name="axe"]').activate();
  },
  attackFormSubmitted: function(ev, win) {
    var form = Event.element(ev);
    if (form.tagName.lc() !== 'form') { // submitted through keyboard event?
      form = form.up('form');
    }
    Event.stop(ev);
    new Ajax.Request(
      form.getAttribute('action'),
      {     method: form.getAttribute('method'),
        parameters: form.serialize(),
         onSuccess: this.attackLaunch.bind(this,win),
         onFailure: function(){
           alert('request failed');
         }.bind(this)
      }
    );
  },

  confirmAttack: function(ev) {
    var a = ev.element(), args = a.href.toQueryParams();
    a.blur();
    ev.stop();
    this.body.down('b.attack-note').update('');
    var win = this.createWindow({title: 'Atac'});
    new Ajax.Request(
      'game.php?village=' + args.village + '&screen=place&try=confirm',
      {     method: 'POST',
        parameters: args,
         onSuccess: this.attackLaunch.bind(this,win),
         onFailure: function(){
            win.setHTMLContent('<p class="error">FAILED!</p>');
            win.close();
         }
      }
    );
    win.showCenter();
  },

  attackLaunch: function(win, t) {
    var html = new Element('div').update(t.responseText.scrubHTMLBody());
    this._testContent.appendChild(html);
    if (html.xselect('.//th[text() = "Poruncă"]').length != 1) {
      var error = html.xselect('.//div[contains(@style, "color: red")]')[0];
      this.body.down('b.attack-note').update(
        error ? '<span class="warn">' + error.textContent.escapeHTML() + '</span>'
              : 'N-ai cu ce!'
      );
      win.close();
      this.body.down('a.attack-same').focus();
      return;
    }
    var form = html.down('form');
    if (!form) {
      win.close();
      alert('request failed');
      return;
    }
    form.observe('submit', this.attackLaunched.bindAsEventListener(this));

    var table = form.up('table');
    table.removeAttribute('width');
    table.select('table').invoke('setStyle', 'width: 100%');

    // CSS hook for enemy player row
    var en = table.xselect('.//td[text() = "Jucător:"]/..')[0];
    if (en) en.addClassName('enemy');

    table.down('p[align="right"]').remove(); // drop "generată în ..."
    win.getContent().update(table);
    html.remove();
    win.updateSize();
    win.getContent().down('input[type="submit"]').focus();
  },

  attackLaunched: function(ev) {
    var form = Event.element(ev);
    if (form.tagName.lc() !== 'form') { // submitted through keyboard event?
      form = form.up('form');
    }
    Event.stop(ev);
    new Ajax.Request(
      form.getAttribute('action'),
      {     method: form.getAttribute('method'),
        parameters: form.serialize(),
         onSuccess: function(){
           var note = this.body.down('b.attack-note');
           if (note) note.update('Done.');
           Windows.getFocusedWindow().close();
           var sterge = this.body.xselect('//td/a[text()="Şterge"]')[0];
           if (sterge) sterge.focus();
           if (!note) alert('Atacul a fost lansat.');
         }.bind(this),
         onFailure: function(){
           alert('request failed');
         }.bind(this)
      }
    );
    return false;
  },

  parseAttackTable: function(table) {
    if (!table) return;
    var player = table.xselect('.//th[2]/a')[0],
        args = player ? player.href.toQueryParams() : {};
    player = player ? { id: args.id, name: player ? player.textContent.clean() : ''} : {};

    var village = table.xselect('.//tr/td[text()="Sat:"]/following-sibling::td/a')[0],
        args    = village.href.toQueryParams(),
        m       = /^(.+?)\s+\((\d+)\|(\d+)\)\s+(K\d+)$/.exec(village.textContent.clean());
    village = {
        id:   args.id,
        name: m[1],
        x:    m[2],
        y:    m[3],
        a:    m[4]
      };

    var units     = {},
        lostUnits = {},
        lossCost  = {wood: 0, stone: 0, iron: 0, units: 0},
        rows      = table.select('table tr');
    if (!rows.length) return;
    var len       = rows[0].cells.length;
    for (var i = 1; i < len; i++) {
      var n = rows[0].cells[i].down('img').src.replace(/^.+_(\w+)\.png$/, '$1');
      units[n]     = rows[1].cells[i].innerHTML.toInteger();
      lostUnits[n] = rows[2].cells[i].innerHTML.toInteger();
      if (lostUnits[n] > 0) {
        rows[2].cells[i].className = 'error';
        var un = this.getUnit(n); 
            lw = (un.wood  * lostUnits[n]),
            ls = (un.stone * lostUnits[n]),
            li = (un.iron  * lostUnits[n]);
        rows[2].cells[i].title = [
          lw.toLocaleString() + ' lemn',
          ls.toLocaleString() + ' argilă',
          li.toLocaleString() + ' fier'
        ].join('; ');
        lossCost.wood  += lw;
        lossCost.stone += ls;
        lossCost.iron  += li;
        lossCost.units += lostUnits[n];
      }
    }

    return {
          table: table,
         player: player,
        village: village,
          units: units,
      lostUnits: lostUnits,
       lossCost: lossCost
    };
  },

  addLossCost: function(aStats) {
    if (!aStats) return;
    // FIXME: what about defending "empty" towns?
    if (aStats.lossCost.units < 1) return;
    var tr = new Element('tr');
    tr.addClassName('loss-cost');
    tr.insert('<td>Cost:</td>');
    var td = new Element('td');
    tr.insert(td);
    var lctable = new Element('table',{'class':'grid loss-cost'}),
        lctr    = new Element('tr');
    lctable.insert(lctr);
    td.insert(lctable);
    for (at in aStats.lossCost) {
      var sp = new Element('td');
      sp.addClassName(at == 'units' ? 'population' : at);
      sp.title = at;
      sp.update(aStats.lossCost[at].toLocaleString());
      lctr.insert(sp);
    }
    (aStats.table.down('tbody') || aStats.table).insert(tr);
  },

  enhanceSpyStats: function(root) {
    var table = root.xselect('.//h4[text()="Spionaj"]/following-sibling::table')[0];
    if (!table) return;
    table.addClassName('spy-stats');

    // minimize space = tables' borders do a good enough job
    table.siblings().filter(function(el){return el.tagName.lc() === 'br'}).invoke('remove');

    // flex container
    // table.up('table[width]').removeAttribute('width');
    table.up('table').down('th[width="400"]').removeAttribute('width');

    // split spy stats in a two-columns display
    var tc = new Element('table');
    tc.style.width = '100%';
    var tcr = new Element('tr'), col1 = new Element('td'), col2 = new Element('td');
    col1.style.width = col1.style.width = '50%';
    tc.insert(tcr.insert(col1).insert(col2));
    col2.insert( table.previous('h4').remove() );
    table.siblings().each(function(el){ col1.insert(el.remove()) });
    table.up().insert(tc);
    col2.insert( table.remove() );

    // Current resources
    var resources = { wood: 0, stone: 0, iron: 0, total: 0 };

    // Resurse
    var thr = table.xselect('.//th[text()="Resurse spionate:"]')[0];
    if (thr) {
      thr.update('Resurse:');
      var tdr = thr.next('td'), match;
      if (tdr && (match = tdr.textContent.match(/([0-9,.]+)\s+([0-9,.]+)\s+([0-9,.]+)/))) {
        resources.wood  = match[1].toInteger();
        resources.stone = match[2].toInteger();
        resources.iron  = match[3].toInteger();
        tdr.update('');
        var total = 0;
        $w('wood stone iron').each(function(res) {
          tdr.insert('<span class="' + res + '">' + resources[res].toLocaleString() + '</span> ');
          resources.total += resources[res];
        }, this);
        if (resources.total > 0) {
          tdr.insert(' (<span class="total">' + resources.total.toLocaleString() + '</span>)');
        }
      }
    }

    // Clădiri
    var thc = table.xselect('.//th[text()="Clădiri:"]')[0], tdc, totalPoints = 0;
    if (thc) {
      thc.setStyle('vertical-align:top');
      tdc = thc.next('td');
    }
    if (!tdc) return;
    var ct = new Element('table', {'class': 'vis grid', summary: 'spy stats'});
    ct.insert('<tr>\
      <th class="icon"></th>\
      <th class="building">Clădire</th>\
      <th class="level" title="Nivel">N</th>\
      <th class="points" title="Puncte">P</th>\
      <th class="bonus" title="Bonus">B</th>\
    </tr>');
    var stats = {};
    tdc.innerHTML.clean().split('<br>').grep(/\S/).each(function(line){
      var row = new Element('tr');
      ct.insert(row);
      var m = line.clean().match(/^(.+?)\s+<b>\(nivelul\s+(\d+)\)<\/b>/);
      if (!m) return row.update('<td colspan="5">' + line + '</td>');
      var name  = m[1],
          level = parseInt(m[2]),
          bld   = this.getBuildingByName(name);
      if (!bld) return row.update('<td colspan="5">' + name + ', #<b>' + level + '</b>' + '</td>');
      var id = bld.id, points = bld.levels[level].points || 0;
      totalPoints += points;
      stats[id] = level;
      row.insert(
        '<td class="icon"><img src="' + bld.icon + '" width="16" height="16" ></td>'
        + '<td class="building building-' + id + '">' + name + '</td>'
        + '<td class="level">' + level + '</td>'
        + '<td class="points">' + points + 'p</td>'
      );
      var bns = this.getBonus(id);
      if (bns) {
        row.insert(
          '<td class="bonus number" title="'
          + bns.desc
          + '">'
          + bns.template.interpolate({bonus: bld.levels[level].bonus.toLocaleString()})
          + '</td>'
        );
      } else {
        row.insert('<td></td>');
      }
    }, this);
    if (totalPoints > 0) {
      thc.insert('<br />(' + totalPoints.toLocaleString() + 'p)');
    }
    tdc.update(ct);

    // Optimal Farming :: FIXME - add the time necessary to fill the booty!
    if (stats.storage && (stats.wood || stats.stone || stats.iron)) {
      var ftr = new Element('tr'), ftd = new Element('td');
      ftr.insert('<th style="vertical-align:top">Farming:</th>').insert(ftd);
      tdc.up('tr').insert({after:ftr});
      var tfarming = new Element('table', {'class': 'vis grid', summary: 'farming prognosis'});
      tfarming.insert('<tr>\
        <th class="timing">Când</th>\
        <th class="booty">Cât</th>\
        <th class="troops">Cine</th>\
      </tr>');
      ftd.insert(tfarming);

      var units = this.getUnits();

      // FIXME: useless, replace with "if you attack now" ("now" + "time to get there")
      if (resources.total) {
        tfarming.insert('<tr class="now">'
          + '<td class="timing">now</td>'
          + '<td class="booty">' + resources.total.toLocaleString() + '</td>'
          + '<td class="troops">'
          + Math.ceil( resources.total / units.light.booty ).toLocaleString()
          + ' <img src="' + units.light.icon + '" width="16" height="16" title="'
          + units.light.name + '" />'
          + '</td>'
          + '</tr>'
        );
      }

      // FIXME: compute "when" + add more units + attack link
      var mines = (stats.wood?1:0) + (stats.stone?1:0) + (stats.iron?1:0);
      var maxBooty = this.getBuilding('storage').levels[stats.storage].bonus;
      if (stats.hide)
        maxBooty -= this.getBuilding('hide').levels[stats.hide].bonus;
      maxBooty *= mines;
      tfarming.insert('<tr class="max">'
        + '<td class="timing">max</td>'
        + '<td class="booty">' + maxBooty.toLocaleString() + '</td>'
        + '<td class="troops">'
        + Math.ceil( maxBooty / units.light.booty ).toLocaleString()
        + ' <img src="' + units.light.icon + '" width="16" height="16" title="'
        + units.light.name + '" />'
        + '</td>'
        + '</tr>'
      );
    }
  },

  filterPopupContent: function(url, root) {
    var args = url.toQueryParams();
    var path = url.replace(/\?.*$/,'');
    if (path == 'targets.php' && args.mode == 'own') {
      if (args.building == 'market') {
        this.enhanceOwnTargetsMarket(root);
      } else {
        this.enhanceOwnTargets(root);
      }
    }
    if (path == 'popup_building.php') {
      root.select('th[width]').invoke('removeAttribute', 'width');
      var thlist = root.xselect('.//th[text()="Necesitate"]');
      if (thlist.length > 0) {
        thlist[0].up('table').addClassName('grid');
        thlist.each(this.enhanceNecesitate.bind(this));
      }
      root.xselect('.//th[text()="Muncitori pentru nivelul/total"]')
          .each(this.enhanceMuncitori.bind(this));
    }
  },

  // cannot be used to populate game data as no village ID is provided!
  enhanceOwnTargetsMarket: function(root) {
    var rows = [];
    root.xselect('.//a[contains(@href,"javascript:selectTarget")]')
      .each(function(a){
        var coords = a.textContent.clean().split('|');
        var td = a.up('td').previous();

        td.addClassName('storage').update(td.textContent.clean().toInteger().toLocaleString());

        td = td.previous();
        var tdn = td.previous();

        var res = td.textContent.clean().split(' ').map(function(s){return s.toInteger()});
        td.addClassName('stone')
          .update(res[1].toLocaleString())
          .insert({before: '<td class="wood">' + res[0].toLocaleString() + '</td>'})
          .insert({after:  '<td class="iron">' + res[2].toLocaleString() + '</td>'});

        var name = tdn.textContent.clean();
        tdn.remove();

        a.up('td').insert({before: '<td class="eta">'
          + this.getDuration( 'merchant',
              {x: this.village.x, y: this.village.y},
              {x: coords[0], y: coords[1]}
            )
          + '</td>'
        });

        a.update(name.escapeHTML() + ' (' + coords[0] + '|' + coords[1] + ')');
        rows.push({
          tr:   a.up('tr'),
          x:    coords[0],
          y:    coords[1],
          name: name
        });
    }, this);
    rows.sort(function(a,b){ // sort villages by name and coordinates
      var va = a.name.lc(),
          vb = b.name.lc();
      return va  < vb  ? -1 : va > vb   ? 1 :
             a.x < b.x ? -1 : a.x > b.x ? 1 :
             a.y < b.y ? -1 : a.y > b.y ? 1 : 0;
    });
    var t = rows[0].tr.up('table');
    t.addClassName('grid');
    rows.each(function(v){ t.insert(v.tr.remove()); });
    root.update(t);
  },

  // cannot be used to populate game data as no village ID is provided!
  enhanceOwnTargets: function(root) {
    var rows = [];
    root.xselect('.//a[contains(@href,"javascript:selectTarget")]')
      .each(function(a){
        var coords = a.textContent.clean().split('|');
        var td = a.up('td').previous();
        var name = td.textContent.clean();
        td.remove();
        a.update(name + ' (' + coords[0] + '|' + coords[1] + ')');
        rows.push({
          tr:   a.up('tr'),
          x:    coords[0],
          y:    coords[1],
          name: name
        });
    });
    rows.sort(function(a,b){ // sort villages by name and coordinates
      var va = a.name.lc(),
          vb = b.name.lc();
      return va  < vb  ? -1 : va > vb   ? 1 :
             a.x < b.x ? -1 : a.x > b.x ? 1 :
             a.y < b.y ? -1 : a.y > b.y ? 1 : 0;
    });
    var t = rows[0].tr.up('table');
    t.addClassName('grid');
    rows.each(function(v){ t.insert(v.tr.remove()); });
    root.update(t);
  },

  addToolbarBuildings: function(tr, vid, after) {
    var tb = this.toolbarBuildings.clone();
    if (!after) tb.reverse();
    var v = this.createOrUpdateVillage(vid);
    tb.each(function(id){
      var b = this.getBuilding(id),
          l = v.buildings[id] || 0,
          a = new Element('a', { href: '/game.php?village=' + vid + '&screen=' + id }),
          i = new Element('img', {src:b.icon, width:16, height:16, alt:id}),
          t = new Element('td');
      if (!l)
        t.addClassName('not-found');
      if (!l || !this.addBuildingTip(t,id,l))
        t.writeAttribute('title', b.name);
      t.addClassName('building').insert(a.insert(i));
      if (this.village.id == v.id && this.screen && this.screen == id)
        t.addClassName('selected');
      tr.insert(after ? t : {top: t});
    }, this);
  },

  // FIXME: obsolete? or just unused? ;-)
  enhanceGrids: function(){
    this.body.xselect('.//tr/th/..').each(function(tr) {
      var table = tr.up('table'), icg = 1, icol = 1;
      if (table.down('colgroup')) return;
      tr.select('th').each(function(th){
        var cg = new Element('colgroup');
        cg.addClassName('cg' + icg++);
        var cols = th.hasAttribute('colspan') ? th.getAttribute('colspan') : 1;
        for (var i = 0; i < cols; i++) {
          var col = new Element('col');
          col.addClassName('col' + icol++);
          cg.insert(col);
        }
        table.insert(cg);
      });
    });
  },

  toggleVillageSwitch: function(ev) {
    var sw = ev.element();
    ev.stop();
    var swl = $('village-switch-list');
    if (!swl) swl = this.createVillageSwitch(sw);
    swl.toggle();
  },

  createVillageSwitch: function(sw) {
    var villages = this.getVillages(),
        vsm      = this.getVillagesList();
    var swl = new Element('table', {id: 'village-switch-list', 'class':'vis'});
    var offset = sw.cumulativeOffset();
    swl.style.top  = (offset.top + sw.getHeight()) + 'px';
    swl.style.left = offset.left + 'px';

    var idx = 1;
    vsm.each(function(id){
      var row = new Element('tr').addClassName(idx % 2 ? 'row_a' : 'row_b');
      swl.insert(row);
      row.insert('<td class="idx">' + (idx++) + '</td>');
      var vtd = new Element('td', {'class':'village'});
      vtd.insert(this.getSwitchVillageLink(id));
      row.insert(vtd);
      this.addToolbarBuildings(row, id, true);
    }, this);

    this.body.insert(swl.hide());
    return swl;
  },

  getSwitchVillageLink: function(vid,opt) {
    var village = this.createOrUpdateVillage(vid),
        args    = Object.extend({},this.args);
    args.village = vid;
    Object.extend(args, opt || {});
    if ('action' in args) delete args.action;
    var a = vid == this.village.id ? new Element('strong') : new Element('a',
      {href: 'game.php?' + Object.toQueryString(args), 'class': 'village'});
    var label = village.name.escapeHTML();
    if (village.x && village.y)
      label += ('<span class="coords"> (#{x}|#{y})</span>').interpolate(village);
    a.update(label);
    var s = args.screen;
    if (s) {
      var bld = this.getBuilding(s);
      a.title = village.name.escapeHTML() + ' → ' + ( bld ? bld.name :
        s.split(/[^a-z]+/).invoke('capitalize').join(' ') );
    }
    return a;
  },

  parseOverview: function(t) {
    var hints = {}; // add here "images" ("under attack", "support", etc)
    var villages = {}; // used for dropping lost villages
    var totals = {
      points:0, wood:0, stone:0, iron:0, storage:0,
      population:0, maxPopulation:0
    };
    t.xselect('.//tr/td/..').each(function(tr){
      var a = tr.down('a'), match, args = a.href.toQueryParams();
      var resA = tr.cells[2].textContent.clean().split(' ')
                                    .map(function(s){return s.toInteger()});
      var popA = tr.cells[4].textContent.clean().split('/')
                                    .map(function(s){return s.toInteger()});
      var village = {
        id:            args.village,
        name:          a.textContent.clean(),
        points:        tr.cells[1].textContent.toInteger(),
        wood:          resA[0],
        stone:         resA[1],
        iron:          resA[2],
        storage:       tr.cells[3].textContent.toInteger(),
        population:    popA[0],
        maxPopulation: popA[1]
      };

      hints[village.id] = [];
      a.select('img').each(function(img){
        hints[village.id].push(img.remove());
      });

      // old code, these bits are not available anymore on L3/L4 :(
      if (match = village.name.match(/^(.+?)\s+\((\d+)\|(\d+)\)\s+(.+)$/)) {
        village.name      = match[1];
        village.x         = match[2];
        village.y         = match[3];
        village.continent = match[4];
      }

      for (attr in totals)
        totals[attr] += village[attr];

      villages[village.id] = this.createOrUpdateVillage(village.id, village);
    }, this);
    this.setMyVillages(villages);
    return {totals: totals, hints: hints};
  },

  drawOverview: function(type, opt) {
    var wrapper  = new Element('div',{'class':'overview-wrap'}),
        scroller = new Element('div',{'class':'overview-scroll'}),
        grid     = new Element('table', {
          id:    'vov-' + type,
          class: 'vis grid overview',
          summary: 'Privire de ansamblu: ' + type
        });
    if (!this._drawOverview[type]) return wrapper;
    return this._drawOverview[type].apply(this,[grid, opt]);
  },

  _drawOverview: {
    'General': function(grid, opt) {
      grid.insert(
       '<thead>\
            <tr>\
              <th class="idx" rowspan="2">#</th>\
              <th class="village" rowspan="2">Sat</th>\
              <th class="resources tiny" colspan="3">Resurse</th>\
              <th class="storage tiny" rowspan="2">Magazie</th>\
              <th class="population tiny" colspan="3" rowspan="2">Populație</th>\
              <th class="points tiny" rowspan="2">Puncte</th>\
            </tr>\
            <tr>\
              <th class="wood tiny">Lemn</th>\
              <th class="stone tiny">Argilă</th>\
              <th class="iron tiny">Fier</th>\
            </tr>\
          </thead>\
          <tfoot>\
            <tr>\
              <td colspan="2"></td>\
              <td class="number wood"></td>\
              <td class="number stone"></td>\
              <td class="number iron"></td>\
              <td class="number storage"></td>\
              <td class="number current-population"></td>\
              <td class="separator">/</td>\
              <td class="number population"></td>\
              <td class="number points"></td>\
            </tr>\
          </tfoot>\
          <tbody></tbody>'
      );
      var idx = 1;
      this.getVillagesList().each(function(id){
        var village = this.getVillage(id);
        var row = new Element('tr', {'class': 'row_' + (idx % 2 ? 'a' : 'b')
                                                          + ' village-' + id});
        row.insert('<td class="idx">' + idx + '</td>');

        var td = new Element('td', {'class': 'village'});
        row.insert(td);
        if (opt.hints[id])
          opt.hints[id].each(function(img){ td.insert(img).insert(' '); });
        td.insert(this.getSwitchVillageLink(id));

        var sWarn = village.storage * 0.95,
            sFull = village.storage;
        ['wood','stone','iron'].each(function(res){
          var td = new Element('td', {'class': 'number ' + res});
          if (village[res] >= sWarn) td.addClassName('warning');
          if (village[res] >= sFull) td.addClassName('storage-overflow');
          td.update(village[res].toLocaleString());
          row.insert(td);
        });

        row.insert('<td class="number storage">'
          + village.storage.toLocaleString() + '</td>');

        row.insert('<td class="number current-population">'
          + village.population.toLocaleString() + '</td>');
        row.insert('<td class="separator">/</td>');
        row.insert('<td class="number population">'
          + village.maxPopulation.toLocaleString() + '</td>');

        row.insert('<td class="number points">'
          + village.points.toLocaleString() + '</td>');

        grid.tBodies[0].appendChild(row);
        idx++;
      }, this);

      var tfrow = $(grid.tFoot.rows[0]);
      for (attr in opt.totals) {
        var cssClass = attr === 'population'    ? 'current-population'
                     : attr === 'maxPopulation' ? 'population'
                     : attr;
        tfrow.down('td.' + cssClass).update(opt.totals[attr].toLocaleString());
      }
      return grid;
    },

    'Clădiri': function(grid, opt) {
      var buildings = this.getBuildingsList();
      grid.insert(
       '<thead>\
            <tr>\
              <th class="idx" rowspan="2">#</th>\
              <th class="village" rowspan="2">Sat</th>\
              <th class="points tiny" rowspan="2">Puncte</th>\
              <th class="buildings tiny" colspan="' + buildings.length +
                                                  '">Cl\u0103diri</th>\
            </tr>\
            <tr>' +
          buildings.map(function(id){
            return ('<th class="building-#{id}" title="#{name}">' +
              '<img src="#{icon}" alt="#{id}" /></th>'
            ).interpolate(this.getBuilding(id));
          },this).join('')
          + '</tr>\
          </thead>\
          <tfoot>\
            <tr>\
              <td colspan="2"></td>\
              <td class="number points"></td>\
              <td colspan="' + buildings.length + '"></td>\
            </tr>\
          </tfoot>\
          <tbody></tbody>'
      );

      var idx = 1;
      this.getVillagesList().each(function(id){
        var village = this.getVillage(id);
        var row = new Element('tr', {'class': 'row_' + (idx % 2 ? 'a' : 'b')
                                                          + ' village-' + id});
        row.insert('<td class="idx">' + idx + '</td>');

        var td = new Element('td', {'class': 'village'});
        row.insert(td);
        if (opt.hints[id])
          opt.hints[id].each(function(img){ td.insert(img).insert(' '); });
        td.insert(this.getSwitchVillageLink(id, {screen:'main'}));

        row.insert('<td class="number points">'
          + village.points.toLocaleString() + '</td>');

        buildings.each(function(bid){
          var lvl = '', state = '';
          if (typeof village.buildings[bid] != 'undefined') {
            lvl   = village.buildings[bid];
            state = lvl ? lvl + 1 < this.getBuilding(bid).levels.length
                            ? 'incomplete' : 'complete' : 'missing';
          }
          if (lvl) {
            var btd = new Element('td', {'class': 'number ' + state});
            btd.insert('<a href="game.php?village=' + id
              + '&screen=' + bid + '">' + lvl + '</a>');
            row.insert(btd);
            this.addBuildingTip(btd,bid,lvl);
          } else {
            row.insert('<td class="number ' + state + '">' + lvl + '</td>');
          }
        }, this);

        grid.tBodies[0].appendChild(row);
        idx++;
      }, this);

      $(grid.tFoot.rows[0]).down('td.points').update(opt.totals.points.toLocaleString());

      return grid;
    },

    'Dezvoltare': function(grid, opt) {
      var queues = $w('main barracks stable garage snob smith');
      grid.insert(
        '<thead><tr>'
        + '<th class="idx">#</th>'
        + '<th class="village">Sat</th>'
        + '<th class="population tiny" colspan="3">Populație</th>'
        + queues.map(function(id){
            return ('<th class="queue-#{id}" title="#{name}">'
              + '<img src="#{icon}" alt="#{id}" /></th>'
            ).interpolate(this.getBuilding(id));
        },this).join('')
        + '</tr></thead><tbody></tbody>'
      );
      var idx = 1;
      var now = new Date(), epoch = now.getTime(),
        tomorrow = now.nextDay(), today = now.thisDay();
      this.getVillagesList().each(function(id){
        var village = this.getVillage(id);
        var row = new Element('tr', {'class': 'row_' + (idx % 2 ? 'a' : 'b')
                                                          + ' village-' + id});
        row.insert('<td class="idx">' + idx + '</td>');

        var td = new Element('td', {'class': 'village'});
        row.insert(td);
        td.insert(this.getSwitchVillageLink(id));

        row.insert('<td class="number current-population">'
          + village.population.toLocaleString() + '</td>');
        row.insert('<td class="separator">/</td>');
        row.insert('<td class="number population">'
          + village.maxPopulation.toLocaleString() + '</td>');

        queues.each(function(qid){
          var td = new Element('td',{'class': 'eta queue-' + qid + ' nowrap'});
          row.insert(td);
          var qlink = new Element('a', {
            href: 'game.php?village=' + village.id + '&screen=' + qid
          });
          td.insert(qlink);

          var qlist = village.queue[qid];
          if (!qlist) return;

          // drop "already done" items
          var finished = 0;
          while(qlist.length && qlist[0].end < epoch) {
            finished++;
            qlist.shift();
          }

          if (finished) td.addClassName('warning');

          if (!qlist.length) {
            var label = '--:--:-- <span class="inactive">(0)</span>'
            if (finished) {
              td.addClassName('storage-overflow');
              label = 'finished';
            }
            qlink.update(label);
            return;
          }

          var qEndsAt = new Date(qlist[qlist.length-1].end);
          // qlink.update(qEndsAt.fuzzy());
          qlink.update(
            ((qEndsAt-now)/1000).toDuration(0,2)
            + ' <span class="inactive">(' + qlist.length + ')</span>'
          );

          // summarize queue contents... in a fancy tooltip
          var tip = new Element('table', {'class': 'vis grid'});

          var tmpl = '<tr class="row_#{odd}">'
            + '<td class="nowrap"><img src="#{icon}" /> #{name}</td>'
            + '<td class="number nowrap">'
            + (qid == 'smith' ? '' : qid == 'main' ? 'nivelul #{num}' : '#{num}')
            + '</td>'
            + '<td class="nowrap">#{end}</td>'
            + '</tr>';

          if (qid == 'main' || qid == 'smith') {
            var j = 1;
            qlist.each(function(item){
              var end = new Date(item.end);
              tip.insert(tmpl.interpolate({
                odd:  (j++ % 2) ? 'a' : 'b',
                icon: item.ref.icon,
                name: item.ref.name,
                id:   item.ref.id,
                num:  item.num,
                end:  ( end.date() == today.date() ? 'astăzi'
                        : end.date() == tomorrow.date() ? 'mâine'
                        : end.date()
                      ) + ' la ora ' + end.time()
              }));
            });
          } else {
            var totals = {};
            qlist.each(function(item) {
              var id = item.ref.id
              if (!totals[id]) {
                totals[id] = {
                  icon: item.ref.icon,
                  name: item.ref.name,
                  id:   item.ref.id,
                  num:  item.num,
                  end:  item.end
                };
              } else {
                totals[id].end  = item.end;
                totals[id].num += item.num;
              }
            });
            var tlist = $H(totals).keys();
            tlist.sort(function(l,r){
              var a = +totals[l].end, b = +totals[r].end;
              return a < b ? -1 : a > b ? 1 : 0;
            });
            var j = 1;
            tlist.each(function(ti) {
              var it = totals[ti], end = new Date(it.end);
              tip.insert(tmpl.interpolate({
                odd:  (j++ % 2) ? 'a' : 'b',
                icon: it.icon,
                name: it.name,
                id:   it.id,
                num:  it.num.toLocaleString(),
                end:  ( end.date() == today.date() ? 'astăzi'
                        : end.date() == tomorrow.date() ? 'mâine'
                        : end.date()
                      ) + ' la ora ' + end.time()
              }));
            });
          }
          new Tip(qlink, tip);
        }, this);

        grid.tBodies[0].appendChild(row);
        idx++;
      }, this);
      return grid;
    },

    'Resurse': function(grid, opt) {
      grid.insert(
       '<thead>\
            <tr>\
              <th class="idx" rowspan="2">#</th>\
              <th class="village" rowspan="2">Sat</th>\
              <th class="wood tiny" colspan="3">Lemn</th>\
              <th class="stone tiny" colspan="3">Argilă</th>\
              <th class="iron tiny" colspan="3">Fier</th>\
              <th class="storage" colspan="2">Magazie</th>\
            </tr>\
            <tr>\
              <th class="tiny" title="Nivel"><img src="/graphic/buildings/wood.png" alt="N." /></th>\
              <th class="tiny" title="Producție / oră">Pph</th>\
              <th class="tiny">Stoc</th>\
              <th class="tiny" title="Nivel"><img src="/graphic/buildings/stone.png" alt="N." /></th>\
              <th class="tiny" title="Producție / oră">Pph</th>\
              <th class="tiny">Stoc</th>\
              <th class="tiny" title="Nivel"><img src="/graphic/buildings/iron.png" alt="N." /></th>\
              <th class="tiny" title="Producție / oră">Pph</th>\
              <th class="tiny">Stoc</th>\
              <th class="tiny" title="Nivel"><img src="/graphic/buildings/storage.png" alt="N." /></th>\
              <th class="tiny" title="Capacitate">Cap.</th>\
            </tr>\
          </thead>\
          <tfoot>\
            <tr>\
              <td colspan="2"></td>\
              <td></td>\
              <td class="number woodprod"></td>\
              <td class="number wood"></td>\
              <td></td>\
              <td class="number stoneprod"></td>\
              <td class="number stone"></td>\
              <td></td>\
              <td class="number ironprod"></td>\
              <td class="number iron"></td>\
              <td></td>\
              <td class="number storage"></td>\
            </tr>\
          </tfoot>\
          <tbody></tbody>'
      );
      var idx = 1;
      opt.totals.woodprod = 0;
      opt.totals.stoneprod = 0;
      opt.totals.ironprod = 0;
      this.getVillagesList().each(function(id){
        var village = this.getVillage(id);
        var row = new Element('tr', {'class': 'row_' + (idx % 2 ? 'a' : 'b')
                                                          + ' village-' + id});
        row.insert('<td class="idx">' + idx + '</td>');

        var td = new Element('td', {'class': 'village'});
        td.insert(this.getSwitchVillageLink(id));
        row.insert(td);

        var sWarn = village.storage * 0.95,
            sFull = village.storage;
        ['wood','stone','iron'].each(function(res){
          var td = new Element('td', {'class': 'number ' + res});
          if (village[res] >= sWarn) td.addClassName('warning');
          if (village[res] >= sFull) td.addClassName('storage-overflow');

          var lvl = '', state = '', bns = 0, bld = this.getBuilding(res);
          if (typeof village.buildings[res] != 'undefined') {
            lvl   = village.buildings[res];
            state = lvl ? lvl + 1 < bld.levels.length
                            ? 'incomplete' : 'complete' : 'missing';
            bns = (bld.levels[lvl].bonus || 0);
          }
          opt.totals[res + 'prod'] += bns;
          row.insert('<td class="number ' + state + '">' + lvl + '</td>');
          row.insert('<td class="number">' + (bns ? bns.toLocaleString() : '') + '</td>');
          row.insert(td.update(village[res].toLocaleString()));
        }, this);

        var slvl = '', sstate = '', sbld = this.getBuilding('storage');
        if (typeof village.buildings.storage != 'undefined') {
          slvl   = village.buildings.storage;
          sstate = slvl ? slvl + 1 < sbld.levels.length
                          ? 'incomplete' : 'complete' : 'missing';
        }
        row.insert('<td class="number ' + sstate + '">' + slvl + '</td>');
        row.insert('<td class="number storage">' + sFull.toLocaleString() + '</td>');

        grid.tBodies[0].appendChild(row);
        idx++;
      }, this);

      var tfrow = $(grid.tFoot.rows[0]);
      for (attr in opt.totals) {
        var td = tfrow.down('td.' + attr);
        if (td) td.update(opt.totals[attr].toLocaleString());
      }

      return grid;
    },

    // FIXME: TBD
    'Populaţie': function(grid, opt) {
      return grid;
    },
    'Comerţ': function(grid, opt) {
      return grid;
    },
    'Război': function(grid, opt) {
      return grid;
    }
  },

  hoverviewListener: function(ev, type) {
    if (ev) Event.stop(ev);
    type = type || 'General';
    var id = 'overview-window-' + type;
    var win = Windows.getWindow(id);
    if (win) return win.close();
    win = this.createWindow({
      id:     id,
      title:  'Privire de ansamblu: ' + type,
      width:  820,
      height: 400,
      top:    60,
      left:   10
    });
    new Ajax.Request(
      'game.php?screen=overview_villages',
      {     method: 'GET',
         onSuccess: this.hoverviewShow.bind(this,win,type),
         onFailure: function(){
            win.setHTMLContent('<p class="error">FAILED!</p>');
            win.close();
         }
      }
    );
    var swl = $('village-switch-list');
    if (swl) swl.remove(); // force a refresh on next call
    win.show();
  },

  hoverviewShow: function(win, type, t) {
    var html = new Element('div').update(t.responseText.scrubHTMLBody());
    this._testContent.appendChild(html);
    win.getContent().update(this.drawOverview(type, this.parseOverview(html.xselect('.//table//th[text()="Sat"]/ancestor::table[1]')[0])));
    html.remove();
    win.updateSize();
    // FIXME: implement it!
    // win.updatePosition();
  },

  enhanceTransportsTable: function(t, isOutgoing) {
    // sort rows by "sosire"
    var sos = t.xselect('.//th[text()="Sosire în:"]')[0];
    if (sos) {
      var rows = t.xselect('.//td[' + (sos.realCellIndex() + 1) + ']').map(function(td){
        return { tr: td.up('tr'), duration: td.textContent.clean().toSeconds() };
      });
      rows.sort(function(left,right){
        var a = left.duration, b = right.duration;
        return a < b ? -1 : a > b ? 1 : 0;
      });
      rows.each(function(v){ t.insert(v.tr.remove()); });
    }

    // CSS hook
    t.addClassName('grid').addClassName(
      (isOutgoing ? 'outgoing' : 'incoming') + '-transports');

    // add "number" for "merchants" cells
    if (isOutgoing) {
      nth = t.xselect('.//th[text()="Negustor"]')[0];
      if (nth) {
        t.xselect('.//td[' + (nth.realCellIndex()+1) + ']').invoke('addClassName', 'number');
      }
    }

    // shorter "labels"
    var labels = {
      'Livrare de la':   '<img src="/graphic/map/map_w.png" alt="←" title="Livrare de la" /> ',
      'Transport către': '<img src="/graphic/map/map_e.png" alt="→" title="Transport către" /> ',
      'Întoarcere din':  '<img src="/graphic/map/map_sw.png" alt="↙" title="Întoarcere din" /> '
    };
    t.xselect('.//td[1]').each(function(td){
      var link = td.down('a').remove();
      var label = td.textContent.clean();
      if (labels[label]) {
        td.update(labels[label]);
      }
      td.insert(link);
    });

    // does it carry any merchandise? enhance its layout :)
    this.enhanceMarfa(t.xselect('.//th[text()="Marfă"]')[0]);
  },

  enhanceMarfa: function(th) {
    if (!th) return;
    var table = th.up('table'),
        midx  = th.realCellIndex()+1,
        total = {wood:0, stone:0, iron:0},
        deimg = {holz:'wood', lehm:'stone', eisen:'iron'};
    th.writeAttribute('colspan', 3);
    table.xselect('.//td[' + midx + ']').each(function(td){
      td.addClassName('tiny');
      var tds = {
        wood:  new Element('td',{'class': 'tiny'}),
        stone: td,
        iron:  new Element('td',{'class': 'tiny'})
      };
      td.insert({before: tds.wood}).insert({after: tds.iron});
      if (!td.innerHTML.match(/\S/)) return;
      td.select('span.grey').invoke('remove');
      td.select('img').each(function(img) {
        var key = img.getAttribute('src').replace(/[.][^.]+$/,'').replace(/^.*\/+/,'');
        img.replace(deimg[key] + ' ');
      });
      var hres = td.innerHTML.clean().split(' ');
      td.update('');
      hres.eachSlice(2, function(p){
        var res = p[0], val = parseInt(p[1],10);
        total[res] += val;
        tds[res].update(val.toLocaleString()).addClassName(res);
      });
    });
    table.insert(
      '<tfoot><tr>' + '<td colspan="' + (midx-1) + '"></td>' +
      ['wood','stone','iron'].map(function(res) {
        var tres = total[res];
        return '<td class="' + (tres ? res : 'tiny') + '">' +
          (tres ? total[res].toLocaleString() : '') +
        '</td>';
      }).join('') + '</tr></tfoot>'
    );
  },

  enhanceNecesitate: function(th) {
    var table = th.up('table');
    var hspan = th.hasAttribute('colspan') ? th.getAttribute('colspan').toInteger() : 1;
    var res = $w('wood stone iron population');
    var i = hspan;
    while (i--)
      table.xselect('.//td[' + (th.cellIndex + 1 + i) + ']').each(function(td){
        // 4: [wood] [stone] [iron] [workers]
        // 3: [wood] [stone] [iron]
        // 2: [wood, stone, iron] [workers]
        // 1: [wood, stone, iron]
        if (hspan > 1) {
          if (hspan > 2 || i > 0) {
              td.addClassName(res[hspan == 2 ? 3 : i]).update(td.textContent.clean().toInteger().toLocaleString());
          }
        }
        if (hspan < 2 || (hspan === 2 && i === 0) ) {
          var n = td.textContent.clean().split(' ').map(function(s){
            return s.toInteger().toLocaleString()
          });
          td.update('');
          for (var ri = 0; ri < n.length; ri++) {
            if (ri) {
              var ntd = new Element('td');
              td.insert({after: ntd});
              td = ntd;
            }
            td.addClassName(res[ri]).update(n[ri]);
            // td.insert(' <span class="' + res[ri] + '">' + n[ri] + '</span>');
          }
          if (hspan == 2) th.setAttribute('colspan',4);
          else if (hspan == 1) th.setAttribute('colspan',3);
        }
      });
  },

  enhanceMuncitori: function(th) {
    var table = th.up('table');
    th.setAttribute('colspan', 3);
    table.xselect('.//td[' + (th.realCellIndex() + 1) + ']').each(function(td){
      var n = td.textContent.clean().split('/').map(function(s){
        return s.toInteger().toLocaleString()
      });
      td.update(n[1]).addClassName('number population')
        .insert({before: '<td class="number current-population">' + n[0] + '</td>'})
        .insert({before: '<td class="separator">/</td>'});
    });
  },

  addBuildingTip: function(to, id, level, nextLevel) {
    if (!level && !nextLevel) return false;
    var diffMode = typeof nextLevel !== 'undefined';

    var bld = this.getBuilding(id);
    if (!bld) return false;

    var db = bld.levels[level];
    if (!db) return false;

    var dbn = diffMode ? bld.levels[nextLevel] : {};

    var tip = new Element('table', {'class': 'vis grid'});
    var idx = 1;
    var tmpl = '<tr class="row_#{odd}">'
        + '<td class="nowrap">#{name}:</td>'
        + '<td class="number nowrap">#{value}</td>'
        + '</tr>';

    tip.insert('<tr><th colspan="2"><img src="#{icon}" /> #{name} (#{level})</th></tr>'.interpolate({
      icon:  bld.icon,
      name:  bld.name,
      // level: diffMode ? nextLevel : level
      level: level + (diffMode ? (nextLevel-level > 0 ? '+':'') + (nextLevel-level) : '')
    }));

    if (db.points || dbn.points) {
      var value;
      if (diffMode) {
        var delta = (dbn.points || 0) - db.points;
        value = delta ? (delta > 0 ? '+' : '') + delta.toLocaleString() : '';
      } else {
        value = db.points.toLocaleString();
      }
      if (value) {
        tip.insert(tmpl.interpolate({
          odd:   (idx++ % 2) ? 'a' : 'b',
          name:  'Puncte',
          value: value
        }));
      }
    }
    if (diffMode) {
      if (db.workersL || dbn.workersL) {
        tip.insert(tmpl.interpolate({
          odd:   (idx++ % 2) ? 'a' : 'b',
          name:  'Populaţie',
          value: (level > nextLevel ? '-' : '+') + (dbn.workersL || db.workersL).toLocaleString()
        }));
      }
    } else {
      if (db.workers) {
        tip.insert(tmpl.interpolate({
          odd:   (idx++ % 2) ? 'a' : 'b',
          name:  'Populaţie',
          value: db.workers.toLocaleString()
        }));
      }
    }

    var bns = this.getBonus(id);
    if (diffMode) {
      if (bns && dbn.level) {
        var delta = ( (dbn.bonus||0) - (db.bonus||0) );
        if (delta) {
          tip.insert(tmpl.interpolate({
            odd:   (idx++ % 2) ? 'a' : 'b',
            name:  bns.desc,
            value: bns.template.interpolate({
              bonus: (delta > 0 ? '+' : '') + delta.toLocaleString()
            })
          }));
        }
      }
    } else {
      if (bns && db.bonus) {
        tip.insert(tmpl.interpolate({
          odd:   (idx++ % 2) ? 'a' : 'b',
          name:  bns.desc,
          value: bns.template.interpolate({
            bonus: db.bonus.toLocaleString()
          })
        }));
      }
    }

    if (idx > 1) {
      new Tip(to, tip);
      return true;
    }
    return false;
  },

  cartograph: function(villages) {
    var map = { coords: { x: [], y: [] }, dots: [] };
    if (!villages || !villages.length) return map;
    var ax = villages.pluck('x'),
        ay = villages.pluck('y'),
        minX = ax.min(),
        maxX = ax.max(),
        minY = ay.min(),
        maxY = ay.max();
    for (var x = minX; x <= maxX; x++) {
      map.coords.x.push(x);
    }
    for (var y = minY; y <= maxY; y++) {
      map.coords.y.push(y);
      map.dots.push(new Array(maxX - minX + 1));
    }
    villages.each(function(village){
      map.dots[village.y - minY][village.x - minX] = village;
    });
    return map;
  },

  setPlotterSwitch: function(hookTo) {
    var plotter = new Element('table', {'class':'plotter', 'id':'village-plotter'});
    var thead = new Element('thead'), theadRow = new Element('tr');
    var map = this.cartograph(this.getVillagesArray());
    theadRow.insert('<th class="coord">X/Y</th>');
    map.coords.x.each(function(x){
      theadRow.insert('<th class="coord">' + x + '</th>');
    });
    thead.insert(theadRow);
    plotter.insert(thead);
    for (var y = 0; y < map.dots.length; y++) {
      var villages = map.dots[y];
      var row = new Element('tr');
      row.insert('<th class="coord">' + map.coords.y[y] + '</th>');
      for (var x = 0; x < villages.length; x++) {
        var td = new Element('td');
        var village = villages[x];
        if (village) {
          td.addClassName('village');
          var a = this.getSwitchVillageLink(village.id);
          a.update(
            '<img src="http://www.triburile.ro/graphic/buildings/' +
            (village.buildings.snob ? 'snob' : 'main') +
            '.png" width="16" height="16" alt="O" />'
          );
          td.insert(a);
          if (a.tagName.uc() !== 'A') td.addClassName('current');
        }
        row.insert(td);
      }
      plotter.insert(row);
    }
    new Tip(hookTo, plotter,
      { className: 'plotter',
        hook:      {target: 'bottomLeft', tip: 'topLeft'},
        hideOn:    false,
        delay:     0.3,
        hideAfter: 0.1
      }
    );
  },

  getDestroyBuildingLevel: function(building) {
    if (!building) return -1;
    var queue = this.village.queue.main;
    var level = this.village.buildings[building.id] || 0;
    this.village.queue.main.each(function(i) {
      if (i.ref == building) level = i.num;
    });
    return level - 1;
  },
  getQueueBuildingLevel: function(building) {
    if (!building) return 0;
    var queue = this.village.queue.main;
    var level = this.village.buildings[building.id] || 0;
    this.village.queue.main.each(function(i) {
      if (i.ref == building) level = i.num;
    });
    return level;
  },

  getDuration: function(unitId, from, to) {
    var speed = unitId == 'merchant'
        ? this.getWorldData().merchantSpeed
        : this.getUnit(unitId).speed;
    var secs = speed * 60 * Math.sqrt(
      Math.pow(from.x - to.x, 2) + Math.pow(from.y - to.y, 2)
    );
    return secs.toDuration(); 
  },

  isMyVillage: function(id) {
    if (this.village.id == id) return true;
    return !!this.getVillage(id);
  },

  isHumanRequired: function(top) {
    if (!top) top = this.document;
    if (top.title && (
          top.title.indexOf('Sesiune depaşită') > -1
          || top.title.indexOf('Protecţie contra Bot') > -1
        )
       )
    {
      top.title = 'EROARE';
      return true;
    }
    if (top.xselect('.//h2[text()="Sesiune depaşită"]').length > 0)
    {
      top.select('input[type="password"]').invoke('activate');
      return true;
    }
    return false;
  },

  // "test" (as in "transient") content wrapper
  _testContent: (function(){
    var d = document.createElement('div');
        d.style.display = 'none';
    var p = document.getElementsByTagName('body')[0]
            || document.getElementsByTagName('head')[0];
    p.appendChild(d);
    return d;
  })(),

  createWindow: function(options) {
    var opt = Object.extend({}, this._defaultWindowOptions);
    if (options) opt = Object.extend(opt, options);
    var win = new Window(opt);
    win.setHTMLContent(opt.HTMLContent || '<span class="loading">loading…</span>');
    return win;
  },
  _defaultWindowOptions: {
    className:      'greylighting',
    width:          600,
    height:         200,
    wiredDrag:      false,
    showEffect:     Element.show,
    hideEffect:     Element.hide,
    recenterAuto:   false,
    destroyOnClose: true,
    title:          'loading…',
    maxWidth:       parseInt(document.documentElement.clientWidth)-50,
    maxHeight:      parseInt(document.documentElement.clientHeight)-50
  },

  getWorldData: function(world) {
    return window.top.TGCS.data[world || this.world];
  },

  getBonuses: function() {
    return window.top.TGCS.bonuses;
  },
  getBonus: function(id) {
    return this.getBonuses()[id];
  },

  getBuildings: function() {
    return this.getWorldData().buildings;
  },
  getBuildingsList: function() {
    var o = this.getBuildings(), l = [];
    for (var i in o) l.push(i);
    return l;
  },
  getBuilding: function(id) {
    return this.getBuildings()[id];
  },
  getBuildingByName: function(name) {
    var buildings = this.getBuildings();
    for (id in buildings)
      if (buildings[id].name == name)
        return buildings[id];
    return;
  },

  getUnits: function() {
    return this.getWorldData().units;
  },
  getUnitsList: function() {
    var o = this.getUnits(), l = [];
    for (var i in o) l.push(i);
    return l;
  },
  getUnit: function(id) {
    return this.getUnits()[id];
  },
  getUnitByName: function(name) {
    var units = this.getUnits();
    for (id in units)
      if (units[id].name == name) return units[id];
    return;
  },

  getVillages: function() {
    var wd = this.getWorldData();
    if (!wd.villages) wd.villages = {};
    return wd.villages;
  },
  getVillagesArray: function() {
    var o = this.getVillages(), l = [];
    for (var i in o) l.push(o[i]);
    l.sort(function(a,b){ // sort villages by name and coordinates
      var va = a.name.lc(),
          vb = b.name.lc();
      return va  < vb  ? -1 : va > vb   ? 1 :
             a.x < b.x ? -1 : a.x > b.x ? 1 :
             a.y < b.y ? -1 : a.y > b.y ? 1 : 0;
    });
    return l;
  },
  getVillagesList: function() {
    var o = this.getVillages(), l = [];
    for (var i in o) l.push(i);
    l.sort(function(a,b){ // sort villages by name and coordinates
      var va = o[a].name.lc(),
          vb = o[b].name.lc();
      return va  < vb  ? -1 : va > vb   ? 1 :
             o[a].x < o[b].x ? -1 : o[a].x > o[b].x ? 1 :
             o[a].y < o[b].y ? -1 : o[a].y > o[b].y ? 1 : 0;
    });
    return l;
  },
  getVillage: function(id) {
    return this.getVillages()[id];
  },
  getVillageByName: function(name) {
    var villages = this.getVillages();
    for (id in villages)
      if (village[id].name == name) return villages[id];
    return;
  },
  createOrUpdateVillage: function(id, options) {
    var village = this.getVillage(id);
    if (!village) return this.createVillage(id, options);
    return Object.extend(village, options || {});
  },
  createVillage: function(id, options) {
    var village = this.getVillage(id);
    if (village) return village;
    village = Object.extend({
        id:            id,
        name:          'UNKNOWN',
        x:             0,
        y:             0,
        continent:     'K??',
        points:        0,
        wood:          0,
        stone:         0,
        iron:          0,
        storage:       0,
        population:    0,
        maxPopulation: 0,
        buildings:     { }, // assume "nothing" (it's used for availability checking!)
        queue:         { barracks: [], // each queue has its own structure!
                         garage:   [],
                         main:     [],
                         smith:    [],
                         snob:     [],
                         stable:   []
                       }
      },
      options || {}
    );
    this.getVillages()[id] = village;
    return village;
  },

  setMyVillages: function(villages) {
    var old = this.getVillages();
    for (id in old)
      if (!villages[id])
        delete old[id];
  },

  toolbarBuildings: $w('main market place barracks stable snob garage smith'),
  icons: {
    abort: 'http://gfx.neohub.com/icons/delete.png'
  },

  baseurl:  window.location.pathname.replace(/[^\/]+$/, ''),
  world:    window.location.hostname.replace(/^([^.]+).+$/,'$1'),
  body:     Element.extend(document.body),
  document: Element.extend(this.document.documentElement),
  window:   window
};

$w('Garage Snob Stable').each(function(b) {
  TGC['enhanceQueue' + b] = TGC.enhanceQueueBarracks;
});

$H({ // aliases for default "mode" enhancers
  Main:'Build', Market:'Send', Place:'Command', Report:'All',
}).each(function(p) {
  var n = 'enhanceScreen' + p.key;
  TGC[n] = TGC[n + p.value];
});

// bail out if game is running in "premium" mode as we WON'T support it!
if (document.getElementById('quickbar')) {
  alert(
    'Premium mode detected!' + '\r\n'
    + 'You should disable this add-on while using a "Premium Account",' + '\r\n'
    + 'as it will NEVER be supported!'
  );
} else {
  TGC.gameLoaded();
}

// restart game's timers
window.setInterval(tick, 1000);

// console.profileEnd();

