// must be overwritten by user //
//XXX create init methods, or a single façade method
var minX = 0;
var minY = 0;
var maxX = 0;
var maxY = 0;
var containerPositionX = 0;
var containerPositionY = 0;
/////////////////////////////////

var borderAndPaddingSize = 0;
var container = undefined;
var draggingBackground = false;
var dragging = false;
var dragged = false;
var draggingDiv = false;
var draggingDiv_scrollLeft = 0;
var draggingDiv_scrollTop = 0;
var relativePoint = new Point(0, 0);
var originalPosition = new Point(0, 0);
var STICKY_THRESHOLD = 20;
var LAYOUT_PADDING = 30;
var divs = new Array();

var callback_selected = function(element) {};

function setSelectedCallback(funct) {
    callback_selected = funct;
}

function getPoint(event) {
    var posx = 0;
    var posy = 0;
    if (event.pageX || event.pageY) {
        posx = event.pageX;
        posy = event.pageY;
    }
    else if (event.clientX || event.clientY) {
        posx = event.clientX;
        posy = event.clientY;
    }
    posx += getXScroll();
    posy += getYScroll();
    posx -= containerPositionX;
    posy -= containerPositionY;
    return new Point(posx, posy);
}

function startdrag(event) {
    if(dragging) {
        //something's wrong (probably the div got glued to the cursor, and the user clicked again somewhere)
        stopdrag();
        return;
    }

    var point = getPoint(event);

    //check container bounds
    if((point.x < minX) || (point.x > maxX) || (point.y < minY) || (point.y > maxY))
        return;

    for (var i = divs.length - 1; i >= 0; i--) {
        originalPosition = getPosition(divs[i]);
        relativePoint = new Point(point.x - originalPosition.x, point.y - originalPosition.y);
        if (contains(divs[i], relativePoint)) {
            selectDraggingDiv(divs[i]);
            return;
        }
    }
    if(hasClass(container, 'scrollBackground')) {
        draggingBackground = true;
    // don't make me create new variables, these will be just fine
        originalPosition = point;
        dragged = false;
        draggingDiv_scrollLeft = container.scrollLeft;
        draggingDiv_scrollTop = container.scrollTop;
    }
}

function selectDraggingDiv(div) {
    draggingDiv = div;
    if(draggingDiv.ismoving)
        return;
    dragging = true;
    dragged = false;
    moveToTop(draggingDiv);
    draggingDiv_scrollLeft = draggingDiv.scrollLeft;
    draggingDiv_scrollTop = draggingDiv.scrollTop;
}

function remove(array, element) {
    var other = new Array();
    while(array.length != 0)
        other.push(array.pop());
    while(other.length != 0) {
        var tmp = other.pop();
        if (tmp != element)
            array.push(tmp);
    }
}

function moveToTop(div) {
    if(divs[divs.length - 1] != div) {
        var scrollTop = div.scrollTop;
        var scrollLeft = div.scrollLeft;
        
        remove(divs, div);
        container.removeChild(div);
        
        divs.push(div);
        container.appendChild(div);
        
        div.scrollTop = scrollTop;
        div.scrollLeft = scrollLeft;
    }
    callback_selected(div);
}

function getDivs() {
    return divs;
}

function setDivs(divArray) {
    divs = new Array();
    for(var i in divArray)
        if (divArray[i].id != undefined)
            if (hasClass(divArray[i], "draggable"))
                divs.push(divArray[i]);
    render();
}

function setContainer(element, scrollBackground) {
    if(scrollBackground == undefined)
        scrollBackground = false;

    container = element;

    if(scrollBackground) {
        element.className += ' scrollBackground';
    }
}

function setBorderAndPaddingSize(size) {
    borderAndPaddingSize = size;
}

function render() {
    if(container == undefined)
        return;
    var dummy = document.createElement('div');
    while(container.hasChildNodes())
    //otherwise if i simply delete container's content, internet explorer deletes divs content children nodes.
        dummy.appendChild(container.lastChild);
    for(var i in divs)
        container.appendChild(divs[i]);
    dummy.innerHTML = '';
}

function stopdrag() {
    if(!dragging && !draggingBackground)
        return;
    dragging = false;
    draggingBackground = false;
    // TODO: save positions on server?
}

function drag(event) {
    if(dragging) {
        if((draggingDiv.scrollLeft != draggingDiv_scrollLeft) || (draggingDiv.scrollTop != draggingDiv_scrollTop)) {
        //not dragging, only scrolling
            stopdrag();
            return;
        }
        var point = getPoint(event);
        dragged = true;
        var newpos = new Point(
            Math.max(minX, Math.min(maxX - getWidth (draggingDiv), (point.x - relativePoint.x))),
            Math.max(minY, Math.min(maxY - getHeight(draggingDiv), (point.y - relativePoint.y))));
        var newpos = clipLocation(new Point(
                point.x - relativePoint.x,
                point.y - relativePoint.y),
            draggingDiv);
        if(squaredDistance(originalPosition, newpos) < (STICKY_THRESHOLD * STICKY_THRESHOLD))
            return;
        setPosition(draggingDiv, newpos);
        originalPosition = new Point(100000, 100000); //XXX unlocks movement - create a boolean var, or use dragged, moving this check
        clearSelection();
    } else if(draggingBackground) {
        var point = getPoint(event);
        if(!dragged && // check only the first time, if it was already moving
            ((container.scrollLeft != draggingDiv_scrollLeft) || (container.scrollTop != draggingDiv_scrollTop))) {
        //not dragging, only scrolling
            stopdrag();
            return;
        }

        // force sticky threshold,
        //because firefox fires the mousemoved event before updating the scrollbar positions,
        //so there is no way in knowing, at the first chance, if it is a scoll or a drag.
        //btw, forcing to a really small value (5 pixels)
        if(squaredDistance(originalPosition, point) < (25))
            return;

        dragged = true;
        container.scrollLeft -= point.x - originalPosition.x;
        container.scrollTop -= point.y - originalPosition.y;
        //after moving the scrollbars, the cursor will be located again in originalPosition
    }
}

function getClasses(element) {
    if(element && element.className)
        return element.className.split(" ");
    return "";
}

function hasClass(element, className) {
// XXX use regexp
    var classes = getClasses(element);
    for (var i in classes)
        if (classes[i] == className)
            return true;
    return false;
}

function Point(x, y) {
    this.x = Math.round(parseInt(x));
    this.y = Math.round(parseInt(y));
    this.equals = function(other) {
        return this.x == other.x && this.y == other.y;
    }
}

function compareToLine(point, B, A) {
    return (point.x - A.x)*(B.y - A.y) - (point.y - A.y)*(B.x - A.x);
}

/**
Tests if a div contains a specified point, with cohordinates relative to the div.
The div must either specify width, height, left, top attributes in css, and be positioned with absolute layout,
or explicitly specify an array of points inside the bounds field of that div; in the last case the polygon must be convex.
If none of these condition is satisifed, then the result is unpredictable.
@param div the div to test
@param point the point relative to the div
*/
function contains(div, point) {
    if (getBounds(div) == undefined) {
        return (
            point.x >= 0
            && point.y >= 0
            && point.x < getWidth(div)
            && point.y < getHeight(div));
    } else {
        var bounds = getBounds(div);
        //for each side
        for(var i = 0 ; i < bounds.length ; i++)
            //checks if point is over that side
            if(compareToLine(
                    new Point(relativePoint.x, relativePoint.y),
                    bounds[i],
                    bounds[(i+1) % bounds.length]
                ) > 0)
                return false;
        return true;
    }
}

function getBounds(div) {
    var classes = getClasses(div);
    for(var i in classes)
        if(bounds[classes[i]] != undefined)
            return bounds[classes[i]];
    return div.bounds;
}

function getWidth(div) {
    if (getBounds(div) == undefined)
        return parseInt(div.style.width.replace("px","")) + borderAndPaddingSize + borderAndPaddingSize;
    
    var max = 0;
    var bounds = getBounds(div);
    for (var i in bounds)
        max = Math.max(max, bounds[i].x);
    return max + 1;
}

function getHeight(div) {
    if (getBounds(div) == undefined)
        return parseInt(div.style.height.replace("px","")) + borderAndPaddingSize + borderAndPaddingSize;

    var max = 0;
    var bounds = getBounds(div);
    for (var i in bounds)
        max = Math.max(max, bounds[i].y);
    return max + 1;
}

function getXPosition(div) {
    return parseInt(div.style.left.replace("px",""));
}

function getYPosition(div) {
    return parseInt(div.style.top.replace("px", ""));
}

function getPosition(div) {
    return new Point(getXPosition(div), getYPosition(div));
}

function setPosition(div, point) {
    div.style.left = point.x + "px";
    div.style.top = point.y + "px";
}

function squaredDistance(pointA, pointB) {
    return (pointA.x - pointB.x) * (pointA.x - pointB.x) + (pointA.y - pointB.y) * (pointA.y - pointB.y);
}

function startMoving(div, point, delay, factor) {
    if(delay == undefined)
        delay = 0;
    //factor may stay undefined

    div.floating_point = point;
    if(div.ismoving)
        ; // do nothing, a timer is already triggered
    else {
        div.ismoving = true;
        if(delay = 0)
            moveTo(div, factor);
        else
            setTimeout("moveTo_NoObjects('" + div.id + "', " + factor + ")", delay);
    }
}

function stopMoving(div) {
    div.ismoving = false;
}

function moveTo_NoObjects(id, factor) {
    var div = document.getElementById(id);
    if(div != undefined)
        moveTo(div, factor);
}

function moveTo(div, factor) {
    if (factor == undefined)
        factor = 9; // slow
    if (getPosition(div).equals(div.floating_point))
        stopMoving(div);
    else {
        setPosition(div, halfway(getPosition(div), div.floating_point, factor));
        setTimeout("moveTo_NoObjects('" + div.id + "', " + factor + ")", 40); // 40 milliseconds == 25 fps
    }
}

function halfway(A, B, factor) {
    return new Point(numberHalfway(A.x, B.x, factor), numberHalfway(A.y, B.y, factor));
}

function numberHalfway(a, b, factor) {
    if (factor == undefined)
        factor = 1;
    var hw = (a * factor + b) / (factor + 1);

    if (a != b && Math.abs(hw - a) < 5) {
        if (b < a)
            hw = Math.max(b, a - 5);
        else
            hw = Math.min(b, a + 5);
    }

    return hw;
}

function layout(randomized, type, moving) {
    if (randomized == undefined)
        randomized = false;
    if (type == undefined)
        type = 'grid';
    if(moving == undefined)
        moving = true;
    var currentPoint = new Point(minX, minY);
    var nextY = minY;
    for (var i in divs) {
        var div = divs[i];
        var divpoint = randomized ?
                clipLocation(new Point(currentPoint.x + Math.random() * LAYOUT_PADDING, currentPoint.y + Math.random() * LAYOUT_PADDING), div)
                :
                currentPoint;
        if (moving)
            startMoving(div, divpoint);
        else
            setPosition(div, divpoint);
            
        switch(type) {
            case 'grid':
                currentPoint.x += getWidth(div) + LAYOUT_PADDING;
                if (currentPoint.x + getWidth(div) > maxX) {
                    currentPoint = new Point(minX, nextY + LAYOUT_PADDING);
                    if (currentPoint.y + getHeight(div) > maxY) {
                        currentPoint = new Point(minX, minY);
                        nextY = minY;
                    }
                }
                nextY = Math.max(nextY, currentPoint.y + getHeight(div));
                break;
            case 'horizontal':
                currentPoint.x += getWidth(div) + LAYOUT_PADDING;
                break;
            case 'vertical':
                currentPoint.y += getHeight(div) + LAYOUT_PADDING;
                break;
        }
    }
}

function randomize(onscreen) {
    if(onscreen == undefined)
        onscreen = true;
    for (var i in divs)
        startMoving(divs[i], getRandomCoordinates(divs[i], onscreen));
}

function randomizeStartingPosition(div) {
    setPosition(div, getRandomCoordinates(div, true));
}



function getXScroll() { return ((document.all)?document.body.scrollLeft:window.pageXOffset) + document.documentElement.scrollLeft + container.scrollLeft; }
function getYScroll() { return ((document.all)?document.body.scrollTop :window.pageYOffset) + document.documentElement.scrollTop  + container.scrollTop ; }

function getMinXVisible() { return Math.max(minX, getXScroll()); }
function getMaxXVisible() { return Math.min(maxX, getXScroll() + window.screen.width ); }
function getMinYVisible() { return Math.max(minY, getYScroll()); }
function getMaxYVisible() { return Math.min(maxY, getYScroll() + window.screen.height); }

function getRandomCoordinates(div, onscreen) {
    if(onscreen == undefined)
        onscreen = false;
    if(onscreen)
        return new Point(
            randomWithinRange(
                getMinXVisible(),
                getMaxXVisible() - getWidth(div)),
            randomWithinRange(
                getMinYVisible(),
                getMaxYVisible() - getHeight(div)));
    else
        return new Point(
            randomWithinRange(minX, maxX - getWidth(div)),
            randomWithinRange(minY, maxY - getHeight(div)));
}

function center(div) {
    startMoving(div, new Point(
        (getMinXVisible() + getMaxXVisible() - getWidth(div)) / 2,
        (getMinYVisible() + getMaxYVisible() - getHeight(div)) / 2)
    );
}

function randomWithinRange(min, max) {
    return Math.random() * (max - min) + min;
}
function clipLocation(point, div) {
    return new Point(
        Math.max(minX, Math.min(maxX - getWidth(div), point.x)),
        Math.max(minY, Math.min(maxY - getHeight(div), point.y)));
    // if the div doesn't fit, it will overflow on the right and/or bottom part of the div
}

function clearSelection() {
    // TODO ie
    if(window.getSelection && window.getSelection().removeAllRanges)
        window.getSelection().removeAllRanges();
}


//////////////
// AUTOHIDE //
//////////////

function makeAutohideDiv(div, hiddenPoint, visiblePoint) {
    div.onmouseover = function() { startMoving(div, visiblePoint, 40, 1); };
    div.onmouseout = function() { startMoving(div, hiddenPoint, 40, 1); };
    setPosition(div, hiddenPoint); // put in hidden position
}
