/*
    Layer: class for wrapping CSS2 positioned elements
    Author: Theo M. Gerrits (tmg)
    
    Dependencies: Logger.js
                  Timer.js
    
    JavaScript1.5

    Copyright (C) 2005 Ontwerpfontein
*/


/*
    Description     : constructor
    Parameters      : id: the Layer id
                      parentWindow: (optional) parent window of the HTML element; if undefined, self is used.
    Properties:
        id: HTML id of the layer. This may be an existing id, in which case the element properties are used,
            or it may be a completely new element that will be created and added to the document
        left: left position of the topleft corner in pixels
        top: top position of the topleft corner in pixels
        width: width in pixels
        height: height in pixels
        clipLeft: relative position: left position of topleft corner of the clipping rectangle in pixels 
        clipTop: relative position: top position of topleft corner of  the clipping rectangle in pixels
        clipWidth: width of the clipping rectangle
        clipHeight: height of the clipping rectangle
        htmlElement: the HTML Element object attached to this layer
        style: the Style object of htmlElement
        globalName: globally stored id (in the top window)
        parentWindow: containing Window
        xDestination: x-coordinate of the destination of the current slide in pixels
        yDestination: y-coordinate of the destination of the current slide in pixels
        horizontalDistance: horizontal distance to slide in pixels
        verticalDistance: horizontal distance to slide in pixels
        stepDistance: distance to slide per slide step in pixels
        stepDuration: duration of 1 slide step in seconds
        dx: horizontal displacement per step in pixels
        dy: vertical displacement per step in pixels
        numberOfStepsRemaining: number of steps to slide yet
        timer: internal Timer object to drive the slide
    Remark: some style properties will be set for each layer:
        positioning will always be absolute
        overflow will always be visible
*/
function Layer(id, parentWindow)
{
    this.id = id;
    this.parentWindow = (parentWindow) ? parentWindow : window;
    this.htmlElement = this.parentWindow.document.getElementById(id);
    LOG.debug("Trying to find element with id: " + id);
    if (this.htmlElement)
    {
        LOG.debug("Element found: " + this.htmlElement);
    }
    else
    {
        LOG.debug("Element not found: creating new HTML element");
        this.htmlElement = document.createElement("DIV");
        this.htmlElement.id = id;
        document.body.appendChild(this.htmlElement);
    }

    this.style = this.htmlElement.style;
    
    // always use absolute positioning
    this.style.position = "absolute";

    // NB1: the style properties are set only if the style was defined inline!! Therefore, initialise with offsetXxx.
    // NB2: don't use style.pixelXxx since this works in IE only.
    // NB3: IE6 returns not the style defined height, but the rendered height:
    //        the layer content may cause this height to be larger than defined.
    this.left = this.htmlElement.offsetLeft;
    this.top = this.htmlElement.offsetTop;
    this.width = this.htmlElement.offsetWidth;
    this.height = this.htmlElement.offsetHeight;

    // don't use overflow property to set clipping. Instead, use the clip property
    this.style.overflow = "visible";
    this.initClippingValues();

    // slide related members
    this.xDestination = this.left;
    this.yDestination = this.top;
    this.stepDistance = this.DEFAULT_STEP_DISTANCE;
    this.stepDuration = this.DEFAULT_STEP_DURATION;
    this.horizontalDistance = 0;
    this.verticalDistance = 0;
    this.dx = 1;
    this.dy = 1;
    this.numberOfStepsRemaining = 0;
    this.timer = new Timer(id + Layer.prototype.TIMER_SUFFIX);
    
    this.globalName = this.id + Layer.prototype.LAYER_SUFFIX;
    eval("top." + this.globalName + "=this;");
    LOG.debug("Layer created: " + this.id);
}

// Layer constants
Layer.prototype.DEFAULT_LAYER_PREFIX = "layer";
Layer.prototype.LAYER_SUFFIX = "Layer";
Layer.prototype.TIMER_SUFFIX = "Slide";
Layer.prototype.DEFAULT_STEP_DISTANCE = 10;
Layer.prototype.DEFAULT_STEP_DURATION = .02;
Layer.prototype.clipEdgeIndex = new Object();
Layer.prototype.clipEdgeIndex["t"] = 0;
Layer.prototype.clipEdgeIndex["r"] = 1;
Layer.prototype.clipEdgeIndex["b"] = 2;
Layer.prototype.clipEdgeIndex["l"] = 3;
Layer.prototype.clipEdgeIndex["top"] = 0;
Layer.prototype.clipEdgeIndex["right"] = 1;
Layer.prototype.clipEdgeIndex["bottom"] = 2;
Layer.prototype.clipEdgeIndex["left"] = 3;

// static method

// Description     : build Layer objects based on DIV elements with an id that starts with a certain prefix
// Parameters      : prefix: the id prefix to search for
// Returns         : void
Layer.prototype.createLayers = function (prefix)
{
    if (!prefix)
        prefix = Layer.prototype.DEFAULT_LAYER_PREFIX;

    // loop through all "DIV" tags to find layers named "layer..."
    var divElements = document.getElementsByTagName("DIV");
    var n = 0;
    for (var i = 0; i < divElements.length; i++)
    {
        var layerId = divElements[i].id;
        if (layerId.substring(0, prefix.length) == prefix)
        {
            new Layer(layerId); // object is saved in the current Window object by constructor
            n++;
        }
    }
    
    LOG.info(n + " Layers created from DIVs with id starting with " + prefix);
}

// Description     : resize the layer
// Parameters      : w: (optional) new width in pixels, a value of 0 or less will be ignored
//                   h: (optional) new height in pixels, a value of 0 or less will be ignored
// Returns         : void
Layer.prototype.resize = function (w, h)
{
    if (w != null && w >= 0)
        this.width = w;
    if (h != null && h >= 0)
        this.height = h;

    this.style.width = this.width + "px";
    this.style.height = this.height + "px";
    LOG.debug("Layer " + this.id + " resized to " + this.width + "x" + this.height);
}

// Description     : resize the layer to fit its content
// Parameters      : none
// Returns         : void
Layer.prototype.resizeToContent = function ()
{
    this.style.width = "auto";
    this.style.height = "auto";
    this.resize(this.htmlElement.offsetWidth, this.htmlElement.offsetHeight);
}

// Description     : vertically resize the layer to fit its content
// Parameters      : none
// Returns         : void
Layer.prototype.resizeVerticallyToContent = function ()
{
    this.style.height = "auto";
    this.resize(null, this.htmlElement.offsetHeight);
}

// Description     : move the layer to the specified position
// Parameters      : destination in pixels
// Returns         : void
Layer.prototype.moveTo = function (x, y)
{
    if (x != null)
        this.left = x;
    if (y != null)
        this.top = y;

    this.style.left = this.left + "px";
    this.style.top = this.top + "px";
    LOG.debug("Layer " + this.id + " moved to (" + this.left + "," + this.top + ")");
}

// Description     : move the layer relatively from its current position
// Parameters      : displacement in pixels
// Returns         : void
Layer.prototype.moveBy = function (dx, dy)
{
    if (!dx)
        dx = 0;
    if (!dy)
        dy = 0;
    this.moveTo(this.left + dx, this.top + dy);
}

// Description     : make the layer visible
// Parameters      : none
// Returns         : void
Layer.prototype.show = function ()
{
    this.style.visibility = "visible";
}

// Description     : hide the layer
// Parameters      : none
// Returns         : void
Layer.prototype.hide = function ()
{
    this.style.visibility = "hidden";
    this.style.visibility = "hidden";
}

// Slide Methods

// Description     : set a destination position and start sliding toward this destination
// Parameters      : xDestination: x-coordinate of destination position in pixels
//                   yDestination: y-coordinate of destination position in pixels
//                   stepDistance: distance in pixels of 1 slide step
//                   stepDuration: duration of 1 step in milliseconds 
// Returns         : void
Layer.prototype.slideTo = function (xDestination, yDestination, stepDistance, stepDuration)
{
    this.xDestination = (xDestination != null) ? xDestination : this.left;
    this.yDestination = (yDestination != null) ? yDestination : this.top;
    this.setStepDistance(stepDistance);
    this.setStepDuration(stepDuration);

    this.horizontalDistance = this.xDestination - this.left;
    this.verticalDistance = this.yDestination - this.top;
    
    this.slideStart();
}

// Description     : set a destination position relative to the current one and start sliding toward this destination
// Parameters      : horizontalDistance: horizontal displacement in pixels
//                   verticalDistance: vertical displacement in pixels
//                   stepDistance: distance in pixels of 1 slide step
//                   stepDuration: duration of 1 step in milliseconds 
// Returns         : void
Layer.prototype.slideBy = function (horizontalDistance, verticalDistance, stepDistance, stepDuration)
{
    this.horizontalDistance = (horizontalDistance) ? horizontalDistance : 0;
    this.verticalDistance = (verticalDistance) ? verticalDistance : 0;
    this.setStepDistance(stepDistance);
    this.setStepDuration(stepDuration);
    
    this.xDestination = this.left + this.horizontalDistance;
    this.yDestination = this.top + this.verticalDistance;

    this.slideStart();
}

Layer.prototype.setStepDistance = function (stepDistance)
{
    this.stepDistance = (stepDistance != null && stepDistance > 0) ? stepDistance : this.DEFAULT_STEP_DISTANCE;
}

Layer.prototype.setStepDuration = function (stepDuration)
{
    this.stepDuration = (stepDuration != null && stepDuration > 0) ? stepDuration/1000 : this.DEFAULT_STEP_DURATION;
}

// Description     : start the slide; the layer will move according to the current slide property values
// Parameters      : none
// Returns         : void
// Remark          : private function
Layer.prototype.slideStart = function ()
{
    // do nothing if already sliding
    if (this.isSlideActive())
        return;
    
    if (this.onSlideStart)
        this.onSlideStart();

    // tmg - this is in general a non-integer value
    this.numberOfStepsRemaining = Math.sqrt(Math.pow(this.horizontalDistance,2) + Math.pow(this.verticalDistance,2))/this.stepDistance;
    LOG.debug("Number of steps: " + this.numberOfStepsRemaining);
    // finish the slide if almost no distance to cover
    if (this.numberOfStepsRemaining <= 1)
    {
        this.slideFinish();
        return;
    }
    
    this.dx = this.horizontalDistance/this.numberOfStepsRemaining;
    this.dy = this.verticalDistance/this.numberOfStepsRemaining;
    
    this.doSlideStep();
}

// Description     : perform 1 slide step
// Parameters      : none
// Returns         : void
// Remark          : private function
Layer.prototype.doSlideStep = function ()
{
    if (!this.isSlideActive())
        return;

    if (this.numberOfStepsRemaining-- > 1)
    {
        this.moveBy(this.dx, this.dy);
        if (this.onSlideStep)
            this.onSlideStep();
        this.timer.start(this.stepDuration, this.doSlideStep, null, this);
    }
    else
    {
        this.slideFinish();
    }
}

// Description     : end the slide correctly
// Parameters      : none
// Returns         : void
// Remark          : private function
Layer.prototype.slideFinish = function ()
{
    this.stop();
    // finish exactly on destination point
    this.moveTo(this.xDestination, this.yDestination);
    if (this.onSlideStep)
        this.onSlideStep();
    if (this.onSlideEnd)
        this.onSlideEnd();
}

// Description     : check if a slide is in progress
// Parameters      : none
// Returns         : true if sliding, false otherwise
Layer.prototype.isSlideActive = function ()
{
    return this.numberOfStepsRemaining != 0;
}

// Description     : stop sliding immediately
// Parameters      : none
// Returns         : void
Layer.prototype.stop = function ()
{
    this.numberOfStepsRemaining = 0;
}

// Description     : initialise sliding; override if necessary
// Parameters      : none
// Returns         : void
Layer.prototype.onSlideStart = null;

// Description     : action to perform repeatedly when sliding, override if necessary
// Parameters      : none
// Returns         : void
Layer.prototype.onSlideStep = null;

// Description     : action to perform when the slide has finished, override if necessary
// Parameters      : none
// Returns         : void
Layer.prototype.onSlideEnd = null;

// Clip Methods

// Description     : set the clipping rect
// Parameters      : the four clipping sides
// Returns         : void
// Remark          : private function
Layer.prototype.initClippingValues = function ()
{
    this.clipTop = this.getClipValue("t");
    var clipRight = this.getClipValue("r");
    var clipBottom = this.getClipValue("b");
    this.clipLeft = this.getClipValue("l");

    this.clipLeft = (this.clipLeft != null) ? this.clipLeft : 0;
    this.clipTop = (this.clipTop != null) ? this.clipTop : 0;
    this.clipWidth = (clipRight != null) ? clipRight - this.clipLeft : this.width - this.clipLeft;
    this.clipHeight = (clipBottom != null) ? clipBottom - this.clipTop : this.height - this.clipTop;
    
    LOG.debug("Clip values: (" + this.clipLeft + "," + this.clipTop + ") " + this.clipWidth + "x" + this.clipHeight);
}

// Description     : the content of the layer may overflow
// Parameters      : none
// Returns         : void
Layer.prototype.overflow = function ()
{
    this.setClippingRect();
}

// Description     : the layer clips its content
// Parameters      : none
// Returns         : void
Layer.prototype.clip = function ()
{
    this.setClippingRect(this.clipTop, this.clipLeft + this.clipWidth, this.clipTop + this.clipHeight, this.clipLeft);
    LOG.debug("Layer " + this.id + " clipped");
}

// Description     : determine if the layer is clipped
// Parameters      : none
// Returns         : true if the layer is clipped, false otherwise
Layer.prototype.isClipped = function ()
{
    return    this.getClipValue("t") != null
           || this.getClipValue("r") != null
           || this.getClipValue("b") != null
           || this.getClipValue("l") != null;
}

// Description     : set the clipping rectangle
// Parameters      : the (optional) coordinates of the clipping rectangle. If a coordinate is not defined, that coordinate will be set to "auto".
// Returns         : void
// Remark 1        : private function, does not adjust clipXxx members
// Remark 2        : the clipping rect is a real rect, so the parameters are actual positions, not clipping side widths
Layer.prototype.setClippingRect = function (clipTop, clipRight, clipBottom, clipLeft)
{
    clipTop = (clipTop != null) ? clipTop + "px": "auto";
    clipRight = (clipRight != null) ? clipRight + "px": "auto";
    clipBottom = (clipBottom != null) ? clipBottom + "px": "auto";
    clipLeft = (clipLeft != null) ? clipLeft + "px": "auto";
    
    // don't use commas, since this will not work in internet Explorer 6
    // Mozilla will automatically add commas if clip is read
    this.style.clip = "rect(" + clipTop + " " + clipRight + " " + clipBottom + " " + clipLeft + ")";
}

// Description     : set the clipping area to a given position
// Parameters      : the (optional) absolute topleft position in pixels of the clipping area
// Returns         : void
Layer.prototype.moveClipTo = function (clipLeft, clipTop)
{
    if (clipTop != null)
        this.clipTop = clipTop;
    if (clipLeft != null)
        this.clipLeft = clipLeft;

    this.clip();
}

// Description     : move the clipping area relatively from its current position 
// Parameters      : the (optional) relative distance in pixels that the clipping area should move
// Returns         : void
Layer.prototype.moveClipBy = function (dx, dy)
{
    if (!dx)
        dx = 0;
    if (!dy)
        dy = 0;

    this.moveClipTo(this.clipLeft + dx, this.clipTop + dy);
}

// Description     : resize the clipping area
// Parameters      : the (optional) width and height in pixels of the clipping area
// Returns         : void
Layer.prototype.resizeClip = function (clipWidth, clipHeight)
{
    if (clipWidth != null && clipWidth >= 0)
        this.clipWidth = clipWidth;
    if (clipHeight != null && clipHeight >= 0)
        this.clipHeight = clipHeight;

    this.clip();
}

// Description     : set the clipping area with on all sides the same clipped width 
// Parameters      : borderWidth: the width of the clipped border in pixels
// Returns         : void
Layer.prototype.setClipBorder = function (borderWidth)
{
    this.clipLeft = borderWidth;
    this.clipTop = borderWidth;
    this.clipWidth = this.width - 2*borderWidth;
    this.clipHeight = this.height - 2*borderWidth;
    this.clip();
}

// Description     : get a specific clip value
// Parameters      : the edge for which the clipping value should be read ("t" = top, "r" = right, "b" = bottom, "l" = left)
// Returns         : the clipping value in pixels or null if it was not defined or defined as "auto"
Layer.prototype.getClipValue = function (whichEdge)
{
    var clipRect = this.style.clip;
    if (clipRect.substring(0, 5) == "rect(")
    {
        var clipCoordinates = clipRect.substring(5, this.style.clip.length - 1);
        var clipValues = clipCoordinates.split(" ");
        var clipValue = clipValues[this.clipEdgeIndex[whichEdge]];
        // remove commas (Mozilla, not Internet Explorer 6)
        clipValue = clipValue.replace(/,/g, "");
        if (clipValue.substring(clipValue.length - 2) == "px")
            clipValue = clipValue.substring(0, clipValue.length - 2);
        if (clipValue == "auto")
            clipValue = null;
    }
    else
        clipValue = null;
    
    return clipValue;
}

// Description     : write HTML content dynamically to the layer
// Parameters      : the new HTML content
// Returns         : void
Layer.prototype.write = function (newHTML)
{
    this.htmlElement.innerHTML = newHTML;
}
