// To do
// * Reasses image sizes
// * Fix "Expand"-is-text problem
//
// Icing
// * Change how previews are loaded
// ** Have two preview images.  Display throbber over old one while
//    new one is loading and swap visibility once new one is loaded.
// ** Be careful about timing issues.  Perhaps use visibility attributes.
// ** Or maybe not, since the trobber isn't transparent
// * Deal better with the focus (see element.focus)
// * Smooth expand thumbnail UI
// * Tag limiting
// ** Have a set of tag limits, displayed above the album
// ** When the user clicks a tag, add it and all of its subtags to the
//    limit list
// *** Maybe show the subtags (just the leaves) in parens?
// ** Have an X button next to each tag in the limit list
// ** Can start out with "Starred" on the limit list

var imgWidth = 100;
var imgHeight = 75;
var cellWidth = imgWidth+2*2;
var cellHeight = imgHeight+2*2;
var minCols = 2;

try {
    console.log();
    info = function (msg) {console.log(msg);};
} catch (e) {
    info = function (msg) {}
}

function fromID(id) {
    return document.getElementById(id);
}

function createAlbum() {
    // In IE, we can't actually create the album until the document is
    // loaded because dimensions are all 0 until then
    var oldOnload = window.onload;
    window.onload = function() {
        var ret = _createAlbum();
        if (oldOnload)
            ret = oldOnload();
        return ret;
    }
}

function _createAlbum() {
    var thumbContainer = fromID("thumbContainer");

    var thumbnailCanvas = new ThumbnailCanvas(thumbContainer);
    var thumbnailUI =
        new ThumbnailUI(images, thumbnailCanvas,
                        thumbContainer,
                        fromID("thumbShelf"),
                        fromID("thumbShelfBottom"),
                        fromID("expandText"));
    var tagUI =
        new TagUI(fromID("tagsTable"),
                  fullscreenSrc);
    var previewUI =
        new PreviewUI(images,
                      fromID("previewContainer"),
                      fromID("preview"),
                      fromID("throbber"),
                      blankSrc, throbberSrc);
    thisAlbum = new AlbumUI(images, thumbnailUI, tagUI, previewUI);
}

//
// Album UI
//

function AlbumUI(images, thumbnailUI, tagUI, previewUI) {
    var self = this;
    this.images = images;
    this.thumbnailUI = thumbnailUI;
    this.thumbnailUI.onselect = function (image) { self.selectImage(image); };
    this.tagUI = tagUI;
    this.previewUI = previewUI;

    this.handleResize();

    // Unfortunately, the document hash can change behind our backs
    // (for example, if the user loads a bookmark), so we need to keep
    // an eye on it.
    this.curhash = document.location.hash;
    window.setInterval(function () { self._checkHash() }, 500);

    if (document.location.hash.length == 0 ||
        !this._selectHash(document.location.hash.slice(1), true))
        this.thumbnailUI.setExpanded(true);

    // Don't register this any earlier or IE is liable to freeze
    window.onresize = function () { self.handleResize(); return true; };
}

AlbumUI.prototype._checkHash = function() {
    if (document.location.hash == this.curhash)
        return;
    info("Document hash changed");
    this.curhash = document.location.hash;
    this._selectHash(document.location.hash.slice(1), false);
}

AlbumUI.prototype.handleResize = function() {
    this.previewUI.handleResize();
    this.thumbnailUI.handleResize();
}

AlbumUI.prototype.scroll = function(dir) {
    this.thumbnailUI.scroll(dir);
}

AlbumUI.prototype.toggleExpanded = function() {
    this.thumbnailUI.toggleExpanded();
}

AlbumUI.prototype.selectImage = function(image) {
    var oldImage = this.previewUI.showImage(image);
    if (oldImage != image) {
        // Select the right image
        this.thumbnailUI.highlight(image);

        // Show the tags
        this.tagUI.showTags(image);

        // Contract the album
        this.thumbnailUI.setExpanded(false);
    }
}

AlbumUI.prototype._selectHash = function(hash, loading) {
    for (var i in this.images) {
        var image = this.images[i];
        if (image.base == hash) {
            this.selectImage(image);
            // It looks nifty to pass loading for immediate, but can
            // potentially hit the thumbnails really, really hard.
            this.thumbnailUI.ensureVisible(image, true);
            return true;
        }
    }
    return false;
}

//
// Thumbnail UI
//

function ThumbnailUI(images, canvas,
                     thumbContainer,
                     thumbShelf, thumbShelfBottom, expandText) {
    this.images = images;
    this.thumbnailCanvas = canvas;
    this.thumbContainer = thumbContainer;
    this.thumbShelf = thumbShelf;
    this.thumbShelfBottom = thumbShelfBottom;
    this.expandText = expandText;

    this.maxCols = null;
    this.nCols = null;
    this.nRows = 3;
    this.expanded = null;

    this.prescrollPos = null;
    this.scrollPos = null;

    this.curHightlight = null;

    this.scrollAnimation = new ScrollAnimation(this);

    this.onselect = function (image) {};
    var self = this;
    canvas.onselect = function (image) { self.onselect(image); }

    // Preload all thumbnails
    this.preloadStart = null;
    this._preloadNext();
}

ThumbnailUI.prototype._preloadNext = function() {
    var index;

    if (this.scrollPos == null)
        index = 0;
    else
        index = Math.min(Math.floor(this.scrollPos*this.nRows),
                         this.images.length-1);

    var self = this;
    var cb2 = function() { self._preloadNext(); return true; };
    var cb = function() {
        // Be nice to bandwidth
        window.setTimeout(cb2, new Date().valueOf() - self.preloadStart);
        return true;
    };

    this.preloadStart = new Date().valueOf();

    // Right of current position
    for (var i = index; i < this.images.length; ++i) {
        var image = this.images[i];
        if (this.thumbnailCanvas.loadThumbnail(image, cb)) {
            info("Preloading right", index, i);
            return;
        }
    }

    // Left of current position
    for (var i = index-1; i >= 0; --i) {
        var image = this.images[i];
        if (this.thumbnailCanvas.loadThumbnail(image, cb)) {
            info("Preloading left", index, i);
            return;
        }
    }

    info("All thumbnails preloaded");
}

ThumbnailUI.prototype.getTotalCols = function() {
    return Math.ceil(this.images.length / this.nRows);
}

ThumbnailUI.prototype.handleResize = function() {
    var left = getOffset(this.thumbContainer)[1];
    var right = left + this.thumbContainer.clientWidth;
    var maxCols = Math.floor(right/cellWidth) - 1;
    this.maxCols = Math.min(maxCols, this.getTotalCols());
}

ThumbnailUI.prototype.scroll = function(dir) {
    var by = Math.max(Math.floor(this.nCols / 2), minCols);
    var max = Math.max(0, this.getTotalCols() - this.nCols);
    if (dir == "left") {
        this.scrollAnimation.scrollBy(-by, 0, max);
    } else if (dir == "right") {
        this.scrollAnimation.scrollBy(by, 0, max);
    }
}

ThumbnailUI.prototype.getScrollPos = function() {
    return this.scrollPos;
}

ThumbnailUI.prototype.prescrollTo = function(pos) {
    this.prescrollPos = pos;
    // Wipe the current scroll pos to force an update
    scrollPos = this.scrollPos;
    this.scrollPos = null;
    this.scrollTo(scrollPos);
}

ThumbnailUI.prototype.scrollTo = function(pos) {
    if (pos == this.scrollPos)
        return;

    var toFree = [];
    var toPlace = [];

    var left = pos-1, right = pos+this.nCols;

    if (this.prescrollPos != null) {
        left = Math.min(left, this.prescrollPos-1);
        right = Math.max(right, this.prescrollPos+this.nCols);
    }

    // Figure out what to do with each image
    for (var i in this.images) {
        var image = this.images[i];
        var cx = Math.floor(i / this.nRows);

        if (cx <= left || cx >= right) {
            if (image.thumb != null)
                toFree[toFree.length] = image;
            continue;
        } else {
            var cy = i % this.nRows;
            toPlace[toPlace.length] = [cx, cy, image];
        }

    }

    // Free thumbnails first for efficiency
    for (var i in toFree) {
        var image = toFree[i];
        if (image == this.curHightlight)
            this._unhighlight(image.thumb);
        this.thumbnailCanvas.hideThumbnail(image);
    }

    // Place thumbnails
    for (var i in toPlace) {
        var cx = toPlace[i][0];
        var cy = toPlace[i][1];
        var image = toPlace[i][2];

        this.thumbnailCanvas.placeThumbnail((cx-pos) * cellWidth,
                                            cy * cellHeight,
                                            image);
        if (image == this.curHightlight)
            this._highlight(image.thumb);
    }

    this.scrollPos = pos;
}

ThumbnailUI.prototype.toggleExpanded = function() {
    if (this.expanded) {
        this.setExpanded(false);
    } else {
        this.setExpanded(true);
    }
}

ThumbnailUI.prototype.setExpanded = function(expand) {
    if (expand == this.expanded)
        return;

    var oldCols = this.nCols;
    if (expand) {
        this.nCols = this.maxCols;
    } else {
        this.nCols = minCols;
    }
    var shelfWidth = (this.nCols*cellWidth);
    var expandBy = shelfWidth-(minCols*cellWidth);

    this.thumbShelf.style.left = (-expandBy) + "px";
    this.thumbContainer.style.width = shelfWidth + "px";
    this.thumbShelfBottom.style.left = (-expandBy) + "px";
    this.thumbShelfBottom.style.width = (shelfWidth+4) + "px";

    if (expand) {
        this.expandText.innerHTML = "Shrink <b>&raquo;</b>";
    } else {
        this.expandText.innerHTML = "<b>&laquo;</b> Expand";
    }

    this.expanded = expand;

    // Immediate scroll to account for window change
    if (oldCols == null)
        this.scrollTo(0);
    else
        this.scrollTo(this.scrollPos - (this.nCols - oldCols));

    // XXX Perhaps shrinking should be immediate to get it out of
    // their way (at least if its shrinking because of a selection),
    // but expansion should be smooth.  Then only clicking
    // expand/contract would be smooth.
    //
    // XXX If the box is going to expand immediately, maybe expansion
    // should be immediate?

    this.ensureVisible(this.curHightlight, false);
}

ThumbnailUI.prototype.ensureVisible = function(image, immediate) {
    var target = this.scrollPos;

    // Ensure that the scroll position isn't negative
    if (target < 0)
        target = 0;

    // Ensure that the image is visible
    if (image != null) {
        var col = null;
        for (var i in this.images) {
            var otherImage = this.images[i];
            if (image == otherImage) {
                col = Math.floor(i / this.nRows);
                break;
            }
        }

        if (col < target)
            target = col;
        if (col >= target + this.nCols)
            target = col - this.nCols + 1;
    }

    // Ensure the right side isn't off the left
    var maxPos = this.getTotalCols() - this.nCols;
    if (target > maxPos)
        target = maxPos;

    if (immediate)
        this.scrollTo(target);
    else
        this.scrollAnimation.scrollTo(target);
}

ThumbnailUI.prototype.highlight = function(image) {
    if (this.curHightlight != null && this.curHightlight.thumb != null) {
        this._unhighlight(this.curHightlight.thumb);
    }

    if (image != null && image.thumb != null) {
        this._highlight(image.thumb);
    }

    this.curHightlight = image;
}

ThumbnailUI.prototype._highlight = function(thumb) {
    thumb.style.borderColor = "#0000ff";
}

ThumbnailUI.prototype._unhighlight = function(thumb) {
    thumb.style.borderColor = "";
}

//
// Tag UI
//

function TagUI(tagsTable, fullscreenSrc) {
    this.tagsTable = tagsTable;
    this.tableBody = tagsTable.getElementsByTagName("tbody")[0]
    this.exampleRow = this.tableBody.getElementsByTagName("tr")[0];
    this.fullscreenSrc = fullscreenSrc;
    this.clear();
}

TagUI.prototype.clear = function() {
    var rows = this.tableBody.getElementsByTagName("tr");
    for (var i = rows.length-1; i >= 0; --i) {
        this.tableBody.removeChild(rows[i]);
    }
}

TagUI.prototype.showTags = function(image) {
    this.clear();

    // Full size view
    var fullUrl = getImageSource(image, image.dims[0])
    this._addRow(this.fullscreenSrc,
                 [document.createTextNode("View/save full size"),
                  document.createElement("br"),
                  document.createTextNode(image.dims[0][0] + "x" +
                                          image.dims[0][1])],
                 fullUrl);

    // Time
    if (image.time.length > 0) {
        var contents = [];
        for (var s in image.time) {
            if (s > 0) {
                if (s > 1)
                    contents[contents.length] = document.createElement("br");
                contents[contents.length] =
                    document.createTextNode(image.time[s]);
            }
        }
        this._addRow(image.time[0], contents);
    }

    // Tags
    for (var ts in image.tags) {
        var tags = image.tags[ts];
        var imgSrc = tags[0];
        var contents = [];
        for (var t in tags) {
            if (t > 0) {
                var str = tags[t];
                if (t > 1)
                    str = ", " + str
                var txt = document.createTextNode(str);
                contents[contents.length] = txt;
            }
        }
        this._addRow(imgSrc, contents);
    }

    // Comment
    if (image.text.length > 0) {
        this._addRow(image.text[0], [document.createTextNode(image.text[1])]);
    }
}

TagUI.prototype._addRow = function(imgSrc, contents, href) {
    var row = this.exampleRow.cloneNode(true);
    var cells = row.getElementsByTagName("td");

    var img = document.createElement("img");
    img.src = imgSrc;
    var left = img;
    if (href) {
        var a = document.createElement("a");
        a.appendChild(img);
        a.href = href;
        left = a;
    }
    cells[0].appendChild(left);

    var right = null;
    if (href) {
        right = document.createElement("a");
        right.href = href;
        cells[1].appendChild(right);
    } else {
        right = cells[1];
    }
    for (var i in contents) {
        right.appendChild(contents[i]);
    }

    this.tableBody.appendChild(row);
}

//
// Preview UI
//

function PreviewUI(images,
                   previewContainer,
                   previewImage,
                   throbber,
                   blankSrc, throbberSrc) {
    this.images = images;
    this.previewContainer = previewContainer;
    this.previewImage = previewImage;
    this.throbber = throbber;
    this.blankSrc = blankSrc;
    throbber.src = throbberSrc;

    this.width = this.height = null;

    this.curImage = null;
}

PreviewUI.prototype.handleResize = function() {
    //
    // Size the preview container
    //

    // Get the width bound
    this.width = this.previewContainer.clientWidth;

    // Compute the height bound based on the window height and the
    // minimum offset of the preview image
    var top = getOffset(this.previewImage.parentNode)[0];
    var minHeight = this.previewContainer.clientHeight;

    this.height = Math.max(document.body.clientHeight - top - 10, minHeight);

    //
    // Position the throbber
    //

    this.throbber.style.left = (this.width-this.throbber.width)/2 + "px";
    this.throbber.style.top = (this.height-this.throbber.height)/2 + "px";

    //
    // Position the preview
    //

    this.showImage(this.curImage);
}

PreviewUI.prototype.showImage = function(image) {
    var oldImage = this.curImage;
    this.curImage = image;
    var previewImage = this.previewImage;

    if (image == null) {
        previewImage.style.visibility = "hidden";
        return oldImage;
    }

    // Figure out which image to load
    var bestFit = getImageBestFit(image, this.width, this.height);
    var newSrc = getImageSource(image, bestFit.best);

    // Load it, if it's changed
    if (newSrc != previewImage.src) {
        // Show the throbber while we're loading
        var throbber = this.throbber;
        previewImage.style.visibility = "hidden";
        throbber.style.visibility = "visible";

        previewImage.onload = function () {
            previewImage.onload = function() {
                throbber.style.visibility = "hidden";
            }
            previewImage.style.visibility = "visible";
            previewImage.src = newSrc;
        }
        previewImage.src = this.blankSrc;
    }

    // Position and scale it
    var width = bestFit.scaled[0];
    var height = bestFit.scaled[1];
    previewImage.style.width = width + "px";
    previewImage.style.height = height + "px";
    previewImage.style.left = (this.width-width)/2 + "px";
    previewImage.style.top = (this.height-height)/2 + "px";

    return oldImage;
}

//
// Thumbnail canvas
//

function ThumbnailCanvas(parent) {
    this.parent = parent;

    this.onselect = function (image) {};
}

// If a callback is specified, returns true if that callback will be
// called at some later time, or false if the image is already loaded
// and the callback will not be called.  If no callback is specified,
// returns the thumbnail.
ThumbnailCanvas.prototype.loadThumbnail = function(image, callback) {
    if (image.thumb) {
        if (callback) {
            var img = image.thumb.getElementsByTagName("img")[0]
            if (img.complete) {
                return false;
            } else if (!img.onload) {
                img.onload = callback;
                return true;
            } else {
                info("ERROR: Thumbnail already has an onload callback");
                return false;
            }
        }
    } else {
        var dim = image.dims[image.dims.length-1];

        info("Loading thumbnail");
        a = document.createElement("a");
        a.style.visibility = "hidden";
        a.href = "#" + image.base;
        var self = this;
        a.onclick = function () { self.onselect(image); return true; }

        var img = document.createElement("img");
        if (callback)
            img.onload = callback;
        img.src = getImageSource(image, dim);
        a.appendChild(img);

        this.parent.appendChild(a);

        image.thumb = a;
    }

    if (callback) {
        return true;
    } else {
        return image.thumb;
    }
}

ThumbnailCanvas.prototype.placeThumbnail = function(x, y, image) {
    var a = this.loadThumbnail(image, null);
    var dim = image.dims[image.dims.length-1];

    a.style.left = (x + (imgWidth - dim[0])/2) + "px";
    a.style.top = (y + (imgHeight - dim[1])/2) + "px";

    if (a.style.visibility == "hidden") {
        info("Showing thumbnail");
        a.style.visibility = "visible";
    }
}

ThumbnailCanvas.prototype.hideThumbnail = function(image) {
    var thumb = image.thumb;
    if (thumb.style.visibility != "hidden") {
        info("Hiding thumbnail");
        thumb.style.visibility = "hidden";
    }
}

//
// Scroll animation
//

function ScrollAnimation(thumbui) {
    this.thumbui = thumbui;

    this.interval = null;
    this.targetPos = null;
    this.lastTick = null;
    this.velocity = 0;

    this.startTime = null;
    this.nTicks = 0;

    this.damping = 0.2;         // sec
    this.max = 6.1;             // cell/sec
    this.maxAcc = 50;           // cell/sec^2
}

ScrollAnimation.prototype.setupInterval = function() {
    var active = (this.targetPos != null);
    if (this.interval == null && active) {
        var self = this;
        this.lastTick = new Date().valueOf();
        this.startTime = this.lastTick;
        this.nTicks = 0;
        this.interval = window.setInterval(function() {self.tick();}, 20);
        info("Going");
    } else if (this.interval != null && !active) {
        window.clearInterval(this.interval);
        this.interval = null

        var secs = (new Date().valueOf() - this.startTime)/1000
        var fps = this.nTicks/secs;
        info("Gone", fps, secs);
    }
}

ScrollAnimation.prototype.scrollBy = function(by, min, max) {
    var pos = null;
    if (this.targetPos == null)
        pos = this.thumbui.getScrollPos() + by;
    else
        pos = this.targetPos + by;
    this.scrollTo(Math.min(Math.max(pos,min),max));
}

ScrollAnimation.prototype.scrollTo = function(pos) {
    info("New scroll request");
    // Prescroll
    this.thumbui.prescrollTo(pos);
    // Animate
    this.targetPos = pos;
    this.setupInterval();
}

ScrollAnimation.prototype.tick = function() {
    var now = new Date().valueOf();
    if (this.lastTick == null)
        this.lastTick = now;
    var delta = (now - this.lastTick)/1000;
    this.lastTick = now;
    ++this.nTicks;

    var scrollPos = this.thumbui.getScrollPos();
    var vdelta = (this.targetPos - scrollPos)/this.damping - this.velocity;
    this.velocity += bicap(vdelta, this.maxAcc*delta);
    this.velocity = bicap(this.velocity, this.max);

    //info(this.velocity, delta, this.velocity*delta);

    if (Math.abs(this.velocity*delta) >= Math.abs(this.targetPos-scrollPos) ||
        Math.abs(this.targetPos-scrollPos) * cellWidth < 1) {
        this.thumbui.scrollTo(this.targetPos);
        this.targetPos = null;
        this.velocity = 0;
        this.thumbui.prescrollTo(null);
        this.setupInterval();
    } else {
        this.thumbui.scrollTo(scrollPos + this.velocity*delta);
    }
}

//
// Utilities
//

function bicap(val, cap) {
    cap = Math.abs(cap);
    return Math.max(Math.min(val, cap), -cap);
}

function getImageBestFit(image, width, height) {
    var bestDim = image.dims[0];

    for (var i in image.dims) {
        var dim = image.dims[i];
        if (bestDim[0] > width || bestDim[1] > height) {
            // Our best doesn't fit, so try to get a smaller one
            if (bestDim[0]*bestDim[1] > dim[0]*dim[1]) {
                bestDim = dim;
            }
        } else {
            // Our best does fit, so try to get a larger one that
            // still fits
            if (dim[0] <= width && dim[1] <= height &&
                dim[0]*dim[1] > bestDim[0]*bestDim[1]) {
                bestDim = dim;
            }
        }
    }

    // This is all nonsense
    // for (var i in image.dims) {
    //     var dim = image.dims[i];
    //     var w = dim[0], h = dim[1];
    //     // Require the image to fill at least 25% of the total area
    //     if (100 * (w * h) / (width * height) < 25) {
    //         continue;
    //     }

    //     // XXX Something's wrong here.  Unless none of them pass the
    //     // above test, bestDim will always fit.

    //     if (// If best dimension doesn't fit
    //         bestDim[0] > width && bestDim[1] > height &&
    //         // ... but this one does
    //         w <= width && h <= height)
    //     {
    //         // Prefer images that fit over images that have to be scaled
    //         bestDim = dim;
    //     } else if (
    //         // If best dimension fits
    //         bestDim[0] <= width && bestDim[1] <= height &&
    //         // ... and this fits
    //         w <= width && h <= height &&
    //         // ... but this fills the area better
    //         w*h > bestDim[0]*bestDim[1])
    //     {
    //         // Prefer images that better fill the area
    //         bestDim = dim;
    //     }
    // }

    // Scale the dimensions to fit
    var scaledDim = null;
    if (bestDim[0] <= width && bestDim[1] <= height) {
        scaledDim = bestDim;
    } else {
        var scaled1 = [width, bestDim[1]*width/bestDim[0]];
        var scaled2 = [bestDim[0]*height/bestDim[1], height];
        if (scaled1[0] < scaled2[0])
            scaledDim = scaled1;
        else
            scaledDim = scaled2;
    }

    return { best: bestDim, scaled: scaledDim }
}

function getImageSource(image, dim) {
    return "albums/" + image.base + "-" + dim[0] + "x" + dim[1] + ".jpg";
}

function getOffset(elt) {
    var top = 0, left = 0;
    while (elt) {
        top += elt.offsetTop;
        left += elt.offsetLeft;
        elt = elt.offsetParent;
    }
    return [top, left]
}
