var Tug = { 
    config: {
        buy_subject: 'Enqury about photo: {uri}',
        buy_content: "Hi Andrew,\n\nI am interested in purchasing this photo:\n\n  {uri}\n"
    }
};

/* various utility functions */

Tug.nothing = function() { 
    // dummy function which does nothing - useful for do-nothing callbacks
    return false;
};

Tug.debug = (window.console && window.console.log)
    ? function() { window.console.log.apply(window.console, arguments); }
    : Tug.nothing;

Tug.debugging = function(e) {
    $('#page').toggleClass('debug');
    Tug.debug("toggling debug");
    return Tug.prevent_default(e);
};

Tug.prevent_default = function(e) {
    if (e && e.preventDefault) {
        e.preventDefault();
    }
    else {
        window.event.returnValue = false;
    }
    return false;
};

/* pseudo-class constructor function */

Tug.Class = function(schema) {
    // function to construct a new object class
    schema.base     = schema.base || Object;
    schema.defaults = schema.defaults;

    // merge any defaults defined in the base class with those specified 
    // in the schema
    var defaults = jQuery.extend({ }, schema.base.defaults, schema.defaults);

    // define a constructor function that merges the class defaults and 
    // object configuration parameters into this.config then calls init()
    var cf = function(config) {
        // Note the nasty hack: passing no args to the base class constructor 
        // below to tell it not to call the init method.  This is going to 
        // come back and bits me some time real soon now....
        if (config) {
            this.config = jQuery.extend({ }, defaults, config);
            this.init(this.config);
        }
    };
    
    // Set the base class prototype and class defaults.
    cf.prototype = new schema.base;  // nasty hack
    cf.defaults  = defaults;
    
    // bind any methods specified in the schema
    if (schema.methods) {
        for (var m in schema.methods) {
            cf.prototype[m] = schema.methods[m];
        }
    }
    
    return cf;
};


/* pseudo base-class */

Tug.Base = Tug.Class({
    methods: {
        // generic debug function - calls console.log() if firebug is running
        debug: function() {
            if (this.config.debug)
                Tug.debug.apply(Tug, arguments);
        }
    }
});

/* Lightbox class for displaying images on overlay with navigation, etc */

Tug.Lightbox = Tug.Class({
    base:       Tug.Base,
    defaults:   {
        blackout_spec:      '#blackout',
        loading_spec:       '#loading',
        lightbox_spec:      '#lightbox',
        image_spec:         '#image',
        caption_spec:       'span.caption',
        attrib_spec:        'span.attrib',
        price_spec:         'span.price',
        prev_spec:          'a.prev',
        next_spec:          'a.next',
        tab_spec:           'a.tab',
        info_spec:          'a.info',
        exif_spec:          'a.exif',
        buy_spec:           'a.buy',
        tabinfo_spec:       'span.info',
        about_spec:         'a.info span.info',
        thumbs_on_spec:     'a.thumbnails-on',
        thumbs_off_spec:    'a.thumbnails-off',
        picture_spec:       'div.picture',
        thumbctl_spec:      'div.thumbnail-controls',
        thumbs_spec:        'div.thumbnails',
        template_spec:      '#thumbnail-template',
        thumbs_class:       'thumbnails',
        thumbnail_path:     '/images/photos/70/{uri}',
        lightbox_path:      '/images/photos/900/{uri}',
        price_format:       '&pound;{price}',
        no_exif_msg:        'No technical information available.',
        image_width:        900,
        frame_height:       40,
        frame_width:        20,
        frame_margin:       25,
        frame_padding:      2,
        thumb_height:       90,
        thumb_width:        90,
        thumb_margin:       10,
        adjust_up:          30,
        debug:              false
    },
    methods: {
        init: function(config) {
            var self = this;
            self.debug('Tug.Lightbox.init(%o)', config);
            this.$blackout  = $(config.blackout_spec);
            this.$loading   = $(config.loading_spec);
            this.$lightbox  = $(config.lightbox_spec);
            this.$image     = this.$lightbox.find(config.image_spec);
            this.$caption   = this.$lightbox.find(config.caption_spec);
            this.$attrib    = this.$lightbox.find(config.attrib_spec);
            this.$price     = this.$lightbox.find(config.price_spec);
            this.$prev      = this.$lightbox.find(config.prev_spec);
            this.$next      = this.$lightbox.find(config.next_spec);
            this.$info_tab  = this.$lightbox.find(config.info_spec);
            this.$tab       = this.$lightbox.find(config.tab_spec);
            this.$about     = this.$lightbox.find(config.about_spec);
            this.$thumbs    = this.$lightbox.find(config.thumbs_spec);
            this.$thumbson  = this.$lightbox.find(config.thumbs_on_spec);
            this.$thumbsoff = this.$lightbox.find(config.thumbs_off_spec);
            this.$controls  = this.$lightbox.find(config.thumbctl_spec);
            this.$exif_tab  = this.$lightbox.find(config.exif_spec);
            this.$buy_tab   = this.$lightbox.find(config.buy_spec);
            this.$exif_info = this.$exif_tab.find(config.tabinfo_spec);
            this.$tab_infos = this.$tab.find(config.tabinfo_spec);

            self.debug("%d blackout, %d lightbox", this.$blackout.size(), this.$lightbox.size());
            self.debug("%d tab infos", this.$tab_infos.size());
            Tug.debug("%d thumbs on", this.$thumbson.size());

            this.$info_tab.click(Tug.prevent_default);
            this.$exif_tab.click(Tug.prevent_default);
            this.$buy_tab.click(
                function(e) {
                    Tug.buy_photo(self.photo);
                    return Tug.prevent_default(e);
                }
            );

            this.close = function(e) {
                self.off();
                return Tug.prevent_default(e);
            };

            this.$blackout.click(this.close);
            this.key_handlers = [ ];
            this.key_handler  = function(e) {
                var key = e == null
                ? event.keyCode         // IE
                : e.which;              // Mozilla
                if (key == 27) {        // escape
                    self.off();
                }
                else if (key == 37) {   // left arrow
                    self.prev();
                }
                else if (key == 39) {   // right arrow
                    self.next();
                }
                else {
                    self.debug("key: %d", key);
                }
            };
            var sclick = this.select_click = function(e) {
                self.select($(this).attr('rel'));
                return Tug.prevent_default(e);
            };
            self.$prev.click(
                function(e) {
                    self.prev();
                    // self.select($(this).attr('rel'));
                    return Tug.prevent_default(e);
                }
            );
            self.$next.click(
                function(e) {
                    self.next();
                    // self.select($(this).attr('rel'));
                    return Tug.prevent_default(e);
                }
            );
            self.$thumbson.click(
                function(e) {
                    self.thumbs_on();
                    return Tug.prevent_default(e);
                }
            );
            self.$thumbsoff.click(
                function(e) {
                    self.thumbs_off();
                    self.select(self.photo.uri);
                    return Tug.prevent_default(e);
                }
            );
        },
        on: function() {
            if (this.is_on)
                return;
            this.$blackout.show();
            this.key_handlers.push(document.onkeyup);
            document.onkeyup = this.key_handler;
            if (this.photos) {
                // OK
            }
            else if (this.config.photos) {
                this.photos = this.config.photos;
                this.index  = Tug.photo_index(this.photos);
            }
            else {
                this.photos = Tug.photos;
                this.index  = Tug.photo;
            }
            this.is_on = true;
        },
        off: function(e) {
            this.$blackout.hide();
            this.$lightbox.hide();
            this.photo = this.photos = undefined;
//            this.photo = undefined;
            this.has_thumbnails = false;
            this.is_on = false;
            this.$thumbs.empty();
            this.$controls.hide();
            document.onkeyup = this.key_handlers.pop();
            return Tug.prevent_default(e);
        },
        prev: function() {
            if (this.showing_thumbs) {
                this.prev_thumbnail_page();
            }
            else if (this.photo.prev) {
                this.select(this.photo.prev);
            }
        },
        next: function() {
            if (this.showing_thumbs) {
                this.next_thumbnail_page();
            }
            else if (this.photo.next) {
                this.select(this.photo.next);
            }
        },
        loading: function() {
            this.on();
            this.$loading.show();
        },
        load: function(url) {
            this.debug("Tug.Lightbox.load(%s)", url);
            this.loading();
            var self        = this;
            var preload     = new Image();
            preload.onload  = function() { self.loaded(preload); };
            preload.src     = url;
        },
        loaded: function(image) {
            this.$loading.hide();
//          this.$image.attr('src', image.src);
            this.photo = this.photo || { };
            // TODO: IE delay
            this.photo.img_src    = image.src;
            this.photo.img_height = image.height;
            this.photo.img_width  = image.width;
            this.show();
        },
        link: function(link) {
            var $link = $(link);
            var url   = $link.attr('href');
            var uri   = $link.attr('rel');
            var pic   = $link.closest(this.config.picture_spec);
//          Tug.debug("looking for photo %s in %o", uri, Tug.photos);
            this.photo = Tug.photo[uri] || {
                caption: pic.find(this.config.caption_spec).html(),
                attrib:  pic.find(this.config.attrib_spec).html()
            };
            this.load(url);
            return false;
        },
        gallery: function(params) {
            this.photos = params.photos;
            this.index  = Tug.photo_index(this.photos);
            this.attrib = params.attrib;
            if (this.photos.length > 1)
                this.$controls.show();
            return this;
        },
        thumbs_on: function(params) {
            this.on();
            this.generate_thumbnails();
            this.showing_thumbs = true;
            this.move_thumbnail_page();
            this.$attrib.hide();
            this.$tab.hide();
            this.$lightbox.addClass(this.config.thumbs_class);
        },
        thumbs_off: function(params) {
            this.on();
            this.showing_thumbs = false;
            this.$attrib.show();
            this.$tab.show();
            this.$lightbox.removeClass(this.config.thumbs_class);
        },
        select: function(uri) {
//            this.photo = this.index ? this.index[uri] : undefined;
//            this.photo = this.photo || (Tug.photo ? Tug.photo[uri] : undefined);
            this.photo = this.index[uri];
            if (! this.photo)
                throw "Invalid photo URI: " + uri;
            this.photo.url = this.photo.url || this.config.lightbox_path.expand({ uri: uri });
            this.load(this.photo.url);
            return false;
        }
    }
});


Tug.Lightbox.prototype.show = function() {
    var config      = this.config;
    var photo       = this.photo || { };
    var width       = photo.img_width;
    var height      = photo.img_height;
    var page_height = Tug.page_height();
    var page_scroll = Tug.page_scroll();
    var max_height  = page_height - (config.frame_height + config.frame_margin) * 2;
    var margin      = 0,
        box_top     = 0,
        box_left    = 0,
        scale       = 0;

    this.debug("Image size: %dx%d", width, height);
    this.debug("page scroll: %d   page height: %d   max height: %d", page_scroll, page_height, max_height);

    if (this.photos.length > 1) {
        this.$controls.show();
    }
    this.thumbs_off();


    // If the image has got a border on then it'll be wider than the expected
    // image width so we apply a negative margin to suck the border outside 
    // the container element
    if (width > config.image_width) {
        margin = parseInt(config.image_width - width, 10) / 2;
        this.debug("image width (%d) exceeds maximum width (%d), adding margin of %dpx", width, config.image_width, margin);
    }
    this.$image.css({
        margin: margin + 'px'
    });

    if (height > max_height) {
        scale  = max_height / height;
        height = max_height;
        width  = parseInt(width * scale, 10);
        this.debug(
            "image height (%d) exceeds maximum height (%d): scaling width by %f from %d to %d", 
            photo.img_height, max_height, scale, photo.img_width, width
        );
    }

    this.$image.attr({
        src:    photo.img_src,
        width:  width  + "px",
        height: height + "px"
    });

    var iwidth = width + (margin * 2) - 20;
    this.debug("width: %s  margin: %s  info width: %s", width, margin, iwidth);
    this.$tab_infos.css({
        width: iwidth + 'px'
    });

    box_top  = page_scroll + ((page_height - height) / 2) - config.frame_height;
    box_left = parseInt(width / 2, 10) + margin + config.frame_width;
    box_top  = box_top  < 0 ? 0 : box_top; 
            
    this.debug("box top: %d   box left: %d", box_top, box_left);

    // move lightbox to middle of screen and display
    this.$lightbox.css({
        top:            box_top  + "px",
        width:          width + (margin * 2) + (config.frame_padding * 2) + 'px',
        'margin-left':  -box_left + 'px'
    });

    this.$caption.html(photo.caption || '');
    this.$attrib.html(photo.attrib  || this.attrib || this.config.attrib || '');

    if (photo.prev) {
        this.$prev.attr('rel', photo.prev);
        this.$prev.show();
    }
    else {
        this.$prev.hide();
        this.$prev.attr('rel', false);
    }
    
    if (photo.next) {
        this.$next.attr('rel', photo.next);
        this.$next.show();
    }
    else {
        this.$next.hide();
    }
    
    if (photo.about) {
        this.$info_tab.show();
        this.$about.html(photo.about);
    }
    else {
        this.$info_tab.hide();
    }

    if (photo.price) {
        this.$price.html(config.price_format.expand(photo));
    }
    else {
        this.$price.empty();
    }

    var info = this.$exif_info.empty();

    info.append(
        this.exif_item('File',photo.uri)
    )

    if (photo.exif && photo.exif.length) {
        for (var i in photo.exif) {
            var d = photo.exif[i];
            info.append(
                this.exif_item(d[0], d[1])
            );
        }
    }

    this.$lightbox.fadeIn();
};

Tug.Lightbox.prototype.exif_item = function(key, value) {
    return $('<span class="exif"/>').append(
        $('<span class="key"/>').html(key + ':'),
        $('<span class="value"/>').html(value)
    );
};


Tug.Lightbox.prototype.generate_thumbnails = function() {
    var self        = this;
    var config      = this.config;
    var page_height = Tug.page_height();
    var page_scroll = Tug.page_scroll();
    var max_height  = page_height - (config.frame_height + config.frame_margin + config.thumb_margin) * 2;
    var height      = max_height,
        width       = config.image_width,
        box_top     = 0,
        box_left    = 0;

    this.$thumbs.css({
        height: height + 'px'
    });

    box_top  = page_scroll + ((page_height - height) / 2) - config.frame_height;
    box_left = parseInt(width / 2, 10) + config.frame_width;
    box_top  = box_top  < 0 ? 0 : box_top; 
            
    this.debug("thumbnail box top: %d   box left: %d", box_top, box_left);

    // move lightbox to middle of screen and display
    this.$lightbox.css({
        top:            box_top  + "px",
        width:          width + (config.frame_padding * 2) + 'px',
        'margin-left':  -box_left + 'px'
    });

    if (this.has_thumbnails)
        return;

    var thumb_cols = Math.floor(width / config.thumb_width);
    var thumb_rows = Math.floor(max_height / config.thumb_height);
    var thumb_page = this.thumbs_per_page = thumb_cols * thumb_rows;
    Tug.debug(
        "can display %d columns and %d rows of thumbnail: %d in total", 
        thumb_cols, thumb_rows, thumb_page
    );

    this.$thumbs.empty();
    var photos = this.photos;
    var pages  = [ ];

    for (var i=0; i < photos.length; i += thumb_page) {
        var page = photos.slice(i, i + thumb_page);
        pages.push(page);
        this.generate_thumbnail_page(page);
    }
    this.$thumbs.css({ width: (width * pages.length) + 'px' });
    this.thumbnail_pages = pages;
    this.thumbnail_page  = 0;
    this.has_thumbnails  = true;
};

Tug.Lightbox.prototype.generate_thumbnail_page = function(photos) {
    var page  = $('<div class="page"></div>');
    var path  = this.config.thumbnail_path;
    var tmpl  = this.$lightbox.find(this.config.template_spec).html();
    var link;

    for (var p in photos) {
        photo           = photos[p];
        photo.thumbnail = path.expand(photo);
        link            = $(tmpl.expand(photo));
        link.click(this.select_click);
        page.append(link);
    }

    this.$thumbs.append(page);
};

Tug.Lightbox.prototype.prev_thumbnail_page = function() {
    if (! this.showing_thumbs || this.thumbnail_page == 0)
        return;
    this.thumbnail_page--;
    this.move_thumbnail_page();
};

Tug.Lightbox.prototype.next_thumbnail_page = function() {
    if (! this.showing_thumbs || this.thumbnail_page >= this.thumbnail_pages.length - 1)
        return;
    this.thumbnail_page++;
    this.move_thumbnail_page();
};

Tug.Lightbox.prototype.move_thumbnail_page = function() {
    var page   = this.thumbnail_page;
    var pages  = this.thumbnail_pages;
    var photos = pages[page];
    var tpp    = this.thumbs_per_page;
    var lastp  = pages.length - 1;
    var start  = page * tpp;
    var offset = - (page * this.config.image_width); 

    if (page) {
        this.$prev.show();
    }
    else {
        this.$prev.hide();
    }
    if (page < lastp) {
        this.$next.show();
    }
    else {
        this.$next.hide();
    }
//  Tug.debug("moving to offset: %s", offset);

    this.$thumbs.css({ 
        left: offset + 'px'
    });
    
    this.$caption.html(
        'Page '                 + (page + 1)    +
        ' of '                  + pages.length  +
        ' showing pictures '    + (start + 1)   +
        ' to '                  + (start + photos.length)
    );
};


/* Tug.init bootstraps an init function which is run when page is loaded.
 * It creates a Tug.Lightbox instance and stores it in Tug.lightbox 
 */
Tug.init = function(params) {
    $(document).ready(
        function() {
            var config   = jQuery.extend({ }, Tug.config, params);
            Tug.config   = config;
            Tug.debug("Tug.init(%o)", config);
            Tug.photos   = config.photos || [ ];
            Tug.photo    = Tug.photo_index(Tug.photos);
            Tug.lightbox = new Tug.Lightbox(config);
        }
    );
};

Tug.page_scroll = function() {
    // based on code from Quirksmode.org via Lightbox JS
    if (self.pageYOffset) {
        // Standards compliant browsers
        return self.pageYOffset;
    } else if (document.documentElement && document.documentElement.scrollTop) {
        // Explorer 6 Strict
        return document.documentElement.scrollTop;
    } else if (document.body) {
        // all other Explorers
        return document.body.scrollTop;
    }
};

Tug.page_height = function() {
    // based on code from Quirksmode.org via Lightbox JS
    // Edit for Firefox by pHaez
    var xScroll, yScroll;
    var windowWidth, windowHeight;
    var pageWidth, pageHeight;
    
    if (self.innerHeight) { 
        // all except Explorer
        return self.innerHeight;
    } else if (document.documentElement && document.documentElement.clientHeight) { 
        // Explorer 6 Strict Mode
        return document.documentElement.clientHeight;
    } else if (document.body) {
        // other Explorers
        return document.body.clientHeight;
    }
};

Tug.pause = function(numberMillis) {
    // Pauses code execution for specified time. Uses busy code, not good.
    // Code from http://www.faqts.com/knowledge_base/view.phtml/aid/1602
    var now = new Date();
    var exitTime = now.getTime() + numberMillis;
    while (true) {
        now = new Date();
        if (now.getTime() > exitTime)
            return;
    }
};

Tug.config.gallery = {
    image_base:     '/images/photos/',
    frame_spec:     'div.feature div.frame',
    image_spec:     'div.image',
    thumbnail_spec: 'div.thumbnails a.thumbnail',
    caption_spec:   '.caption',
    attrib_spec:    '.attrib',
    lightbox_spec:  'a.lightbox',
    lightbox_size:  900,
    blank:          '&nbsp;',
    featured_class: 'featured',
    photos:         { }
};

Tug.gallery = function(spec, params) {
    var config    = jQuery.extend({ }, Tug.config.gallery, params);
    var container = $(spec);
    var frame     = container.find(config.frame_spec);
    var image     = frame.find(config.image_spec);
    var caption   = frame.find(config.caption_spec);
    var attrib    = frame.find(config.attrib_spec);
    var light     = image.find(config.lightbox_spec);
    var thumbs    = container.find(config.thumbnail_spec);
    var photos    = config.photos;
    var index     = Tug.photo_index(photos);

    var thumbclick = function(e) {
        var link  = $(this);
        var href  = link.attr('href');
        var rel   = link.attr('rel');
        var large = config.image_base + config.lightbox_size + '/' + rel;
        var photo = index[rel] || { };
        var css   = {
            'background-image': 'url(' + href + ')'
        };

        var max_width = image.width();
        var width     = parseInt(photo.width, 10);
        var height    = parseInt(photo.height, 10);
        var margin    = parseInt((max_width - width) / 2, 10);
//      Tug.debug("max width: %s   photo width: %s  photo height: %s   margin: %s", max_width, width, height, margin);

        if (margin) {
            // note: margin is negative
            height += (margin * 2);
            css['background-position'] = margin + 'px ' + margin + 'px';
//          Tug.debug("set background-position to %s", css['background-position']);
//          Tug.debug("new height: %s", height);
        }
        else {
            css['background-position'] = 'center center';
        }

        if (photo['height']) {
            css['height'] = height;
        }

        /* update image CSS to set new background and height */
        image.css(css);

        /* set caption and attribution */
        caption.html(photo['caption'] || config.blank);
        attrib.html(photo['attrib'] || config.attrib || config.blank);

        /* set link to lightbox for larger version of image */
        light.attr('href', large);
        light.attr('rel', rel);
        light.attr('title', photo['caption']);

        /* highlight link to indicate picture is featured */
        link.solo(config.featured_class);

        return Tug.prevent_default(e);
    };

    light.attr('onclick', '');
    light.click( 
        function(e) {
            Tug.lightbox.gallery(config).select(light.attr('rel'));
            return Tug.prevent_default(e);
        } 
    );
    thumbs.first().addClass(config.featured_class);
    thumbs.click(thumbclick);
};


Tug.photo_index = function(photos) {
    var p, photo;
    var index = { };
    var prev  = false;

    for (p in photos) {
        photo = photos[p];
        index[photo.uri] = photo;

        if (prev) {
            prev.next  = photo.uri;
            photo.prev = prev.uri;
        }
        prev = photo;
    }

    return index;
};

Tug.buy_photo = function(photo) {
    var uri = photo.uri;
    return Tug.mailto(
        Tug.config.buy_subject.expand(photo),
        Tug.config.buy_content.expand(photo)
    );
};

Tug.mailto = function(subject, body) {
    var url = [
        'mailto:',
        'andrew',
        '@tug.c',
        'om?sub',
        'ject=',
        encodeURIComponent(subject),
        '&body=',
        encodeURIComponent(body)
    ];
    url = url.join('');
    window.location = url;
    return false;
};


String.prototype.expand = function (obj) {
    return this.replace(/(?:{|%7B)([^{}%\.]*)(?:\.([^{}%]*))?(?:}|%7D)/gi,
        function (all, name, filter) {
            var res = name.length ? obj[name] : undefined;
            var val = typeof res === 'string' 
                   || typeof res === 'number' 
                    ? res : all;

            if (filter) {
                var f = Tug.template_filters[filter];
                if (! f) throw "Invalid template filter: " + filter;
                val = f(obj, name, val, filter);
            }
            return val;
        }
    );
};



jQuery.fn.extend({
    // add a class to an element and remove it from all siblings
    solo: function(c) {
        this.addClass(c).siblings('.' + c).removeClass(c);
        return this;
    },
    random: function() {
        return $(
            this.get(
                Math.floor(
                    Math.random() * this.size()
                )
            )
        );
    }
});

