// $Id: gallery.js 2691 2012-01-25 20:38:16Z sdalu $

/*
 * Copyright (c)  2007-2009  Stephane D'Alu
 * http://www.sdalu.com/
 */

/*
 * #cart-default
 * #thumbnail-sizer
 *
 */

var Gallery = (typeof module !== "undefined" && module.exports) || {};

(function (exports) {
exports.name                = "gallery.js";
exports.version             = "0.5.0";
exports.highslide_start     = hs_start;
exports.thumbnail_sizer     = thumbnail_sizer;
exports.cart_checkout       = function(store, opts) {
    cart_gallery_render(store, opts);
    cart_checkout_render(store, opts) };
exports.store_create        = store_create;
exports.store_enhance_order = store_enhance_order;
exports.thumbnail_as_link   = gallery_thumbnail_as_link;
exports.thumbnail_panels    = gallery_panels_render;
exports.map_vs_list         = gallery_list_vs_map;
exports.map_render          = gallery_handle_map;
exports.highslide_prepare   = hs_prepare;

//======================================================================

// Cookie
var cookie = function() {};
if (typeof MoXoW !== 'undefined')	cookie = MoXoW.cookie;
else if ($.cookie)    			cookie = $.cookie;

// Lang
var LANG   = (typeof MoXoW !== 'undefined' 
	      ? MoXoW.getLang() : $('html').attr('lang')) || 'en';


//== Localization ======================================================

var L10N = { fr: {} };

L10N.fr.cart = {
    cart_empty_q      : 'Vider le panier?',
    cart_confirm_q    : 'Confirmer le panier?',
    item_missing      : 'L\'objet n\'existe pas?!'
};

L10N.fr.sizer     = {
    smaller           : 'Plus petit',
    larger            : 'Plus large'
};

L10N.fr.highslide = {
    cssDirection      : 'ltr',
    loadingText       : 'Chargement...',
    loadingTitle      : 'Cliquer pour annuler',
    focusTitle        : 'Cliquer pour amener au premier plan',
    fullExpandTitle   : 'Afficher à la taille réelle',
    creditsText       : 'Développé sur <i>Highslide JS</i>',
    creditsTitle      : 'Site Web de Highslide JS',
    previousText      : 'Précédente',
    nextText          : 'Suivante',
    moveText          : 'Déplacer',
    closeText         : 'Fermer',
    closeTitle        : 'Fermer (esc ou Échappement)',
    resizeTitle       : 'Redimensionner',
    playText          : 'Lancer',
    playTitle         : 'Lancer le diaporama (barre d\'espace)',
    pauseText         : 'Pause',
    pauseTitle        : 'Suspendre le diaporama (barre d\'espace)',
    previousTitle     : 'Précédente (flèche gauche)',
    nextTitle         : 'Suivante (flèche droite)',
    moveTitle         : 'Déplacer',
    fullExpandText    : 'Taille réelle',
    number            : 'Image %1 of %2',
    restoreTitle      : 'Cliquer pour fermer l\'image,'
	              +' cliquer et faire glisser pour déplacer,'
                      +' utiliser les touches flèches droite et gauche'
	              +' pour suivant et précédent.',
    restoreTitleAsNext: 'Cliquer pour passer à la suivante',
    unknown           : 'Inconnu',
    fromAlbum         : 'Dans l\'album'
};



//== Helper ============================================================

function currency(val) {
    var a = Math.floor(val);
    var b = Math.floor((val-a)*100);
    return a + '.'  + (b < 10 ? b+'0' : b ) +' €';
}

function date_ymdhis(date) {
    return date.getUTCFullYear() + '.'
         + date.getUTCMonth()    + '.'
         + date.getUTCDay()      + '-'
         + date.getUTCHours()    + ':'
         + date.getUTCMinutes()  + ':'
         + date.getUTCSeconds();
}

// This function is from Google's polyline utility.
function decode_google_polyline (encoded) {
    var len   = encoded.length;
    var index = 0;
    var array = [];
    var lat   = 0;
    var lng   = 0;

    while (index < len) {
	var b;
	var shift  = 0;
	var result = 0;
	do { b = encoded.charCodeAt(index++) - 63;
	     result |= (b & 0x1f) << shift;
	     shift += 5;
	   } while (b >= 0x20);
	var dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
	lat += dlat;
	
	shift  = 0;
	result = 0;
	do { b = encoded.charCodeAt(index++) - 63;
	     result |= (b & 0x1f) << shift;
	     shift += 5;
	   } while (b >= 0x20);
	var dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
	lng += dlng;
	
	array.push(new google.maps.LatLng(lat * 1e-5, lng * 1e-5));
    }
    
    return array;
}

//== Observer pattern ==================================================

var Observer = function() { };
Observer.prototype = {
    on: function(name, func) {
	// Create if first time
	if (!this._observe) this._observe = {};
	// Retrieve observer list
	var list = this._observe[name];
	if (!list) list = this._observe[name] = [];
	// Check if function is already registered
	if (list.some(function(f) { return f == func; })) return this;
	// Register function
	list.push(func);
	return this;
    },
    off: function(name, func) {
	// If never created, no observer to remove
	if (!this._observe) return this;
	// Remove from observer list
	this._observe[name] = this._observe[name].filter(function(f) {
	    return f != func; });
	return this;
    },
    fire: function(name) {
	// If never created, firing is useless
	if (!this._observe) return;
	// Call all the observers
	var me   = this;
	var args = Array.prototype.slice.call(arguments).splice(1);
	(this._observe[name] || []).forEach(function(fn) {
	    fn.apply(me, args); });
    },
    trigger: function() {
	this.fire.call(this, arguments);
    }
};



//== Templates =========================================================

var TPL = {};
TPL.cart_item = 
    '<div class="sc-item" onselectstart="return false" '		+
          'data-sc-photo-key="{{key}}" '				+
          'data-sc-buy-vrt="{{vrt}}">'					+
      '<table><tr>'							+
        '<td class="sc-thumbnail" rowspan="4">'				+
          '<img src="{{thumb}}" alt="img"/></td>'			+
        '<td class="sc-name" colspan="3">{{name}}</td>'			+
        '<td class="sc-panel" rowspan="4"><table>'			+
          '<tr><td class="m-button a-inc"><span>▲</span></td></tr>'	+
          '<tr><td class="m-button a-del"><span>x</span></td></tr>'	+
          '<tr><td class="m-button a-dec"><span>▼</span></td></tr>'	+
        '</table></td>'							+
      '</tr><tr>'							+
        '<td class="sc-info" colspan="3">{{info}}</td>'			+
      '</tr><tr class="sc-object">'					+
        '<td class="sc-quantity">{{quantity}}</td>'			+
        '<td class="sc-operator">x</td>'				+
        '<td class="sc-price">{{price}}</td>'				+
      '</tr><tr class="sc-cost">'					+
        '<td></td>'							+
        '<td class="sc-operator">=</td>'				+
        '<td class="sc-price">{{total}}</td>'				+
      '</tr></table>'							+
    '</div>';


TPL.checkout_summary = 
    '<table class="sc-checkout-summary">'				+
      '<tr class="sc-object">'						+
        '<td class="sc-description">{{quantity}} objet(s) en commande</td>'+
        '<td></td>'							+
        '<td class="sc-price">{{basket}}</td>'				+
      '</tr><tr class="sc-shipping">'					+
        '<td class="sc-description">Frais de livraison</td>'		+
        '<td class="sc-operator">+</td>'				+
        '<td class="sc-price">{{shipping}}</td>'			+
      '</tr><tr class="sc-cost">'					+
        '<td class="sc-description">Total</td>'				+
        '<td class="sc-operator">=</td>'				+
        '<td class="sc-price">{{cost}}</td>'				+
      '</tr>'								+
    '</table>';



//== Gallery ===========================================================

/*
 * Turn whole thumbnail into a link
 */
function gallery_thumbnail_as_link() {
    // Make cursor a pointer on whole thumbnail
    $.rule('.gallery .thumbnail').css({ cursor: 'pointer' });

    $('.gallery').on('click', '.thumbnail', function(evt) { 
	var a = $(this).find('.frame a');
	if (a) window.location.href = a.attr('href');
    });
}


/*
 * Information and panels for thumbnail
 */
function gallery_panels_render() {
    $('.gallery .thumbnail').each(function() {
	// Info
	var info      = $(this).find('> .thumb-info'     );
	var infoBtn   = $(this).find('> .thumb-info-btn' );
	if (info.length && infoBtn.length) {
	    var infoLater = $.task();
	    infoBtn.on('mouseenter', function() { 
		infoLater.cancel();
		if ( info.is(':visible')) return;
		infoLater.task(function() { info.fadeIn() }).delay();	});
	    infoBtn.on('mouseleave', function() {
		infoLater.cancel();
		if (!info.is(':visible')) return;
		info.fadeOut();						});
	}
	
	// Panel
	var pnl       = $(this).find('> .thumb-add-panel');
	var pnlBtn    = $(this).find('> .thumb-add-btn'  );
	if (pnl.length && pnlBtn.length) {
	    var pnlLater  = $.task();
	    pnlBtn.on('mouseenter', function() { 
		pnlLater.cancel();
		if ( pnl.is(':visible')) return;
		pnlLater.task(function() { pnl.fadeIn() }).delay(800);	});
	    pnlBtn.on('mouseleave', function(evt) {
		pnlLater.cancel();
		if (pnl.is(':visible') && pnl.index(evt.relatedTarget) < 0)
		    pnl.fadeOut();      				});
	    pnl.on('mouseleave', function(evt) {
		pnlLater.cancel();
		if (pnl.is(':visible') && pnlBtn.index(evt.relatedTarget) < 0)
		    pnl.fadeOut();     					});
	}
    });
}

//== Map ===============================================================

function gallery_list_vs_map() {
    $('#gallery-view-as-list').on('click', function() {
	$('#gallery-main')
	    .replaceClass('gallery-as-map',  'gallery-as-list')
	    .replaceClass('compact-view',    'full-view'      );
	$('#gallery-view-as-list').radioClass('selected');
	cookie('gallery-view-as', 'list');
    });
    $('#gallery-view-as-map').on('click', function() {
	$('#gallery-main')
	    .replaceClass('gallery-as-list', 'gallery-as-map' )
	    .replaceClass('full-view',       'compact-view'   );
	$('#gallery-view-as-map').radioClass('selected');
	cookie('gallery-view-as', 'map');
    });
    
    if (cookie('gallery-view-as') == 'list') {
	$('#gallery-main')
	    .replaceClass('gallery-as-map',  'gallery-as-list')
	    .replaceClass('compact-view',    'full-view'      );
	$('#gallery-view-as-list').radioClass('selected');
    }
}

function gallery_handle_map(geomap_country) {
/*
 * Make data for use in google maps
 */
// Create capital icon for google maps Marker
var capitalMarkerImage       = 
	new google.maps.MarkerImage('/images/red-pushpin.png',
				    new google.maps.Size(32, 32), 
				    new google.maps.Point(0,0),
				    new google.maps.Point(10, 30));

// Tweek geomap_country (from geomap.js) for google maps usage
//  - add location marker for capitals
//  - convert encoded polylines to Polygon
var country = geomap_country;
for (c in country) {
    var paths = [];
    for (var i = 0 ; i < country[c].poly.length ; i++)
        paths.push(decode_google_polyline(country[c].poly[i].points));

    country[c].loc    = new google.maps.LatLng(country[c].lat, country[c].lng);
    country[c].marker = new google.maps.Marker({position: country[c].loc, 
						title   : country[c].name,
						icon    : capitalMarkerImage});
    country[c].poly   = new google.maps.Polygon({
	clickable     : false,          paths         : paths,
	fillColor     : '#728f1f',	fillOpacity   : 0.2,
	strokeColor   : '#728f1f',	strokeOpacity : 1,
	strokeWeight  : 2
    });
}

// Where to center the map by default
var center  = new google.maps.LatLng(33, 11);



/*
 * Create map
 */
// Map creation with control elements
var map     = new google.maps.Map(document.getElementById('gallery-map'), {
    zoom                     : 2,
    center                   : center,
    mapTypeId                : google.maps.MapTypeId.TERRAIN,
    mapTypeControl           : true,
    mapTypeControlOptions    : {
      style      : google.maps.MapTypeControlStyle.DROPDOWN_MENU,
      mapTypeIds : [ google.maps.MapTypeId.ROADMAP,
                     google.maps.MapTypeId.HYBRID,
                     google.maps.MapTypeId.SATELLITE,
                     google.maps.MapTypeId.TERRAIN ] },
    streetViewControl	     : false,
    navigationControl        : true,
    navigationControlOptions : {
      style      : google.maps.NavigationControlStyle.SMALL }
});

// Take action according to zoom level (restriction and dragging)
google.maps.event.addListener(map, 'zoom_changed', function() { 
    // Ensure that zoom level is included beetwen 1 and 4
    var zoom = map.getZoom();
    if      (zoom > 4) { map.setZoom(4); } 
    else if (zoom < 1) { map.setZoom(1); } 
    
    // Desactive dragging if useless according to zoom level
    if (map.getZoom() <= 1) { 
	map.setCenter(center); 
	map.setOptions({ draggable: false });
    } else {
	map.setOptions({ draggable: true  });
    }
});



/*
 * Selection country according to thumbnail
 */
var navigation  = $('#gallery-main'   );
var info_img    = $('#gallery-img'    );
var info_caption= $('#gallery-caption');
var overlays    = [];

var select_task = $.task(function(thumb) {
    thumb = $(thumb);
    var match   = thumb.attr('id').match(/^gallery-country-(.*)/);
    if (! match) return;
    var isocode = match[1];
    var info    = country[isocode];

    // Clear previous overlays
    for (i in overlays) overlays[i].setMap(null);
    overlays.length = 0;

    // Set new overlays
    info.marker.setMap(map);
    info.poly.setMap(map);
    overlays.push(info.marker, info.poly);

    // If zoomed center map to the current capital
    if (map.getZoom() > 1) map.panTo(info.loc);

    // Extract information from thumbnail
    //  and put in view (reserved emplacement in layout)
    var img     = thumb.find('.frame img').attr('src');
    var caption = thumb.find('.caption');			  
    info_img.attr('src', img);
    info_caption.html(caption.clone());

    // Remove warning asking from selection
    navigation.removeClass('please-select'); 
});


$('.gallery')
    .on('mouseenter', '.thumbnail', function(evt) { 
	select_task.delay(350, [evt.target]);   })
    .on('mouseleave', '.thumbnail', function(evt) { 
        select_task.cancel();  		    	});


// Start with highlighting a country
var first = $('#gallery-country-fr')[0] || $('#gallery .thumbnail')[0];
if (first) select_task.now([first]);
}


//== Highslide configuration ===========================================
/*
 * Add extra configuration for diaporama
 *  class: diaporama
 *  id   : diaporama-area
 */
function hs_configure_diaporama(opts) {
    hs.restoreCursor     = null;
    hs.outlineType       = null;
    hs.wrapperClassName  = 'diaporama dark';
    hs.allowSizeReduction= false;
    hs.useBox            = true;
    hs.width             = 600;
    hs.height            = 400;
    hs.targetX           = 'diaporama-area 10px';
    hs.targetY           = 'diaporama-area 8px';
    hs.captionEval       = 'this.thumb.alt';
    hs.autoplay          = true;
    hs.lang.restoreTitle = hs.lang.restoreTitleAsNext;
 
    hs.addSlideshow({
	interval      : 5000,
	repeat        : true,
	useControls   : true,
	overlayOptions: { position  : 'bottom right',
		          offsetY   : 66               },
	thumbstrip    : { position  : 'above',
			  mode      : 'horizontal',
			  relativeTo: 'expander'       }
    });
 
    hs.Expander.prototype.onImageClick = function() {
	if (/diaporama/.test(this.wrapper.className))  return hs.next();  };
    hs.Expander.prototype.onBeforeClose = function() {
	if (/diaporama/.test(this.wrapper.className))  return false;      };
    hs.Expander.prototype.onDrag = function() {
	if (/diaporama/.test(this.wrapper.className))  return false;      };
    hs.Expander.prototype.onAfterGetCaption = function (sender) {
	var src  = this.a.getAttribute('src-extra');
	var orig = this.a.getAttribute('src-title');
	if (src && sender.caption) {
	    if (!orig) {
		var m    = src.match(/([^\/]*)\/?$/);
		var orig = m ? m[1].replace(/_/g, ' ') 
		             : '<i>'+hs.lang.unkown+'</i>';
	    }
	    sender.caption.innerHTML += '<div class="from-album">' + 
		hs.lang.fromAlbum + ': <a href="' + src + '">' + 
		orig + '</a></div>';
	}
    };

    // Keep the position after window resize
    hs.addEventListener(window, 'resize', function() {
	hs.getPageSize();
 
	for (var i = 0; i < hs.expanders.length; i++) {
	    var exp = hs.expanders[i];
	    if (exp) {
		var x = exp.x;
		var y = exp.y;
		// get new thumb positions
		exp.tpos = hs.getPosition(exp.el);
		x.calcThumb();
		y.calcThumb();
		// calculate new popup position
		x.pos        = x.tpos - x.cb + x.tb;
		x.scroll     = hs.page.scrollLeft;
		x.clientSize = hs.page.width;
		y.pos        = y.tpos - y.cb + y.tb;
		y.scroll     = hs.page.scrollTop;
		y.clientSize = hs.page.height;
		exp.justify(x, true);
		exp.justify(y, true);
		// set new left and top to wrapper and outline
		exp.moveTo(x.pos, y.pos);
	    }
	}
    });
}


/*
 * Add extra configuration for gallery
 */
function hs_configure_gallery(opts) {
    hs.outlineType      = 'glossy-dark';
    hs.wrapperClassName = 'dark';
    hs.dimmingOpacity   = 0.75;
    hs.marginLeft       = 30;
    hs.marginRight      = 30;
    hs.marginTop        = 30;
    hs.marginBottom     = 70;

    hs.addSlideshow({
	slideshowGroup: 'default',
	interval      : 5000,
	repeat        : false,
	useControls   : true,
	fixedControls : 'fit',
	overlayOptions: { overlayId     : 'controlbar',
                          opacity       : .6,
		          position      : 'bottom center',
		          offsetX       : '0',
		          offsetY       : '-15',
		          hideOnMouseOut: true },
        thumbstrip    : { mode          : 'horizontal',
		          position      : 'bottom center',
		          relativeTo    : 'viewport' }
    }); 
}


/*
 * Perform configuration of highslide
 */
function hs_configure(opts) {
    opts = opts || {};

    // English additional
    hs.lang.restoreTitleAsNext = 'Click for more next image';
    hs.lang.unknown            = 'Unknown';
    hs.lang.fromAlbum          = 'From album';

    // Perform language selection
    if (opts.l10n) hs.lang = opts.l10n;

    // Basic configuration
    hs.graphicsDir      = opts.dir || '/highslide/graphics/';
    hs.align            = 'center';
    hs.transitions      = [ 'expand', 'crossfade' ];
    hs.fadeInOut        = true;
    hs.showCredits      = false;
    hs.captionOverlay   = { fade : false };

    // Expand open image
    hs.Expander.prototype.onDoFullExpand = function() {
	window.location.href = this.custom.url;
    }
    
    // Use image alt as title
    if (opts.title) {
	hs.captionEval             = 'this.thumb.alt';
	hs.captionOverlay.position = 'above';
    }

    // Configure for gallery or diaporama
    if (opts.diaporama) hs_configure_diaporama(opts);
    else                hs_configure_gallery(opts);
}


/*
 * Action to perform when document is ready (DOM ready required):
 *   - create additional markup 
 *   - refresh highslide
 *   - start diaporama if requested
 */
var gallery_group = [];
function hs_prepare(galleries, opts) {
    opts = opts || {};

    // Mark thumbnail for use in highslide
    //  - thumbnail and thumbnail-inline can't be mixed
    galleries.each(function() {
	var gallery = $(this);
	var group   = opts.group || gallery.attr('data-slideshow-group');
	var hsopts  = { slideshowGroup  : group || 'default' };

	//-- thumbnail -----------------------------------------------
	var thumbnails = gallery.find('.thumbnail').each(function() {
	    var a = $(this).find('.frame a').addClass('highslide');
	    if (!a.length) return;
	    a[0].onclick = function() { return hs.expand(this, hsopts); };
	});
	if (thumbnails.length) return;


	//-- thumbnail-inline ----------------------------------------
	if (group) { // Add specific styling for groups
	    hsopts.wrapperClassName = 'borderless';
	    hsopts.outlineType      = 'rounded-white';
	    hsopts.dimmingOpacity   = 0.5;
	}

	var thumbnails = gallery.find('.thumbnail-inline').each(function() {
            var a = $(this).find('a').addClass('highslide');
	    if (!a.length) return;
	    a[0].onclick = function() { return hs.expand(this, hsopts); };
            $(this).find('.caption').addClass('highslide-caption');
	});


	// Ensure we only perform slideshow initialisation
	//  only once for the group
	if (gallery_group.indexOf(group) < 0) gallery_group.push(group);
	else                                  group = null;

	if (group)
	    hs.registerOverlay({
		slideshowGroup: group,
		html          : '<div class="closebutton"' 
	                          + ' onclick="return hs.close(this)"'
	                          + ' title="'+ hs.lang.closeText +'"></div>',
		position      : 'top right',
		fade          : 2,
		hideOnMouseOut: false
	    });

	if (group && thumbnails.length > 1 && hs.addSlideshow ) 
	    hs.addSlideshow({
		slideshowGroup: group,
		interval      : 5000,
		repeat        : false,
		useControls   : true,
		fixedControls : 'fit',
		overlayOptions: { opacity       : 0.8,
				  className     : 'prev-next',
				  position      : 'bottom center',
				  offsetX       : '0',
				  offsetY       : '-15',
				  hideOnMouseOut: true },
		thumbstrip    : { mode          : 'horizontal',
				  position      : 'bottom center',
				  relativeTo    : 'viewport' }
	    });
    });

    // Force highslide reload
    hs.setClickEvents();

    // Ensure document ready is performed for highslide
    hs.ready();
}



/*
 * Start highslide
 *   load script and css, performe configuration
 */
function hs_start(opts) {
    // Load highslide
    $.getCSS   ('/highslide/highslide.css');
    $.getScript('/highslide/highslide-full.packed.js', function() {
	$(function() { // DOM ready
	    // Retrieve configuration from the DOM
	    var hash = window.location.hash.substring(1);
	    var opts = $.extend({
		dir       : '/highslide/graphics/',
		l10n      : L10N[LANG] && L10N[LANG].highslide,
		title     : $('.use-highslide-title'    ).length,
		diaporama : $('.use-highslide-diaporama').length,
		highlight : document.getElementById(hash)
	    }, opts);

	    // Configure Highslide
	    hs_configure(opts); 

	    // Prepare markups
	    hs_prepare($('.highslide-gallery'), opts); 

	    // If in diaporama launch first picture
	    if (opts.diaporama)
		$('#diaporama-start').trigger('click');
	    
	    // If hash is a thumbnail launch picture
	    // XXX: Perhaps a better idea is to use CSS and :target
	    if (opts.highlight && $(opts.highlight).hasClass('thumbnail'))
		$(opts.highlight).find('a').trigger('click');
	});
    });
}



//== Thumbnail sizer ===================================================

/*
 * Add a slider allowing to do thumbnail resizing
 */
function thumbnail_sizer(sizer, opts) {
    var thumbSelector = '.gallery.imgbrowse .thumbnail';
    sizer = $(sizer || '#thumbnail-sizer');
    opts  = $.extend({ small : '/images/common/misc/thumbnail-small.png',
		       large : '/images/common/misc/thumbnail-large.png',
		       l10n  : (L10N[LANG] && L10N[LANG].sizer) || {},
		       cookie: 'thumbnail-sizer'
		     }, opts);

    // Sanity check
    //   - need to be an .gallery.imgbrowse
    //   - at least one thumbnail must be present
    //   - sizer required
    if (!$(thumbSelector).length || !sizer.length)
	return;

    // Create stylesheet/rules and attach them
    //   WARN: having {} at the end, implies it is a creation of a new rule
    //       | otherwise it will retrieve an existing rule
    var sheet         = $('<style type="text/css"/>').appendTo($('head'));
    var imgRule       = $.rule(thumbSelector + ' .frame .inner img {}')
	                 .appendTo(sheet);
    var frameRule     = $.rule(thumbSelector + ' .frame .inner     {}')
	                 .appendTo(sheet);

    // Use the first element as reference for computing 
    //  extra width added by border and padding
    var first  = $(thumbSelector + ' .frame .inner img:first');
    var tb     = first.outerHeight() - first.height();
    var lr     = first.outerWidth()  - first.width();

    // Get parameters for sizing
    var width  = parseInt(sizer.attr('data-thumb-width'  ));
    var height = parseInt(sizer.attr('data-thumb-height' ));
    var min    = parseInt(sizer.attr('data-thumb-min'    ));
    var max    = parseInt(sizer.attr('data-thumb-max'    ));
    var dflt   = parseInt(sizer.attr('data-thumb-default'));
	
    // Create updater callback
    var updater = function(value) {
	var pct      = value;
	var n_width  = Math.floor(width  * pct / 100);
	var n_height = Math.floor(height * pct / 100);
	frameRule.css({ width    : (n_width  + lr),
		        height   : (n_height + tb)  });
	imgRule.css  ({ maxWidth : n_width,
		        maxHeight: n_height   });
	// Update cookie
	cookie(opts.cookie, pct, { path: '/' });
    };

    // Construct widget
    var slider = $('<div/>')
	.css   ({ width     : 150,
		  fontSize: 10, 
		  marginLeft: 10, 
		  marginRight: 10})
	.slider({ min   : min,
		  max   : max,
		  value : dflt,
		  slide : function(evt, ui){ updater(ui.value) },
		  change: function(evt, ui){ updater(ui.value) }
		});
    var small  = $('<img class="m-button"/>')
	.attr('src',   opts.small)
	.attr('title', opts.l10n.smaller || 'Smaller')
	.on('click', function() {
	    var val = slider.slider('option', 'value') - 10;
	    slider.slider('value', val); 
	}); 
    var large  = $('<img class="m-button"/>')
	.attr('src',   opts.large)
	.attr('title', opts.l10n.larger  || 'Larger' )
	.on('click', function() {
	    var val = slider.slider('option', 'value') + 10;
	    slider.slider('value', val); 
	}); 
    var widget = $('<table/>')
	.append(($('<tr/>').append($('<td/>').append(small ))
		           .append($('<td/>').append(slider))
		           .append($('<td/>').append(large ))))
	.css({ verticalAlign: 'middle' })
        .appendTo(sizer);
	
    // Update thumbnail size
    //   According to cookie or default value
    //   (Ideally thumbnail size should be performed before dom ready)
    var val = parseInt(cookie(opts.cookie));
    slider.slider('value', val ? val : dflt);
}



//== Cart checkout =====================================================

function cart_gallery_render(store, opts) {
    // Item selection on gallery
    $('.gallery')
	// Action in panel
	.on('click', '.thumbnail .thumb-add-panel .m-button', function(evt) { 
	    var action   = $(evt.target);
	    var thumb    = action.closest('.thumbnail');
	    var item     = action.closest('.thumb-add-item');
	    var key      = thumb.attr('data-thumb-key');
	    var vrt      = item.attr('data-thumb-vrt');
	    var opt      = { src: thumb,
			     img: thumb.find('.inner img').attr('src')};
	    if (action.hasClass('a-inc')) { store.add   (key, vrt, opt); }
	    if (action.hasClass('a-dec')) { store.remove(key, vrt, opt); }
	})

	// Toggled selected button
	.on('click', '.thumbnail .thumb-checked-btn',  function(evt) { 
	    var thumb    = $(evt.target).closest('.thumbnail');
	    var key      = thumb.attr('data-thumb-key');
	    var opt      = { src: thumb };
	    store.trash(key, null, opt); 
	})

	// Adding item
	.on('click', '.thumbnail .thumb-add-btn', function(evt) { 
	    var thumb    = $(evt.target).closest('.thumbnail');
	    var pnl      = thumb.find('> .thumb-add-panel');
	    var key      = thumb.attr('data-thumb-key');
	    var opt      = { src: thumb, 
			     img: thumb.find('.inner img').attr('src')};
	    var vrt      = 'dft';

	    if (pnl) {
		var val = $('.sc-default input[type=radio]:checked').val();
		if (val) { // Retrieve matching default choice
		    var dflts = val.split(',');
		    pnl.find('.thumb-add-item').each(function() {
			var v = $(this).attr('data-thumb-vrt');
			if (dflts.indexOf(v) >= 0) {
			    vrt = v; return false;
			}
		    });
		} else {   // Take the first one defined in panel
		    vrt = pnl.find('.thumb-add-item').attr('data-thumb-vrt');
		}
	    }
	    store.add(key, vrt, opt);	
	});


    // Mark thumbnail as selected
    store.on('clear',  function(opt) {
	$('.gallery .thumbnail .thumb-checked-btn')
	    .removeClass('thumb-checked');
      });
    store.on('delete',  function(key, vrt, cnt, opt) {
	if (cnt > 0) return;
	$('.gallery .thumbnail[data-thumb-key="'+ key +
	      '"] .thumb-checked-btn').removeClass('thumb-checked');
      });
    store.on('update', function(key, vrt, qty, opt) {
	$('.gallery .thumbnail[data-thumb-key="'+ key +
	      '"] .thumb-checked-btn').addClass('thumb-checked');
      });

    // Update item panel content
    store.on('clear',  function(opt) {
	$('.gallery .thumbnail .thumb-add-quantity')
	    .html('0');
      });
    store.on('delete',  function(key, vrt, cnt, opt) {
	$('.gallery .thumbnail[data-thumb-key="'+ key +'"]'        +
	      ((vrt != null) ? ' [data-thumb-vrt="' + vrt + '"]' : '') +
	      ' .thumb-add-quantity').html('0');
      });
    store.on('update', function(key, vrt, qty, opt) {
	$('.gallery .thumbnail[data-thumb-key="'+ key +'"] [data-thumb-vrt="' +
	      vrt + '"] .thumb-add-quantity').html(qty.toString());
      });
}

function cart_checkout_render(store, opts) {
    opts  = $.extend({ l10n  : (L10N[LANG] && L10N[LANG].cart) || {},
		     }, opts);

    /* Checkout
     */
    // Insert checkout summary
    var checkout = $('.sc-checkout')
	.attr('disabled', 'disabled' )
	.append(Mustache.render(TPL.checkout_summary, {
		   basket  : currency(0), 
		   shipping: currency(0), 
		   cost    : currency(0), 
	           quantity: 0 }));

    // Action on checkout: Emtpy
    $('.sc-checkout-empty').on('click', function() {
	if (! store.getCount()) return;
	if (! confirm(opts.l10n.cart_empty_q)) return;
	store.empty();
	store.save();
    });

    // Action on checkout: Confirm
    $('.sc-checkout-validate').on('click', function() {
        if (! store.getCount()) return;
	if (! confirm(opts.l10n.cart_confirm_q)) return;
	store.save();
	store.order();
    });

    // Update checkout content
    var updateCheckout = function() {
	var basket   = store.getTotal();
	var shipping = basket ? store.getShipping() : 0;
	var cost     = basket + shipping;
	var quantity = store.getCount();
	checkout.each(function() {
	    var node = $(Mustache.render(TPL.checkout_summary, {
		    basket  : currency(basket),
		    shipping: currency(shipping),
		    cost    : currency(cost),
		    quantity: quantity }));
	    if (quantity > 0) $(this).removeAttr('disabled');
	    else              $(this).attr('disabled', 'disabled' );
	    $(this).children().remove();
	    $(this).append(node);
	    });
    };
    store.on('update', updateCheckout);
    store.on('delete', updateCheckout);
    store.on('clear',  updateCheckout);


    /* Cart
     */
    var cart_helper = $('.sc-cart-helper');

    // Action on cart item: add/remove/trash
    var cart = $('.sc-cart').on('click', '.sc-item', function(evt) {
	var action = $(evt.target).closest('.m-button');
	var item   = $(evt.target).closest('.sc-item');
	if (!action.length) return;
	var opts    = { src: item };
	var key     = item.attr('data-sc-photo-key');
	var vrt     = item.attr('data-sc-buy-vrt');
	
	try {
	    if (action.hasClass('a-inc')) store.add   (key, vrt, opts);
	    if (action.hasClass('a-dec')) store.remove(key, vrt, opts);
	    if (action.hasClass('a-del')) store.trash (key, vrt, opts);
	} catch(e) {
	    throw e;
	}
      });

    // Update cart content
    store.on('update', function(key, vrt, qty, opts) {
	// Find information about requested item variant
	var variant = null;
	for (var i = 0 ; i < sc_data.variant.length ; i++)
	    if ((variant = sc_data.variant[i]).key == vrt)
		break;
	if (!variant) {
	    alert('Variant '+vrt+' not found for item '+key);
	    return;
	}
    
	// Create corresponding node from template
	var name = key;
	if (parseInt(key) !== NaN) name = 'N°' + key;
	if (opts && opts.name    ) name = opts.name;
	var node = $(Mustache.render(TPL.cart_item, {
		key     : key,
		vrt     : vrt,
		info    : '(' + variant.title + ')',
		name    : name,
		quantity: qty,
		price   : currency(variant.price      ),
		total   : currency(variant.price * qty),
		thumb   : (opts && opts.img) ? opts.img : '/foo/bar.png'
	}));

	// Hide cart helper
	//  WARN: Can't use :visible as the whole cart can be hidden
	if (!cart_helper.data('sc-hidden'))
	    cart_helper.data('sc-hidden', true).hide();

	// Update cart
	var items  = cart.find('[data-sc-photo-key="'  + key + '"]');
	if (items.length) {	// Item exists: added variant or update?
	    if (! items.filter('[data-sc-buy-vrt="'    + vrt + '"]:first')
		       .replaceWith(node).length)
		items.last().after(node);
	} else {		// New item add it to the cart
	    cart.append(node);
	}

	// Make animation
	if (opts && opts.src 			&& 
	    !opts.src.hasClass('sc-item') 	&&
	    cart.is(':visible')) {
	    cart.scrollTo(node, 'slow');
	    opts.src.effect('transfer', { to       : cart,
					  className: "ui-effects-transfer" });
	}
    });

    store.on('delete' , function(key, vrt, cnt, opts) {
	// Item has been deleted
	//   if no variant (vrt), it's all of them
	var selector       = '.sc-item'
	                   + '[data-sc-photo-key="' + key + '"]';
	if (vrt) selector += '[data-sc-buy-vrt="'   + vrt + '"]';
	cart.find(selector).slideUp().promise().done(function() {
	    this.remove();
	    if (!$('.sc-item:first').length && cart_helper.data('sc-hidden'))
		cart_helper.data('sc-hidden', false).fadeIn(); });
    });

    store.on('clear', function() {
	cart.find('.sc-item').slideUp().promise().done(function() {
	    this.remove();
	    cart_helper.data('sc-hidden', false).fadeIn(); });
    });
}



//== Store =============================================================

/*
 * Store for keeping track of items and their variants
 */
function store_create() {
    return $.extend({ store : {} }, Observer.prototype, {
	remove: function(key, vrt, opt) {
	    var root = this.store[key];
	    if ((root == null) || (root.vrt[vrt] == null)) return;
	    var data = root.vrt[vrt];
	    data.qty -= 1;
	    if (data.qty < 0) data.qty = 0;
	    if (opt.img) data.img = opt.img;
	    else         opt.img  = data.img;
	    this.fire('update', key, vrt, data.qty, opt); 
	},
	add: function(key, vrt, opt) {
            var root = this.store[key];
	    if (root == null) root = this.store[key] = { vrt: {} };
	    var data  = root.vrt[vrt];
	    if (data == null) data = root.vrt[vrt] = { qty : 0 };
	    data.qty += 1;
	    if (opt.img) data.img = opt.img;
	    else         opt.img  = data.img;
	    this.fire('update', key, vrt, data.qty, opt);
	},
	trash: function(key, vrt, opt) {
            var root = this.store[key];
	    var cnt  = 0;
	    if (root == null) return;
	    if (vrt  != null) { 
		delete root.vrt[vrt]; for (e in root.vrt) cnt++; }
	    if (! cnt) delete this.store[key];
	    this.fire('delete', key, vrt, cnt, opt);
	},
	empty: function(opt) {
            this.store  = {};
	    this.fire('clear', opt);
	},
	get: function(key, vrt) {
	    var root = this.store[key];
	    return root == null ? null : root.vrt[vrt];
	},
	each: function(fn, context) {
	    for (key in this.store) {
		var root = this.store[key];
		for (vrt in root.vrt)
		    fn.apply(context, [key, vrt, root.vrt[vrt].qty]);
	    }
	},
	getCount : function() {
	    var count = 0;
	    this.each(function(key, vrt, qty) { count += 1; });
	    return count;
	},
	save  : function() {
	},
	load  : function() {
	}
    });
} 

// https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_html_Appx_websitestandard_htmlvariables

function store_enhance_order(store, sc_data) {
    store.getTotal = function() {
        var total = 0;
	this.each(function(key, vrt, qty) { 
	    var variant = null;
	    for (var i = 0 ; i < sc_data.variant.length ; i++)
		if ((variant = sc_data.variant[i]).key == vrt)
		    break;
	    total += variant.price * qty;
	});
	return total;
    };

    store.getShipping = function() {
	return sc_data.shipping;
    };

    store.order = function() {
	var seller = sc_data.seller;
	var winpar = 'width=950,height=600' + 
	             ',scrollbars,location=0,resizable,status=0';
	var params        = {
	    cmd		  : '_cart',
	    invoice	  : date_ymdhis(new Date),
	    upload	  : 1,
	    business	  : seller.id,
	    currency_code : 'EUR',
	    lc		  : 'FR',
	    handling_cart : this.getShipping() };
	if (seller.origin)
	    params.custom           = seller.origin;
	if (seller.banner)
	    params.cpp_header_image = seller.banner;
	if (seller.notify)
	    params.notify_url       = seller.notify;
	if (seller['return'])
	    params['return']        = seller['return'];
	if (seller.custom)
	    params.custom           = seller.custom;

	var counter       = 1;
	this.each(function(key, vrt, qty) {
	    if (qty <= 0) return true;

	    var variant = null;
	    for (var i = 0 ; i < sc_data.variant.length ; i++)
		if ((variant = sc_data.variant[i]).key == vrt)
		    break;
	    params['item_name_'   + counter] = 
		' No' + key + ' (' + variant.title + ')';
	  //params['item_number_' + counter] = rec.get('reference') + '/' +
	  //	rec.get('support') + '/' + rec.get('format');
	    params['quantity_'    + counter] = qty;
	    params['amount_'      + counter] = variant.price;
	    counter  += 1;
        });

	var data = [];
        for (var key in params) {
	    var val   = params[key];
	    var e_key = encodeURIComponent(key);
	    var e_val = encodeURIComponent(val);
	    if (val == null)    data.push(e_key);
	    else                data.push(e_key + '=' + e_val);
	}
	data = data.join('&');
	window.open ("https://www.paypal.com/cgi-bin/webscr?" + data,
		     "paypal", winpar);
    };
}

})(Gallery);



// Thumbnail sizer
$.available('#thumbnail-sizer', function() {
    $.available('.gallery.imgbrowse .thumbnail', function() {
	Gallery.thumbnail_sizer();
    });
});

// Highslide
$.available('.use-highslide', function() {
    Gallery.highslide_start();
});


$(function() {
    // Handling of panels (info/panel)
    Gallery.thumbnail_panels();

    // Use a shopping carte
    if ($('.sc-cart').length) {
	var store = Gallery.store_create();
	Gallery.store_enhance_order(store, sc_data);
	Gallery.cart_checkout(store);
    }

    // Turn thumbnail into link
    //  except is highslide is running
    if (!$('.use-highslide').length)
	Gallery.thumbnail_as_link();
});

