// ======================= STARTUP ===============================

$(document).ready(function() {
  $(window).load(function() { $('body').removeClass('splash') });
  initutils();
  initdash();
  inittabs();
  initinfo();
  initinfo_preview();
  initinfo_az();
  initinfo_menu();
  initpedals();
  initjsdb();
  initplayerlink();
  inittest();
  $('body').attr('ontouchmove', 'event.preventDefault()');
  if (!$.browser.webkit) {
    $('body').addClass('moz');
    translatestylesheet();
  }
  $('body').setcolourbuttonevents();
  resize();
  window.onresize = resize;
  window.onorientationchange = function() { location.reload() };
  playerlink.sync();
  $info.select(localStorage.scenename? localStorage.scenename : 'newcomers');
  setInterval(function() { $dashplayer.refreshtimeline(1) }, 1000);
  setInterval(function() {
    var now = new Date();
    if (now.getHours() == 5) $info.select('newcomers'); // 05:xx daily
  }, 3600*1000); // hourly
});

// ======================= CONSTANTS ===============================
// ============== (subject to test variations) =====================

var expserv = location.hostname.match(/localhost|192\.168\.2\.11/i) != null;
var simulator = location.pathname.match(/simulator/i) != null;
var onipad = navigator.userAgent.match(/iPad/i) != null;
var clickstart = onipad? 'touchstart':'mousedown';
var clickend = onipad? 'touchend':'mouseup';
var movestart = onipad? 'touchmove':'mousedown';
var moveend = onipad? 'touchend':'mouseup';
var isportrait = innerHeight > innerWidth;
var transitionend = $.browser.webkit?'webkitTransitionEnd':'transitionend';
var useiscroll = $.browser.webkit;
var domdelay = $.browser.webkit?0:100;
var inspectthumb = false;
var searchlimit = 30;
var historylimit = isportrait?36:35;
var spinner_ms = 250;
var showtouched_ms = 300;

var $dash, $dashnowplaying, $dashplayer, $dashqueue;
var $tabs, $google;
var $info, $info_preview, $info_az, $info_menu;
var $pedals;

var jsdb = {};
var player = {}, playerlink = {};

//----------------------------------------------------------------------------------
function inittest() {

  if (expserv) {
    $('body').addClass('experimental');
//    useiscroll = true;
//    useiscroll = false;
//    inspectthumb = 2;
  }
}

// ======================= DASHBOARD ===============================

function initdash() {

  $dash = $('#dash');

  $dashnowplaying = $('#dashnowplaying');
  $dashnowplaying.resize = function() {
    var width = (window.innerWidth-$dashplayer.width())/2-this.find('img').width();
    changestylesheet(['div#dashnowplaying div.wide   {width: ' +(width+50)+ 'px}'], 'dashnowplaying.resize');
    changestylesheet(['div#dashnowplaying div.narrow {width: '   +(width)+  'px}'], 'dashnowplaying.resize');
  };
  $dashnowplaying.refresh = function() {
    if (player.fault || player.playmode == "STOP") {
      this.find('.cover').hide();
      this.find('div').html('');
    } else if (player.externalmedia) {
      this.find('.cover').attr('src', $('#snippets .oddmedia').attr('src')).show();
      this.find('.tracktitle').html('External media');
      this.find('.trackartist').html(player.externalmedia);
      this.find('.albumtitle').html('');
    } else {
      this.find('.cover').attr('src', '/data/covers/' +player.mediakey.split('|')[0]+ '.jpg').show();
      var trackorpiece = jsdb.gettrackorpiece(player.mediakey);
      this.find('.tracktitle').html(trackorpiece.title);
      this.find('.trackartist').html(trackorpiece.artist);
      this.find('.albumtitle').html(trackorpiece.album.title);
    }
    return this;
  };
  $dashnowplaying.touch(function(event) { $info.select('nowplaying_album') });

  $dashplayer = $('#dashplayer');
  $dashplayer.find('.button').touch(function(event) {
    var id = this.id.substring(4); // remove 'dash'
    if (id == 'prev' || id == 'next') {
      playerlink.httpQ(id);
    } else if (player.playmode == "STOP") {
      if ($info_menu.is(':visible')) {
        $info_menu.selectfirst(); // Play Album or Play Tracks
      } else if (player.playpos) {
        playerlink.httpQproxied("&setplaylistpos="+(player.playpos-1)+"&play");
      }
    } else if (player.playmode == "PAUSE") {
      playerlink.httpQ("play");
    } else {
      playerlink.httpQ("pause");
    }
    var $this = $(this);
    $this.addClass('touched');
    if (id == 'play') return; // sync will update it
    setTimeout(function() { $this.removeClass('touched') }, showtouched_ms);
  });
  $dashplayer.refresh = function() {
    this.toggleClass('fault', !!player.fault);
    this.find('#dashplay').toggleClass('playing', player.playmode == "PLAY").removeClass('touched');
    this.refreshtimeline(0);
  }
  $dashplayer.refreshtimeline = function(inc) {
    playerlink.resyncaftersleepmaybe();
    if (!player.playmode) return;
    if (player.playmode == "STOP") {
      this.updatetimeline('', '', 0);
      return;
    }
    if (player.playmode == "PLAY") player.secsplayed = parseInt(player.secsplayed) + inc;
    if (player.tracksecs) { // normal
      player.secsplayed = Math.max(Math.min(player.secsplayed, player.tracksecs), 0);
      this.updatetimeline(hhmmss(player.secsplayed), hhmmss(player.tracksecs-player.secsplayed), player.secsplayed/player.tracksecs);
    } else {
      this.updatetimeline(hhmmss(player.secsplayed), '??:??', 0);
    }
  };
  $dashplayer.updatetimeline = function(played, left, fraction) {
    this.gettimelinemetrics();
    this.find('.played').html(played);
    this.find('.left').html(left);
    var offset = this.timelinewidth * fraction;
    this.find('.timeline_e').css('width', offset+'px');
    this.find('.timeline_n').css('marginLeft', (offset-25)+'px'); // -25 for timeline_n half-width
  }
  $dashplayer.gettimelinemetrics = function() {
    if (!this.timelinestart) {
      var $bg = this.find('.timeline_b');
      if ($bg.length) {
        this.timelinestart = $bg.offset().left+1+5; // +1 for the css left difference n-b, +5 for blob half-width
        this.timelinewidth = 224; // timeline_b width -13
      }
    }
  };
  $dashplayer.find('.timeline_n').bind(movestart+' '+moveend, function(event) {
    if (!player.tracksecs) return;
    if (event.type == movestart) {
      var $this = $dashplayer;
      $this.gettimelinemetrics();
      var offset = onipad ? event.originalEvent.touches[0].clientX : event.clientX;
      player.secsplayed = (offset-$this.timelinestart)/$this.timelinewidth*player.tracksecs;
      $this.refreshtimeline(0);
    } else {
      playerlink.httpQ("jumptotime", "ms=" + ((player.secsplayed+1)*1000)); // +1 estimated round-trip time
    }
  });
  $dashplayer.find('img.fault').touch(function(event) { alert(player.fault) });

  $dashqueue = $('#dashqueue');
  $dashqueue.refresh = function() {
    var covers = '';
    var secs = 0;
    $.each(player.queue, function() {
      var mediakey = this.split('@')[0];
      bits = mediakey.split('|');
      var albumid = bits[0];
      var tracknbr = bits[1];
      covers += '<img class="' +(tracknbr?'':'album')+ '" src="/data/covers/' + albumid+ '.jpg">';
      secs += parseInt(jsdb.getalbumortrackorpiece(mediakey).secs);
    });
    this.find('.covers').html(covers);
    this.find('.mmss').html(secs?'+'+hhmmss(secs):'');
    return this;
  };
  $dashqueue.touch(function(event) { $info.select('nowplaying_queue') });
}

// ======================= TABS ===============================

function inittabs() {

  $tabs = $('#tabs');
  $tabs.resize = function() {
    this.height(window.innerHeight - $('#dashboard').height() - $pedals.outerHeight());
  }
  $tabs.bind(clickstart, function() { $info_menu.hide() }); // not touch(): kills setfocus()
  $tabs.find('li:not(#tab_search)').touch(function(event) { $info.select(this.id.substring(4)) });
  $tabs.find('li#tab_search').bind(clickstart, function() { $info.select(this.id.substring(4)) }).click(function(event) { $info.setfocus() });
  $tabs.show = function(tabname) {
    this.find('li.selected').removeClass('selected');
    this.find('li#tab_'+tabname).addClass('selected');
  };
  $tabs.gethref = function(scenename) {
    var link = this.find('#tab_'+scenename+' a');
    if (!link.length) link = $('#pedal_'+scenename+' a');
    return link.length ? link[0].href : '';
  };

  $tabs.showhref = function(href) { this.find('.infohref').html(href) };

  $google = $('#google');
  $google.setquery = function(query) { this.html('"' +query+ '"') };
  $google.touch(function(event) {
    var query = $(this).html().replace(/"/g, '');
    window.open('http://google.co.uk/search?q=' + query, 'google');
  });
}

// ======================= INFO ===============================

function initinfo() {

  $info = $('#info');
  $info.scenes = {};
  $info.find('>div.scene').each(initscene);
  initscenesearch();
  initsceneluckydip();
  initscenehistory();
  initscenenowplayingalbum();
  initscenenowplayingartist();
  initscenenowplayinglyrics();
  initscenenowplayingqueue();
  initscenesound();
  $info.resize = function() {
    this.width(window.innerWidth - $tabs.width()).height($tabs.height());
    $.each(this.scenes, function() { this.refreshscrolling() });
  };
  $info.select = function(scenename) {
    var bits = scenename.split('_');
    var tabname = bits[0];
    var pedalname = bits[1] ? bits[1] : $pedals.selected[tabname];
    $pedals.selected[tabname] = pedalname;
    scenename = pedalname ? tabname+'_'+pedalname : tabname;
    $tabs.show(tabname);
    $pedals.show(scenename);
    localStorage.scenename = scenename;
    localStorage.pedals = JSON.stringify($pedals.selected);
    if (this.selected) this.selected.hideall();
    this.selected = this.scenes[scenename];
    this.find('>div.selectedtab').removeClass('selectedtab');
    this.selected.addClass('selectedtab');
    this.selected.loadorshow();
  };
  $info.setfocus = function() {
    this.selected.find('.focusme').focus(); // on iPad, works only in an onclick()
  };
  $info.push = function(subject_href) {
    var oldscene = this.selected;
    var newlevel = this.hasClass('push1')? 2 : 1;
    this.selected = this.scenes['push'+newlevel];
    this.selected.html('<h2>' + subject_href.split('|')[0] + '</h2>');
    this.maketransition(function($this) {
      $this.addClass('push'+newlevel);
    }, function($this) {
      oldscene.css('visibility', 'hidden'); // so that it's not visible if preview opened
      $info.selected.request(subject_href.split('|')[1]);
    });
    this.selected.pushedfromscene = oldscene;
    return this.selected;
  };
  $info.pop = function() {
    var oldlevel = this.hasClass('push2')? 2 : 1;
    this.selected = this.selected.pushedfromscene;
    this.selected.css('visibility', '');
    this.removeClass('push'+oldlevel);
    return this.selected;
  };
  $info.unpush = function() {
    if (this.selected.pushedfromscene) this.pop();
    if (this.selected.pushedfromscene) this.pop();
  }
  $info.selected_prepush = function() {
    return this.hasClass('push1') ? this.selected.pushedfromscene : this.selected;
  }
  $info.flagplayingmedia = function() {
    this.find('li.playing,tr.playing').removeClass('playing');
    if (!player.mediakey) return;
    this.find('li[title=' +player.mediakey.split('|')[0]+ ']').addClass('playing');
    this.find('tr.'+player.mediakey.replace('|', '_')).addClass('playing');
  };
  $info.flagqueuedmedia = function() {
    this.find('li.queued,tr.queued').removeClass('queued');
    if (!player.queue) return;
    $.each(player.queue, function() {
      var bits = this.split('@');
      var mediakey = bits[0];
      if (mediakey.indexOf('|')<0) $info.find('li[title=' +mediakey+ ']').addClass('queued');
      $info.find('tr.'+mediakey.replace('|', '_')).addClass('queued');
    });
  };
  $info.findscene = function(el) {
    return this.scenes[$(el).parents('div.scene')[0].id.substring(5)];
  };
}

//------------------------------------------------------------------------
function initscene() {

  var $scene = $(this);
  var scenename = this.id.substring(5); // remove 'info_'
  $info.scenes[scenename] = $scene;
  $scene.scenename = scenename;
  $scene.loadorshow = function() {
    if (this.iscurrent()) {
      if (this.iscroll) this.iscroll.scrollTo(0, 0, 0);
      this.addClass('shown');
      $info_az.showmaybe(this);
      this.setgooglequery();
      if (this.scenename == 'nowplaying_album') this.scrollmaybe();
      $tabs.showhref('');
      return;
    }
    this.loadme();
  };
  $scene.loadme = function() {  // default
    var href = $tabs.gethref(scenename);
    if (isportrait) href = href.replace('landscape', 'portrait');
    this.request(href);
    $tabs.showhref(href);
    this.iscurrent = function() { return true };
  };
  $scene.request = function(href) {
    this.cuespinner();
    $.ajax({ url: href, context: this, complete: this.cancelspinner, error: function(jqXHR, errmsg, httperrmsg) { this.load(errmsg + '<br>' + httperrmsg) }, success: this.load });
  };
  $scene.cuespinner = function() {
    clearTimeout(this.spinnertimer);
    this.spinnertimer = setTimeout(function() {$scene.append($('#snippets div.spinner').html()) }, spinner_ms);
  };
  $scene.cancelspinner = function() {
    clearTimeout(this.spinnertimer);
    this.spinnertimer = null;
  };
  $scene.load = function(html) {
    this.removescrolling();
    this.html(html);
    if (useiscroll) this.find('.scrollable').append('<br>');
    this.setevents();
    $info_az.showmaybe(this);
    this.setgooglequery();
    doitsoon(function() { $info.selected.setscrolling(); $info.flagplayingmedia(); $info.flagqueuedmedia(); $info.selected.addClass('shown') });
  };
  $scene.loadnoinfo = function(text) { this.load('<div class=noinfo>' +text+ '</div>') };
  $scene.setscrolling = function() {
    var $viewport = this.find('.viewport');
    if (!$viewport.length) return;
    this.$viewport = $viewport;
    $viewport.height(this.height()-$viewport.position().top);
    if (useiscroll && !this.iscroll) {
      var params = {hScrollbar: false};
      if ($info_az.is(':visible')) params.vScrollbar = false;
      this.iscroll = new iScroll($viewport[0], params);
    }
  };
  $scene.refreshscrolling = function() { if (this.iscroll) this.iscroll.refresh() };
  $scene.removescrolling = function() { if (this.iscroll) {this.iscroll.destroy(); this.iscroll = null} };
  $scene.scrolltomaybe = function($target, centre) { // (copied to $preview.scrolltomaybe)
    if (this.is(':hidden')) return 0;
    if (!$target || $target.length == 0) return 0;
    var targetrect = $target[0].getBoundingClientRect();
    if (!this.$viewport || this.$viewport.length == 0) return 0;
    var viewportrect = this.$viewport[0].getBoundingClientRect();
    if (targetrect.top >= viewportrect.top && targetrect.bottom <= viewportrect.bottom) return 0;
    var target = $target[0];
    if (!this.iscroll) {
      target.scrollIntoView();
      return 0;
    }
    var scrollablerect = this.$viewport.find('.scrollable')[0].getBoundingClientRect();
    var newpos = targetrect.top-scrollablerect.top; // top
    var delay = 400;
    var gap = 10;
    if (centre) {
      newpos -= (viewportrect.height-targetrect.height)/2; // centre
      newpos = Math.max(newpos, 0); // top
      newpos = Math.min(newpos, scrollablerect.height-viewportrect.height); // bottom
      this.iscroll.scrollTo(0, -newpos, delay);
    } else if (targetrect.top < viewportrect.top+gap) {
      this.iscroll.scrollTo(0, targetrect.top-viewportrect.top-gap, delay, true);
    } else if (targetrect.bottom > viewportrect.bottom-gap) {
      this.iscroll.scrollTo(0, targetrect.bottom-viewportrect.bottom+gap, delay, true);
    }
    return delay;
  };
  $scene.setevents = function() {
    this.find('.hotspot').unbind('click').click(function(event) {
      var $hotspot = $(event.currentTarget);
      $hotspot.addClass('clicked');
      var $scene = $info.findscene(this);
      var mediakey = this.title;
      var mystery = mediakey.substring(0, 5) == 'jsdb.';
      if (mystery) mediakey = eval(mediakey);
      var entity = mediakey.indexOf('@')>=0 ? 'tracks' : mediakey.indexOf('|')<0 ? 'album' : mediakey.indexOf('-')<0 ? 'track' : 'piece';
      $info_menu.clear();
      $info_menu.add('Play '+entity, function() { playerlink.addmedia(mediakey, true, !mystery) });
      if (entity == 'album' && $hotspot.hasClass('queued')) {
        $info_menu.add('Unqueue album', function() { $hotspot.removeClass('queued'); playerlink.togglequeued(mediakey, false) });
      } else {
        $info_menu.add('Queue '+entity, function() { playerlink.addmedia(mediakey, false, !mystery) });
      }
      if (!mystery) {
        $info_menu.add(entity == 'album' ? 'Tracks...' : 'Album...', function() { $info_preview.expose(mediakey, $hotspot) });
        $google.setquery(jsdb.getalbumortrackorpiece(mediakey).artist);
      }
      var delay = $scene.scrolltomaybe($hotspot);
      setTimeout(function() { $info_menu.show($hotspot) }, delay);
    });
    this.find('.hotspot').each(function() {
      var mediakey = this.title;
      var mystery = mediakey.substring(0, 5) == 'jsdb.';
      if (mystery) return;
      var bits = mediakey.split('|');
      $(this).addClass(bits[0]); // for flags
      if (bits[1]) $(this).addClass(bits[0]+'_'+bits[1])
    });
    this.find('.indexpush').addClass('colourbutton wedge right').unbind('click').click(function(event) {
      $info_preview.kill();
      $info.push(this.title);
    });
    this.find('.indexpop').addClass('colourbutton wedge left').unbind('click').click(function(event) {
      $info_preview.kill();
      var $scene = $info.pop();
      $info_az.showmaybe($scene);
      $info.findscene(this).setgooglequery();
    });
    this.find('.seemore').click(function() { $info.scenes.search.seemore() });
    this.setcolourbuttonevents();
  }
  $scene.loadpart = function (selector, html) {
    if (useiscroll && selector == '.scrollable') html += '<br>';
    this.find(selector).html(html);
    this.setevents();
    var that = this;
    doitsoon(function() { that.refreshscrolling() });
  };
  $scene.refresh = function() {};      // default
  $scene.iscurrent = function() { return false }; // default
  $scene.setgooglequery = function() {
    var query = this.find('.google');
    if (query.length) $google.setquery(query.html());
  };
  $scene.hideall = function() {
    $info_preview.kill();
    $info_az.hide();
    $info_menu.hide(true);
    $info.unpush();
    $info.selected.removeClass('shown');
  }
}  

//------------------------------------------------------------------------
function initscenesearch() {
  
  var $scenesearch = $info.scenes.search;
  $scenesearch.loadme = function() { 
    this.load($('#snippets .search').html());
    this.find('input').keyup(function() {
      var query = this.value;
      $scenesearch.query = query;
      $scenesearch.results = jsdb.dosearch(query);
      var summary; var list;
      if (query == '') {
        summary = '';
        list = '';
      } else if (!$scenesearch.results) {
        summary = 'no results';
        list = '';
      } else {
        var nbrresults = $scenesearch.results.length;
        summary = nbrresults + ' result' + (nbrresults==1?"":"s");
        var sublists = $scenesearch.formatpage(query);
        list ='<ul class=gallery>' +sublists.albums+ '</ul><table class=listing>' +sublists.tracksorpieces+ '</table>';
        if ($scenesearch.results.length) list += $('#snippets .moreresults').html().replace(/\d+/, $scenesearch.results.length);
      }
      var $scene = $info.scenes.search;
      $scene.find('.summary').html(summary);
      $scene.loadpart('.scrollable', list);
      $info.flagplayingmedia();
      $info.flagqueuedmedia();
    });
    this.iscurrent = function() { return true };
  };
  $scenesearch.formatpage = function(query) {
    var albums = '', tracksorpieces = '';
    for (var nbr=1; nbr<=searchlimit && this.results.length; nbr++) {
      var mediakey = this.results.shift().split('@')[1];
      if (mediakey.indexOf('|')<0) { // albumid
        albums += jsdb.getgallery(mediakey, query);
      } else { // track or piece
        tracksorpieces += jsdb.getlisting(mediakey, query);
      }
    }
    return {albums: albums, tracksorpieces: tracksorpieces};
  };
  $scenesearch.seemore = function(query) {
    this.find('.seemore').remove();
    var sublists = this.formatpage(this.query);
//    this.find('ul').append(sublists.albums);
//    this.find('tbody').append(sublists.tracksorpieces);
    this.find('.scrollable').append('<ul class=gallery>' +sublists.albums+ '</ul><table class=listing>' +sublists.tracksorpieces+ '</table>');
    if (this.results.length) this.find('.scrollable').append($('#snippets .moreresults').html().replace(/\d+/, this.results.length));
    doitsoon(function() {
//      $info.scenes.search.find('.seemore').click(function() { $info.scenes.search.seemore() });
      $info.scenes.search.setevents();
      $info.scenes.search.refreshscrolling();
    });
  };
}

//------------------------------------------------------------------------
function initsceneluckydip() {

  $info.scenes.luckydip.loadme = function() {
    this.load($('#snippets .luckydip').html());
    var plis = this.getgallery('pop', 8);
    var clis = this.getgallery('cla', 4);
    var lis = [];
    $.each((isportrait?"PPPCCCPPP132":"PPPP1CCCC3PPPP2").split(''), function(i, char) {
      if (char == 'P') {
        lis.push(plis.pop());
      } else if (char== 'C') {
        lis.push(clis.pop());
      } else {
        lis.push($('#snippets .luckydip_'+char).html());
      }
    });
    this.loadpart('ul', lis.join(''));
  }
  $info.scenes.luckydip.getgallery = function(genre, howmany) {
    var artists = {};
    var cells = [];
    while (howmany) {
      var albumid = jsdb.getrandomalbum(genre);
      var artist = albumid.split('_')[0];
      if (artists[artist]) continue;
      artists[artist] = 1;
      cells.push(jsdb.getgallery(albumid));
      howmany--;
    }
    return cells;
  };
}

//------------------------------------------------------------------------
function initscenehistory() {

  $info.scenes.history.loadme = function() {
    this.load($('#snippets .history').html());
    var cells = '';
    this.history = localStorage.history;
    $.each(this.history.split("\n", historylimit), function() { cells += jsdb.getgallery(this) });
    this.loadpart('.scrollable', '<ul class=gallery>'+cells+'</ul>');
    this.iscurrent = function() { return localStorage.history == this.history };
  };
  $info.scenes.history.update = function(mediakey) {
    if (!mediakey) return;
    var newalbumid = mediakey.split('|')[0];
    var newhistory = [newalbumid];
    if (localStorage.history) $.each(localStorage.history.split("\n"), function(i, albumid) { if (albumid != newalbumid) newhistory.push(albumid) });
    if (newhistory.length > historylimit) newhistory.length = historylimit;
    localStorage.history = newhistory.join('\n');
  };
}

//------------------------------------------------------------------------
function initscenenowplayingalbum() {

  $info.scenes.nowplaying_album.loadme = function() {
    if (player.albumid) {
      this.load(jsdb.getalbumdetails(player.albumid, false));
      this.find('.albuminfo').append($('#snippets .actions').html())
      this.setcolourbuttonevents();
      this.find('.actions').touch(function(event) { $info_preview.expose(player.albumid, $(this)) });
      this.find('table.albumtracks').addClass('slim');
      this.albumid = player.albumid;
      setTimeout(function() { $info.scenes.nowplaying_album.scrollmaybe() }, 500);  // allow iScroll to get going
    } else {
      this.loadnoinfo('Nothing playing');
      this.albumid = '';
    }
  };
  $info.scenes.nowplaying_album.refresh = function() {
    if (this.iscurrent()) {
      this.scrollmaybe();
    } else {
      this.loadme();
    }
  };
  $info.scenes.nowplaying_album.iscurrent = function() { return this.html() && this.albumid == player.albumid && !$info_preview.is(':visible') };
  $info.scenes.nowplaying_album.scrollmaybe = function() { this.scrolltomaybe(this.find('tr.playing'), true) };
}

//------------------------------------------------------------------------
function initscenenowplayingartist() {

  $info.scenes.nowplaying_artist.loadme = function() {
    if (player.albumid) {
      var artistid = player.trackartist.replace(/&#.*;|[^A-Za-z0-9]/g, '').toLowerCase();
      this.request('/data/index2_' +player.genre+ '_' +(player.genre == 'cla' ? 'composers':'allartists')+ '/' +artistid+ '.html');
      this.trackartist = player.trackartist;
    } else {
      this.loadnoinfo('Nothing playing');
      this.trackartist = '';
    }
  };
  $info.scenes.nowplaying_artist.refresh = function() { if (!this.iscurrent()) this.loadme() };
  $info.scenes.nowplaying_artist.iscurrent = function() { return this.html() && this.trackartist == player.trackartist };
}

//------------------------------------------------------------------------
function initscenenowplayinglyrics() {

  $info.scenes.nowplaying_lyrics.loadme = function() {
    if (player.mediakey) {
      var href = '/cgi-bin/lyrics.cgi?mediakey='+player.mediakey;
      $tabs.showhref(href);
      this.cuespinner();
      $.ajax({ url: href, context: this, complete: this.cancelspinner, success: this.loadlyrics });
      this.mediakey = player.mediakey;
    } else {
      this.loadnoinfo('Nothing playing');
      this.mediakey = '';
    }
  };
  $info.scenes.nowplaying_lyrics.loadlyrics = function(text) {
    if (text) {
      this.load($('#snippets .nowplayinglyrics').html());
      this.loadpart('.text', text);
    } else {
      this.loadnoinfo('No lyrics');
    }
  };
  $info.scenes.nowplaying_lyrics.refresh = function() { if (!this.iscurrent()) this.loadme() };
  $info.scenes.nowplaying_lyrics.iscurrent = function() { return this.html() && this.mediakey == player.mediakey };
}

//------------------------------------------------------------------------
function initscenenowplayingqueue() {

  $info.scenes.nowplaying_queue.loadme = function() {
    if (!player.queue || player.queue.length == 0) {
      this.loadnoinfo('Nothing queued');
      return;
    }
    var albumrow = $('#snippets .queuealbum tbody').html();
    var trackrow = $('#snippets .queuetrack tbody').html();
    var rows = '';
    $.each(player.queue, function(i, queuekey) {
      var bits = queuekey.split('@');
      var mediakey = bits[0];
      var qpos = bits[1];
      bits = mediakey.split('|');
      var albumid = bits[0];
      var tracknbr = bits[1];
      var row, rowvars;
      if (tracknbr) {
        row = trackrow;
        var track = jsdb.gettrackorpiece(mediakey);
        rowvars = {QKEY: queuekey, COVERIMG: '<img src="/data/covers/' + albumid + '.jpg">', TITLE: track.title, ARTIST: track.artist, DURATION: hhmmss(track.secs)};
      } else {
        row = albumrow;
        var album = jsdb.getalbum(albumid);
        rowvars = {QKEY: queuekey, COVERIMG: '<img src="/data/covers/' + albumid + '.jpg">', TITLE: album.title, ARTIST: album.artist, DURATION: hhmmss(album.secs)};
      }
      $.each(rowvars, function(key, value) { row = row.replace('=='+key+'==', value===undefined ? '' : value) });
      rows += row;
    });
    this.load($('#snippets .queue').html().replace('<tr></tr>', rows));
    this.find('tr').click(function(event) {
      var $scene = $info.scenes.nowplaying_queue;
      var $currentrow = $(event.currentTarget);
      $currentrow.addClass('clicked');
      $info_menu.clear();
      if (!$currentrow.is(':first-child')) $info_menu.add('Remove all<br>above this', function() { $scene.prune($currentrow, -1) });
      $info_menu.add('Remove this', function() { $scene.prune($currentrow) });
      if (!$currentrow.is(':last-child')) $info_menu.add('Remove all<br>below this', function() { $scene.prune($currentrow, 1) });
      var delay = $scene.scrolltomaybe($currentrow);
      setTimeout(function() { $info_menu.show($currentrow) }, delay);
    });
  };
  $info.scenes.nowplaying_queue.prune = function($row, range) {
    var thisindex = playerlink.getqueueindex($row.attr('title'));
    if (thisindex == null) return;
    var prunelist;
    if (range<0) {
      prunelist = player.queue.slice(0, thisindex);
    } else if (range>0) {
      prunelist = player.queue.slice(thisindex+1);
    } else {
      prunelist = player.queue.slice(thisindex, thisindex+1);
    }
    playerlink.prunequeue(prunelist);
    this.loadme();
    doitsoon(function() { $info.selected.refreshscrolling() });
  };
//  $info.scenes.nowplaying_queue.refresh = function() { if (!($info_menu && $info_menu.is(':visible'))) this.loadme() };
  $info.scenes.nowplaying_queue.refresh = function() { this.loadme() };
}

//------------------------------------------------------------------------
function initscenesound() {

  var $scenesound = $info.scenes.sound;
  $scenesound.loadme = function() { 
    this.load($('#snippets .sound').html());
    this.find('map').attr('id', 'map_remote').attr('name', 'map_remote'); // name necessary for iPad only
    var buttons = ['volup', 'voldn', 'radio', 'jukebox', 'mute', 'power']; // hotspot order in Fireworks
    this.find('area').each(function() {
      var operation = buttons.shift();
       $(this)
        .touch(function(event) { $scenesound.buttontouched(operation) })
        .bind(clickend, function(event) { $scenesound.buttonuntouched() });
    });
    this.iscurrent = function() { return true };
  };
  $scenesound.url_unit = 'http://' + (expserv?'192.168.2.31':'redeye_F0101-37871.local.') + '/cgi-bin/play_iph.sh?/devicedata/';
  $scenesound.url_operation = {
      jukebox: 'CaptuvnDuc9%201',
      radio:   'LINE-00014-00.hex%201',
      volup:   'LINE-99999-05.hex%201',
      voldn:   'LINE-99999-04.hex%201',
      mute:    'Captu6WkweG%201',
      power:   'CaptuGh0VD6%201'
  };
  $scenesound.buttontouched = function(operation) {
    if (expserv || !onipad) return;
    this.find('.xmit').addClass('lit');
    var cmdfunc = function() { $.ajax({url: $scenesound.url_unit+$scenesound.url_operation[operation]}) };
    cmdfunc();
    if (operation == 'volup' || operation == 'voldn') this.timer = setInterval(cmdfunc, 250);
  };
  $scenesound.buttonuntouched = function() {
    this.find('.xmit').removeClass('lit');
    clearTimeout(this.timer);
    this.timer = null;
  };
  $scenesound.select = function() { this.buttontouched('jukebox') };
}

// ======================= INFO AZ ===============================

function initinfo_az() {

  $info_az = $('#info_az');
  $.each("ABCDEFGHIJKLMNOPQRSTUVWXYZ#".split(''), function(nbr, initial) { $info_az.append('<div>'+initial+'</div>') });
  $info_az.resize = function() {
    this.css('left', window.innerWidth-this.outerWidth());
    this.css('top', $info.position().top+($info.height()-this.outerHeight())/2); // (should really be centred on viewport)
    };
  $info_az.showmaybe = function($targetscene) {
    this.toggle(!!$targetscene.find('tr.initial').length);
    this.$targetscene = $targetscene;
  };
  $info_az.find('div').touch(function(event) { 
    $info_az.goto($(this).html());
    $info_az.addClass('touched');
  });
  $info_az.goto = function(initial) {    
    var $anchor = this.$targetscene.find(initial == '#' ? '.i_0' : '.i_'+initial);
    if (!$anchor.length) return;
    var anchor = $anchor[0];
    if (this.$targetscene.iscroll) {
      this.$targetscene.iscroll.scrollToElement(anchor, 500);
    } else {
      anchor.scrollIntoView();
    }
  };
  $info_az.bind('touchmove', function(event) {
    var $this = $(this);
    var origin = $this.find(':first-child').offset().top;
    var step = ($this.find(':last-child').offset().top-origin)/26;
    var y = event.originalEvent.touches[0].clientY;
    var offset = Math.max(Math.min(Math.floor((y-origin)/step), 26), 0);
    var initial = offset < 26 ? String.fromCharCode(65+offset) : '0';
    $info_az.goto(initial);
  });
  $info_az.bind(clickend, function() { $info_az.removeClass('touched') });
  $info_az.click(function(event) { return false });
}

// ======================= INFO MENU ===============================

function initinfo_menu() {

  $info_menu = $('#info_menu');
  $info_menu.clear = function() {
    this.html('<ul></ul>');
    this.actions = {};
    return this;
  };
  $info_menu.add = function(text, action) {
    var liclass = (text.indexOf('<br>')<=0) ? '' : 'long';
    this.find('ul').append('<li class="' +liclass+ '">' + text + '</li>');
    this.actions[text] = action;
    return this;
  };
  $info_menu.show = function($source) {
    this.$source = $source;
    this.find('li').touch(function(event) {
      $(event.currentTarget).addClass('selected');
      setTimeout(function() { $info_menu.act() }, showtouched_ms);
    });
    this.css({left: '', top: ''}); // ?? previous position distorts reported dimensions
    var menu = {width: this.outerWidth(), height: this.outerHeight()};
    var source = {left: $source.offset().left, top: $source.offset().top, height: $source.outerHeight()};
    var gap = 12;
    menu.left = source.left - gap - menu.width;
    menu.top = source.top + (source.height-menu.height)/2;
    menu.top = Math.min(menu.top, $('body').height()-menu.height);
    this.css({left: menu.left, top: menu.top});
    this.append($('#snippets .menuptr').html());
    var ptrheight = 44;
    var kludge = {x: 7, y: 6};
    this.find('img').css({left: menu.width-kludge.x, top: source.top-menu.top+source.height/2-ptrheight/2-kludge.y});
    this.css('display', 'block');
    $('#info_menushield').show();
  };
  $info_menu.act = function() {
    $info_menu.actions[$info_menu.find('.selected').html()]();
    $info_menu.hide();
  };
  $info_menu.hide = function(quickly) {
    this.fadeOut(quickly?0:500);
    if (this.$source) this.$source.removeClass('clicked');
    $('#info_menushield').hide();
  };
  $info_menu.selectfirst = function() { 
    var $test = $info_menu.find('li:first-child')
    $test.touch();
    };
  $('#info_menushield').touch(function(event) { $info_menu.hide() });
}

// ======================= PREVIEW <-> INFO ===============================

function initinfo_preview() {

  $info_preview = $('#info_preview');
  initpreview();
  $info_preview.expose = function(mediakey, source) {
    this.mediakey = mediakey;
    $preview.load(mediakey);
    this.thumbtransform = calctransform($info, source.find('div.thumbmarker'));
    this.css(cssdialect('-webkit-transform'), this.thumbtransform).show();
    if (inspectthumb == 1) return;
    setTimeout(function() { $info_preview.expose2() }, domdelay);
  };
  $info_preview.expose2 = function() {
    this.maketransition(function($this) {
      $this.css(cssdialect('-webkit-transform'), '');
    }, function($this) {
      if (inspectthumb == 1) return;
      doitsoon(function() {
        $preview.setscrollingtracks();
        $preview.setscrollinglyrics();
        $info.flagplayingmedia();
        $info.flagqueuedmedia();
        $preview.requestlyrics($info_preview.mediakey.split('|')[0]);
      });
    });
  };
  $info_preview.conceal = function(quickly) {
    if (!this.is(':visible')) return;
    if ($preview.iscrolltracks) $preview.iscrolltracks.destroy();
    $preview.iscrolltracks = null;
    if ($preview.iscrolllyrics) $preview.iscrolllyrics.destroy();
    $preview.iscrolllyrics = null;
    if (quickly) {
      this.hide();
    } else {
      this.maketransition(function($this) {
        $this.css(cssdialect('-webkit-transform'), $info_preview.thumbtransform);
      }, function() {
        doitsoon(function() {$info_preview.hide() });
      });
    }
  };
  $info_preview.kill = function() { this.conceal(true) };
}

// ======================= PREVIEW ===============================

function initpreview() {

  $preview = $('#info_preview');
  $preview.load = function(mediakey) {
    var bits = mediakey.split('|');
    var albumid = bits[0];
    this.html(jsdb.getalbumdetails(albumid, true));
    this.find('.albuminfo').after($('#snippets .ribbon').html());
    this.find('.ribbon').append('<div class="close colourbutton black right">Close</div>');
    this.setcolourbuttonevents();
    this.find('.all').touch(function(event) {
      if (this.innerHTML.match(/Tick/)) {
        $preview.$trackrows.addClass('ticked');
      } else {
        $preview.$trackrows.filter('tr.ticked').removeClass('ticked');
      }
      $preview.refreshribbon();
    });
    this.find('.play,.queue').attr('title', $info_preview.mediakey).touch(function(event) {
      var mediakey = this.title;
      var playnow = this.innerHTML.match(/^P/);
      var nbrticked = $preview.find('tr.ticked').length;
      if (nbrticked == $preview.$trackrows.length) {
        playerlink.addmedia(mediakey.split('|')[0], playnow, true);
      } else {
        var mediakeys = [];
        $preview.$trackrows.filter('tr.ticked').each(function() { mediakeys.push(this.title) });
        if (mediakeys.length) playerlink.addmedia(mediakeys.join('@'), playnow, true);
      }
    });
    this.find('table').removeClass('slim');
    this.$trackrows = this.find('tr').not('.bonus');
    this.$trackrows.find('td.tracknbr').after($('#snippets .preview_tick').html());
    this.$trackrows.find('td.tracktitle').prepend('<div class=thumbmarker></div>');
    this.$trackrows.addClass('ticked');
    this.refreshribbon();
    this.lyrics = '';
    this.$trackrows.click(function(event) {
      if ($info_menu.is(':visible')) $info_menu.hide();
      var $currentrow = $(event.currentTarget);
      $currentrow.addClass('clicked');
      var mediakey = this.title;
      $info_menu.clear();
      $info_menu.add('Play track', function() { playerlink.addmedia($currentrow[0].title, true, true) });
      var queued = $currentrow.hasClass('queued');
      $info_menu.add((queued?'Unqueue':'Queue')+' track', function() { $currentrow.toggleClass('queued', !queued); playerlink.togglequeued($currentrow[0].title, !queued) });
      var ticked = $currentrow.hasClass('ticked');
      $info_menu.add((ticked?'Untick':'Tick')+' track', function() { $currentrow.toggleClass('ticked', !ticked); $preview.refreshribbon() });
      if ($currentrow.hasClass('haslyrics')) $info_menu.add('Lyrics', function() {$preview.showlyrics(mediakey, $currentrow.find('.thumbmarker'))} );
      if (expserv) $info_menu.add('Edit lyrics', function() {$preview.editlyrics(mediakey)} );
      $info_menu.show($currentrow);
    });
    this.find('div.lyrics img').touch(function(event) { $preview.hidelyrics() });
    this.find('.close').touch(function(event) { $info_preview.conceal() });
  };
  $preview.refreshribbon = function() {
    this.find('.all').html((this.$trackrows.filter('tr:not(.ticked)').length?"Tick":"Untick")+' all tracks');
    this.find('.play,.queue').each(function() { $(this).html($(this).html().replace(/\d+/, $preview.$trackrows.filter('tr.ticked').length)) });
  };
  $preview.requestlyrics = function(albumid) {
    $.ajax({url: '/cgi-bin/lyrics.cgi?mediakey=' + albumid, context: this, success: this.storelyrics });
  };
  $preview.storelyrics = function(lyrics) {
    this.lyrics = lyrics.split('|');
    $.each(this.lyrics, function(nbr, text) { if (text) $($preview.$trackrows[nbr]).addClass('haslyrics') });
  };
  $preview.setscrollingtracks = function() {
    var $viewport = this.find('.viewport.tracks');
    this.$viewport = $viewport;
    $viewport.width(this.width()).height(this.height()-$viewport.position().top);
    if (useiscroll && !this.iscrolltracks) this.iscrolltracks = new iScroll($viewport[0], {hScrollbar: false});
  };
  $preview.setscrollinglyrics = function() {
    var $viewport = this.find('.viewport.lyrics');
    $viewport.width(this.$viewport.width()-2).height(this.$viewport.height()-2); // -2 for border
    $viewport.css('top', this.$viewport.position().top); 
    if (useiscroll) this.iscrolllyrics = new iScroll($viewport[0]);
  };
  $preview.scrolltomaybe = $info.scenes.about.scrolltomaybe; // (any scene)
  $preview.showlyrics = function(mediakey, thumbmarker) {
    var text = this.lyrics[mediakey.split('|')[1]-1];
    if (!text) return;
    var $lyrics = this.find('.lyrics');
    $lyrics.find('.text').html(text);
    this.thumbtransform = calctransform(this.find('div.tracksorlyrics'), $(thumbmarker));
    $lyrics.css(cssdialect('-webkit-transform'), this.thumbtransform).show();
    if (inspectthumb) return;
    setTimeout(function() { $preview.showlyrics2() }, domdelay);
  };
  $preview.showlyrics2 = function() {
    this.maketransition(function($this) {
      $this.find('div.lyrics').css(cssdialect('-webkit-transform'), '');
    }, function($this) {
      if ($preview.iscrolllyrics) $preview.iscrolllyrics.refresh();
    });
  };
  $preview.hidelyrics = function() {
    this.maketransition(function($this) {
      $this.find('div.lyrics').css(cssdialect('-webkit-transform'), $preview.thumbtransform);
    }, function($this) {
      $this.find('div.lyrics').hide();
    });
  };
  $preview.editlyrics = function(mediakey) { window.open('http://' +window.location.hostname+ ':' +window.location.port+ '/cgi-bin/editlyrics.cgi?mediakey=' +mediakey, 'editliyrics') };
}

// ======================= PEDALS ===============================

function initpedals() {
  
  $pedals = $('#pedals');
  $pedals.selected = localStorage.pedals ? JSON.parse(localStorage.pedals) : {'pop': 'top20artists', 'cla': 'top20composers', 'misc': 'albumartists', 'nowplaying': 'album'};
  $pedals.touch(function(event) { $info_menu.hide() });
  $pedals.find('div.radio').touch(function(event) { $info.select(this.id.substring(6)) });
  $pedals.show = function(scenename) {
    var tabname = scenename.split('_')[0];
    this.find('>div.radiogroup').hide();
    this.find('>div.'+tabname).show();
    this.find('div.'+tabname+' div.selected').removeClass('selected');
    this.find('div#pedal_'+scenename).addClass('selected');
  };
  $pedals.find('button.reload').touch(function(event) { location.reload() });
}

// ======================= JSDB ===============================

function initjsdb() {
  
  $.each('albums tracks pieces search'.split(' '), function() {
    $.ajax({ url: '/data/jsdb/' +this+ '.txt', async: false, cache: false, context: this, success: function(indextext) { jsdb[this] = indextext }});
  });
  jsdb.getalbum = function(albumid) {
    var matches = this.albums.match(RegExp('^' +albumid+ '@(.*)$', 'm'));
    if (!matches) matches = ['', albumid];
    var bits = matches[1].split('|');
    return {genre: bits[0], artist: bits[1], title: bits[2], secs: bits[3], year: bits[4], added: bits[5]};
  };
  jsdb.getalbumdetails = function(albumid, withlyrics) {
    var album = this.getalbum(albumid);
    var tracks = this.gettracks(albumid);
    var nbrtracks = tracks.length;
    var html = '<img class=cover src="/data/covers/' +albumid+ '.jpg">\n';
    var title = '<div class=albumtitle>' + album.title + '</div>\n';
    var artist = '<div class="albumartist google">' +album.artist+ '</div>\n';
    var albuminfo = album.genre == 'cla' ? artist+title : title+artist;
    var statssep = '&nbsp;&middot;&nbsp;';
    var stats = nbrtracks + ' tracks' + statssep + hhmmss(album.secs);
    if (album.genre == 'pop') stats += statssep + 'Released ' + album.year;
    stats +=  statssep + 'Added ' + album.added;
    albuminfo += '<div class=stats>' + stats + '</div>\n';
    html += '<div class=albuminfo>\n' + albuminfo + '</div>\n';
    var trackrows = '';
    var bonusrow = '<tr class=bonus><td colspan=4>&mdash; Bonus Tracks &mdash;</td></tr>\n';
    for (var tracknbr=1; tracknbr<=nbrtracks; tracknbr++) {
      var trackrow = '<td class="tracknbr flag">' +tracknbr+ '.</td>\n';
      var track = tracks.shift().split('\t');
      var trackartist = track[1] ? '<div class="artist weak">' + track[1]+ '</div>' : '';
      trackrow += '<td class="tracktitle strong">' + (album.genre == 'cla' ? trackartist+track[0] : track[0]+trackartist) + '</td>\n';
      trackrow += '<td class=trackduration>' +hhmmss(track[2])+ '</td>\n';
      trackrow = '<tr class="' +albumid+ ' ' +albumid+'_'+tracknbr+ '" title="' +albumid +'|' +tracknbr+ '">\n' + trackrow + '</tr>\n';
      if (track[3].indexOf('B')>=0) {
        trackrow = bonusrow+trackrow;
        bonusrow = '';
      }
      trackrows += trackrow;
    }
    if (withlyrics) {
      var tracksviewport = '<div class="viewport tracks">\n<div class=scrollable>\n<table class="albumtracks listing slim">\n' + trackrows + '</table><br>\n</div>\n</div>\n';
      var lyricsviewport = '<div class="viewport lyrics">\n<div class=scrollable>\n' + $('#snippets .previewlyrics').html() + '<br></div>\n</div>\n';
      html += '<div class=tracksorlyrics>' +tracksviewport+lyricsviewport+ '</div>\n';
    } else {
      html += '<div class=viewport>\n<div class=scrollable>\n<table class="albumtracks listing">\n' + trackrows + '</table><br>\n</div>\n</div>\n';
    }
    return html;
  };
  jsdb.gettrackorpiece = function(mediakey) {
    var bits = mediakey.split('|');
    var albumid = bits[0];
    var album = this.getalbum(albumid);
    var tracknbrs = bits[1];
    if (!tracknbrs) tracknbrs = "1";
    var matches = (tracknbrs.indexOf('-')<0?this.tracks:this.pieces).match(RegExp('^' +albumid+ '@(.*)$', 'm'));
    if (!matches) return {albumid: albumid, album: {}, title: 'track '+tracknbrs};
    bits = matches[1].split('|')[parseInt(tracknbrs)-1].split('\t');
    return {albumid: albumid, album: album, title: bits[0], artist: (bits[1] ? bits[1] : album.artist), secs: (bits[2] ? bits[2] : 0) , unlucky: (bits[3] && bits[3].indexOf('U')>=0), bonus: (bits[3] && bits[3].indexOf('B')>=0)};
  };
  jsdb.gettracks = function(albumid) {
    var matches = this.tracks.match(RegExp('^' +albumid+ '@(.*)$', 'm'));
    return matches ? matches[1].split('|') : [];
  };
  jsdb.getnbrtracks = function(albumid) {
    return this.gettracks(albumid).length;
  };
  jsdb.explodealbum = function(albumid) {
    var mediakeys = [];
    for (var tracknbr=this.getnbrtracks(albumid); tracknbr>=1; tracknbr--) { mediakeys.unshift(albumid+'|'+tracknbr) }
    return mediakeys;
  };
  jsdb.getgallery = function(albumid, query) {
    var album = this.getalbum(albumid);
    var artist = '<div class=artist>' +highlightmaybe(album.artist, query)+ '</div>';
    var title = '<div class="title flag">' +highlightmaybe(album.title, query)+ '</div>';
    var lihtml = '<img class=cover src="/data/covers/' +albumid+ '.jpg"><div class=thumbmarker></div>' + (album.genre == 'cla' ? artist+title : title+artist);
    return '<li class=hotspot title="' +albumid+ '">' +lihtml+ '</li>';
  };
  jsdb.getlisting = function(mediakey, query) {
    var trackorpiece = this.gettrackorpiece(mediakey);
    var title = '<td class="title flag">' +highlightmaybe(trackorpiece.title, query)+ '</td>';
    var artist = '<td class=artist>' +highlightmaybe(trackorpiece.artist, query)+ '</td>';
    var trhtml = '<td class=cover><div class=thumbmarker></div><img src="/data/covers/' + trackorpiece.albumid + '.jpg"></td>' + (trackorpiece.album.genre == 'cla' ? artist+title : title+artist);
    return '<tr class=hotspot title="' +mediakey+ '">' + trhtml + '</tr>';
  };
  jsdb.getrandomalbum = function(genre) {
    var newline = this.albums.match(/[\r\n]+/)[0];
    try {
      var chunklen = 300;
      var startpos = Math.floor(Math.random()*(this.albums.length-chunklen));
      var chunk = this.albums.substring(startpos, startpos+chunklen);
      var matches = chunk.split(newline)[1].match(RegExp('^(\\w+)@'+genre));
      if (!matches) return this.getrandomalbum(genre); // forestall firebug
      return matches[1]; // albumid
    } catch (e) {
      // alert('jsdb.getrandomalbum(): retry'); // e.g. wrong genre
      return this.getrandomalbum(genre);
    }
  };
  jsdb.getrandomtracks = function(genre, howmany) {
    var artists = {};
    var tracks = [];
    while (howmany) {
      var mediakey = this.getrandomtrack(genre);
      var artist = mediakey.split('_')[0];
      if (artists[artist]) continue;
      artists[artist] = 1;
      if (this.gettrackorpiece(mediakey).unlucky) {
  //      alert('getmysterytracks(): unlucky: ' +mediakey);
        continue;
      }
      tracks.push(mediakey);
      howmany--;
    }
    return tracks.join('@');
  };
  jsdb.getrandomtrack = function(genre) {
    var albumid = this.getrandomalbum(genre);
    var tracknbr = 1+Math.floor(Math.random()*this.getnbrtracks(albumid));
    return albumid + '|' + tracknbr;
  };
  jsdb.dosearch = function(query) {
    if (query == '') return null;
    var regex = '^.*' + slashpunct(query).toLowerCase() + '.*@.*$';
    var prevquery = this.query;
    this.query = query;
    if (prevquery && query.indexOf(prevquery) == 0) {
      this.results = this.results.join('\n').match(RegExp(regex, 'gm'));
    } else {
      this.results = this.search.match(RegExp(regex, 'gm'));
    }
    return this.results? this.results.slice(0) : []; // copy
  };
  jsdb.getalbumortrackorpiece = function(mediakey) { return mediakey.indexOf('|')>=0 ? this.gettrackorpiece(mediakey) : this.getalbum(mediakey) };
}

// ======================= PLAYER LINK ===============================

function initplayerlink() {

  playerlink.status = {};
  playerlink.addmedia = function (mediakeys, playnow, history) { // can be multiple keys separated by @ (mystery or preview album tracks)
    this.simulateaddmedia(mediakeys, playnow);
    var cmds = '&playfile='+escape(mediakeys);
    if (playnow) {
      $dashnowplaying.refresh().zoom();
      $dashplayer.refreshtimeline(0);
      $dashqueue.refresh();
      cmds = '&stop&delete' +cmds+ '&play=';
    } else {
      $dashqueue.refresh().zoom();
    }
    this.httpQproxied(cmds);
    $info.scenes.sound.select();
    $info.scenes.history.update(mediakeys.split('@')[0]);
  };
  playerlink.getqueueindex = function(queuekey) {
    var result = null;
    $.each(player.queue, function(index, key) {
      if (key != queuekey) return;
      result = index;
      return false;
    });
    return result;
  };    
  playerlink.togglequeued = function(mediakey, add) {
    if (add) {
      this.addmedia(mediakey, false, true);
    } else {
      var albumid = mediakey.split('|')[0];
      if (mediakey != albumid) { // track
        $.each(player.queue, function() {
          if (this.split('@')[0] == albumid) {
            playerlink.explodequeuealbum(albumid);
            return false;
          }
        });
      }
      var queuekeys = [];
      $.each(player.queue, function() {
        if (this.split('@')[0] == mediakey) queuekeys.push(this);
      });
      this.prunequeue(queuekeys, true);
    }
  };
  playerlink.prunequeue = function(prunekeys, zoom) { // prunekeys[] must be subset of player.queue[] in proper order
    this.simulateprunequeue(prunekeys);
    if (zoom) $dashqueue.refresh().zoom();
    var params = '';
    $.each(prunekeys.reverse(), function() {
      var bits = this.split('@')[1].split('-');
      if (bits[1]===undefined) bits[1] = bits[0];
      for (var pos=parseInt(bits[1]); pos>=bits[0]; pos--) { // parseInt() apparently necessary
        params += '&deletepos=' +  (pos-1).toString()
      }
    });
//    if (params) this.httpQproxied(params);
    if (params) { this.httpQproxied(params); console.log(params); }
  };
  playerlink.sync = function() { this.requeststatus("/cgi-bin/slowload.cgi?f=llama.json&n=player&n2=playerlink.status") };
  playerlink.requeststatus = function(url) {
    var hismaxsecs = 3*60
    $.ajax({ url: url + "&m=" + hismaxsecs, context: this, error: this.fault, success: this.success });
  };
  playerlink.fault = function(jqXHR, errmess) {
    setTimeout(function() { playerlink.sync() }, 10*1000); // schedule a retry in 10 seconds
    if (expserv) console.log('Player link error: message = "' +errmess+ '", status = ' + jqXHR.status);
//    if (expserv) alert('Player link error: message = "' +errmess+ '", status = ' + jqXHR.status);
    if (jqXHR.status == 0) return;
    player.fault = errmess;
    this.showsync(false);
    doitsoon(function() { playerlink.updatedisplay() }); // avoid nested request
  };
  playerlink.success = function(response, mess, jqXHR) {
    if (response.substring(0, 10) != "player = {") {
      this.fault(jqXHR, "slowload.cgi strange responseText: " + response.substring(0, 10));
      return;
    }
    try {
      eval(unescape(response)) // sets player and playerlink.status
      if (player.playmode == "PLAY")
        player.secsplayed += playerlink.status.loaddelay;
    } catch(e) {
      this.fault(jqXHR, 'eval() error: "' + e.message + '" in:\n' + response);
      return;
    }
    if (playerlink.status.errmsg) {
      this.fault(jqXHR, 'Slowload.cgi: ' + playerlink.status.errmsg);
      return;
    }
    if (player.errmsg) {
      this.fault(jqXHR, 'Llama.pl: ' + player.errmsg);
      return;
    }
    player.fault = null;
    this.showsync(true);
    if (playerlink.status.newdata) doitsoon(function() { playerlink.updatedisplay() }); // avoid nested request
    this.requeststatus(playerlink.status.newurl)
  };
  playerlink.updatedisplay = function() {
    $dashnowplaying.refresh();
    $dashplayer.refresh();
    $dashqueue.refresh();
    $info.flagplayingmedia();
    $info.flagqueuedmedia();
    $info.selected.refresh();
  };
  playerlink.resyncaftersleepmaybe = function() {
    var now = new Date();
    var msecs = now.getTime();
    if (this.intervalstarted) {
      if (msecs > this.intervalstarted+5000) { // slept
        this.sync();
      }
    }
    this.intervalstarted = msecs;
  };
  playerlink.httpQ = function(cmd, params)   { $.ajax({ url: 'http://' +document.location.hostname+ ':4800/' +cmd+ '?p=' +(params? "&"+params : "") }); this.showsync(false) };
  playerlink.httpQproxied = function(params) { $.ajax({ url: '/cgi-bin/proxyget.cgi?black=white' +params }); this.showsync(false) };
  playerlink.simulateaddmedia = function(mediakeys, playnow) {
    if (playnow) {
      player.playmode = 'PLAY';
      player.externalmedia = null;
      player.mediakey = mediakeys.split('@')[0];
      player.queue = removefirsttrack(mediakeys.split('@'));
      player.secsplayed = 0;
      player.tracksecs = jsdb.gettrackorpiece(player.mediakey).secs;
    } else {
      player.queue = player.queue.concat(mediakeys.split('@'));
    }
  };
  playerlink.simulateprunequeue = function(prunekeys) { // prunekeys[] must be subset of player.queue[] in proper order
  // must renumber: do for actual simulation only
//    var newqueue = [];
//    var myprunekeys = prunekeys.slice(0);
//    var prunekey = myprunekeys.shift();
//    $.each(player.queue, function(i, queuekey) {
//      if (queuekey != prunekey) { // *not* this
//        newqueue.push(queuekey);
//        prunekey = myprunekeys.shift();
//      }
//    });
//    player.queue = newqueue;
  };
  playerlink.explodequeuealbum = function(albumid) {
    var newqueue = [];
    $.each(player.queue, function() {
      var newentry = this;
      if (this.split('@')[0] == albumid) {
        var nbrtracks = jsdb.getnbrtracks(albumid);
        var pos = this.split('@')[1].split('-')[0];
        newentry = [];
        for (var tracknbr=1; tracknbr<=nbrtracks; tracknbr++,pos++) { newentry.push(albumid+'|'+tracknbr+'@'+pos) }
      }
      newqueue = newqueue.concat(newentry);
    });
    player.queue = newqueue;
  };
  playerlink.showsync = function(syncstate) { $('body').toggleClass('synced', syncstate) };
  if (simulator) {
    playerlink.sync = function() {};
    playerlink.httpQ = function() {};
    playerlink.httpQproxied = function() {};
    player.playmode = 'STOP';
    player.queue = [];
    $dashnowplaying.refresh();
    $dashplayer.refresh();
    $dashqueue.refresh();
  }
}

// ======================= UTILITY ===============================

function initutils() {
  
  $.fn.reverse = [].reverse;
  $.fn.setcolourbuttonevents = function() {
    this.find('.colourbutton').each(function() {
      var $this = $(this);
      $this.bind(clickstart, function() {
        $this.addClass('touched');
        setTimeout(function() { $this.removeClass('touched') }, showtouched_ms); // (clickend missing if button obscured)
      });
    });
    return this;
  }
  $.fn.zoom = function() {
    this.addClass('panel');
    if (inspectthumb) return;
    this.maketransition(function($this) {
      $this.addClass('zoomed');
    }, function($this) {
      $this.maketransition(function($this) {
        $this.removeClass('zoomed');
      }, function($this) {
        $this.removeClass('panel');
      });
    });
  }
  $.fn.maketransition = function(startfunc, atendfunc) {
    this.unbind(transitionend).bind(transitionend, function() {
      $(this).unbind(transitionend);
      atendfunc($(this));
    });
    startfunc($(this));
  }
  $.fn.touch = function(action) {
    return this.each(function() {
      if (action) {
        $(this).bind(clickstart, function(event) { event.preventDefault(); action.call(this, event) });
      } else {
        $(this).trigger(clickstart);
      }
    });
  };
}

//----------------------------------------------------------------------------------
  
function translatestylesheet() {

  doitsoon(function() { $.ajax({url: this.location, success: webkit2moz })});
}
    
//------------------------------------------------------------------------
function webkit2moz(oldsheet) {

  var newrules = [];
  $.each(oldsheet.split(/[\r\n]+/), function() {
    if (this.match(/-webkit-/)) newrules.push(cssdialect(this));
  });
  changestylesheet(newrules, 'webkit2moz');
}

//------------------------------------------------------------------------
function cssdialect(rule) {

  if ($.browser.webkit) return rule;
  if (rule.match(/gradient/)) {
    rule = rule.replace(/-webkit-gradient\(linear, left top, left bottom/, '-moz-linear-gradient(top');
    rule = rule.replace(/from\((.*?)\)/, '$1 0%');
    rule = rule.replace(/color-stop\((\d+%), (.*?)\)/g, '$2 $1');
    rule = rule.replace(/to\((.*?)\)/, '$1 100%');
    console.log(rule);
    return rule;
  }
  return rule.replace(/-webkit-/g, '-moz-');
}

//------------------------------------------------------------------------
function changestylesheet(rules, caller) {

  $('head style.' + caller).remove();
  $('head').append('<style class=' + caller + '>' + rules.join('\n') + '</style>');
}

//------------------------------------------------------------------------
function hhmmss(secs) {
  
  var mins = Math.floor(secs/60);
  secs -= mins*60;
  var hrs;
  if (mins>59) {
    hrs = Math.floor(mins/60);
    mins = hrs + ":" + String(100+mins-hrs*60).substr(1,2);
  }
  return mins + ":" + String(100+secs).substr(1,2);
}

//----------------------------------------------------------------------------------
function resize() {
  
  $dashnowplaying.resize();
  $tabs.resize();
  $info.resize();
  $info_az.resize();
}

//------------------------------------------------------------------------
function doitsoon(doit, rightnow) {
  
  if (rightnow) { doit() } else { setTimeout(doit, 0) }
}

//------------------------------------------------------------------------
function highlightmaybe(text, query) {
  
  if (query===undefined) return text;
  query = slashpunct(query);
  if (text.match('&#')) {
    query = query.replace(/a/ig, '(a|&#(192|193|194|195|196|197|224|225|226|227|228|229);)');
    query = query.replace(/ae/ig, '(ae|&#(198|230);)');
    query = query.replace(/c/ig, '(c|&#(199|231);)');
    query = query.replace(/e/ig, '(e|&#(200|201|202|203|232|233|234|235);)');
    query = query.replace(/i/ig, '(i|&#(204|205|206|207|236|237|238|239);)');
    query = query.replace(/d/ig, '(d|&#(208|240);)');
    query = query.replace(/n/ig, '(n|&#(209|241);)');
    query = query.replace(/o/ig, '(o|&#(210|211|212|213|214|216|242|243|244|245|246|248);)');
    query = query.replace(/u/ig, '(u|&#(217|218|219|220|249|250|251|252);)');
    query = query.replace(/y/ig, '(y|&#(221|253|255);)');
    query = query.replace(/p/ig, '(p|&#(222|254);)');
    query = query.replace(/b/ig, '(b|&#(223|254);)');
  }
  return text.replace(RegExp('('+ query+')', 'ig'), '<span class=query>$1</span>');
}

//------------------------------------------------------------------------
function slashpunct(text) {
  
  return text.replace(/([^\w])/g, '\\$1');
}

//----------------------------------------------------------------------------------
function calctransform($large, $smallmarker) {

  var large = {width: $large.width(), height: $large.height()};
  large = $.extend(large, {left: $large.offset().left+large.width/2, top: $large.offset().top+large.height/2}); // centre
  var smallmarker = $smallmarker.offset();
  smallmarker = $.extend(smallmarker, {height: $smallmarker.height()});
  var small = {left: smallmarker.left, top: smallmarker.top+smallmarker.height/2}; // centre
  var scale = smallmarker.height/large.height;
  var offset = {x: small.left-large.left, y: small.top-large.top};    
  return 'translate(' + offset.x + 'px, ' + offset.y + 'px) scale(' + scale +')';
}

//----------------------------------------------------------------------------------
function removefirsttrack(keys) {
  
  var bits = keys.shift().split('|');
  if (!bits[1]) { // album
    var albumid = bits[0];
    for (var tracknbr=jsdb.getnbrtracks(albumid); tracknbr>=2; tracknbr--) { keys.unshift(albumid+'|'+tracknbr) }
  }
  return keys;
}

