/**
 * ajaxWM - the free web based window manager for remote terminals
 * Copyright (C) 2007 Dennis Felsing, Andreas Waidler
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $Id: AjaxWM_Window.js 51 2007-11-29 14:33:13Z pwnd $
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * AjaxWM_Window
 *
 * This class is responsible for the correct display of a window, it creates
 * its events, handles user interaction and executes corresponding actions.
 * Each window sets up a dynamic timout after which the synchronization
 * method of the window manager should be called to poll the current contents
 * of this window from the server. After that, the manager feeds the update
 * method of that window.
 *
 * Note that incomming keys are not captured by the window itsself but by the
 * window manager to handle hotkeys first. When no AjaxWM hotkey is detected
 * the keys are sent directly from the manager to the server. The manager will
 * then feed the corresponding window with the response of the server.
 */
//-----------------------------------------------------------------------------

AjaxWM_Window = function(deskId, id, width, height, pos_x, pos_y)
{
//{{{ properties

    // reference of this object
    var _this = this;
    var _class = 'AjaxWM_Window';

    var _deskId = deskId;
    var _id = id;
    var _width  = width;
    var _height = height;
    var _charWidth;
    var _lineHeight;

    // the status of this window
    // values: initializing, default, hidden, maximized, closing
    var _status = 'initializing';

    // timeout value (in milliseconds) after which the window
    // will check if something happened.
    // default value (minimum)
    var _defaultTimeout = 30;
    // maximum value
    var _maxTimeout = 2400;
    // dynamically increasing value, generated by AjaxWM_Window.update()
    var _dynTimeout = _defaultTimeout;
    // timeout object
    var _timeout;

    // position of the mouse cursor (set by some event handlers)
    var _mouseX; // x axis
    var _mouseY; // y axis

    // contains direction to which the window is resized
    var _direction;

    // HTML nodes

    var _node = new Object();

//}}}
//{{{ this.focus = function()

    /**
     * Focuses this window.
     * Callback function.
     */

    this.focus = function()
    {
        ajaxWM.switchToWindow(_this);
    }

//}}}
//{{{ this.show = function()

    /**
     * Displays this window.
     */

    this.show = function()
    {
        _node['main'].style.display = "block";
        _status = "default";
    }

//}}}
//{{{ this.hide = function()

    /**
     * Hides the window.
     */

    this.hide = function()
    {
        _node['main'].style.display = "none";
        _status = "hidden";
    }

//}}}
//{{{ this.update = function(contents)

    /**
     * Updates the window with the contents passed.
     *
     * Checks what kind of response has been passed. When the response
     * is <idem></idem> nothing changed and the content is left untouched
     * and the timeout until the next synchronization will be increased.
     * If not, the content will be updated and the timeout set back to
     * its default value.
     */

    this.update = function(contents)
    {
//        ajaxWM.debugMessage(_class, "update",
//            "id=" + _id + ", tagName=" + contents.tagName); 

        window.clearTimeout(_timeout);

        if (contents.tagName != "pre") {
            _dynTimeout *= 2;
            if (_dynTimeout > _maxTimeout)
                _dynTimeout = _maxTimeout;
        } else {
            Sarissa.updateContentFromNode(contents, _node['content']);
            _dynTimeout = _defaultTimeout;
        }

        if (_status != 'closing')
            _timeout = window.setTimeout(_sync, _dynTimeout);
    }

//}}}
//{{{ this.moveToDeskId = function(id)

    /**
     * Moves this window to the desktop with the passed ID.
     */

    this.moveToDeskId = function(id)
    {
//        ajaxWM.getDesktopById(_deskId).removeChild(_node['main']);
        ajaxWM.getDesktopById(id).appendChild(_node['main']);
        this._deskId = id;
    }

//}}}
//this.moveTo = function(x, y)//{{{

    /**
     * Moves window to specified coordinates.
     *
     * Coordinates have to be integers.
     *
     * @param x int Position on x-axis
     * @param y int Position on y-ayis
     * @return bool True on moving being (partially) successful
     */

    this.moveTo = function(x, y)
    {
        x = parseInt(x);
        if (isNaN(x))
            return false;
        y = parseInt(y);
        if (isNaN(y))
            return false;

        // TODO: move all classes in _node['setpos']
        _node['main'].style.left = x + "px";
        _node['main'].style.top  = y + "px";

        return true;
    }

//}}}
//this.resizeTo = function(x, y)//{{{

    /**
     * Resizes a window to the amount of characters/lines passed.
     *
     * @param x int New width [characters]
     * @param y int New height [lines]
     * @return bool True on resizing being (partially) successful
     */

    this.resizeTo = function(x, y)
    {
        x = parseInt(x);
        if (isNaN(x))
            return false;
        y = parseInt(y);
        if (isNaN(y))
            return false;

        _width  += x;
        _height += y;


        // FIXME resize all elements in _node['setsize']

        _node['main'].style.width  = _getProperty(_node['main'], "width")
                                   + x + "px";
        _node['main'].style.height = _getProperty(_node['main'], "height")
                                   + y + "px";

        return true;
    }

//}}}
//{{{this.close = function()

    /**
     * Starts the closing procedure of this window.
     *
     * Sets a flag that this window is currently closing and calls
     * the window manager to close it on the server. Window
     * is destroyed by the manager if the server reports success.
     */

    this.close = function()
    {
        _status = 'closing';
        ajaxWM.closeWindow(_this);
    }

//}}}
//{{{ this.destroy = function()

    /**
     * Destroy this window: Remove nodes, free resources.
     */

    this.destroy = function()
    {
        if (_status != 'closing') {
            ajaxWM.debugMessage(_class, 'destroy',
                'Warning. Status was ' + _status);
            _status = 'closing';
        }

        ajaxWM.getDesktopById(_deskId).removeChild(_node['main']);
        _node['main'].innerHTML = '';
        delete(_node['main']);

        return true;
    }

//}}}
    this.getDeskId = function(){return _deskId}
    this.getId     = function(){return _id}
    this.getWidth  = function(){return _getProperty(_node['main'], 'width')}
    this.getHeight = function(){return _getProperty(_node['main'], 'height')}
    this.getStatus = function(){return _status}
    this.getNode   = function(){return _node['main']}
//{{{ function _init()

    /**
     * Initializes properties of this window and creates its nodes.
     *
     * This method will create every HTML node and their events without
     * noticing the user. The objects method _draw() should be called
     * afterwards which will finally display it.
     * It also calls the method _sync() which synchronizes its contents
     * with the server and sets up a timeout for further synchronizations.
     */

    function _init()
    {
        ajaxWM.debugMessage(_class, "_init",
            "id=" + _id + ', w=' + _width + ', h=' + _height);
        var template = ajaxWM.getTemplate();
        _node = template.cloneNodes('window');
        ajaxWM.getDesktopById(_deskId).appendChild(_node['main']);
        _registerEvents();
    }

//}}}
//{{{ function _draw()

    /**
     * Starts displaying the window and its contents.
     *
     * Should be called only once after init().
     * At this moment, this is only a "wrapper" for display() but it could get
     * some fancy features in the future.
     */

    function _draw()
    {
        _node['main'].style.display = "block";
        _status = "default";
    }

//}}}
//{{{ function _sync()

    /**
     * Synchronizes the contents of this window with the server.
     *
     * Calls the AjaxWM_Manager to send a synchronization event for
     * this window to the server. AjaxWM_Manager then calls
     * AjaxWM_Window.update().
     * Wont be executed when window is already closing to prevent a
     * synchronization event to be enqueued after the close- or
     * kill-window-event.
     */

    function _sync()
    {
        if (_status == 'closing')
            return;

        _node['title'].innerHTML = 'ajaxWM-' + ajaxWM.getVersion();
        ajaxWM.syncWindow(_this);
    }

//}}}
//function _calcCharSize()//{{{

    /**
     * Calculates width and height of a character
     * (in pre.content) in pixels.
     * FIXME: add calculation of height
     */

    function _calcCharSize()
    {
        var width = _getProperty(_node['content'], 'width');
        _charWidth = width / _width;
        ajaxWM.debugMessage(_class, 'calcCharSize',
            _charWidth + ' = ' + width + ' / ' + _width);
    }

//}}}
//{{{ function _getProperty(node, property, withUnit)

    /**
     * Gives a property (size) of a document node.
     *
     * @param  node     the node to get information about
     * @param  property the information to get
     * @param  withUnit optional. Return the value with its unit
     * @return int/float size of the property passed without unit
     * @return string    size of the property passed without unit
     */

    function _getProperty(node, property, withUnit)
    {
        var prpty  = node.style[property];
        if (true == withUnit)
            var result = prpty;
        else
            var result = prpty.substr(0, prpty.length - 2);

        if (result == "" || isNaN(result)) {
            // fallback determination of property,
            // all values are returned WITHOUT unit, unit would be px
            switch (property) {
            case "width":
                result = node.clientWidth;
                break;

            case "height":
                result = node.clientHeight;
                break;

            case "left":
                result = node.offsetLeft;
                break;

            case "top":
                result = node.offsetTop;
                break;

            default:
                ajaxWM.debugMessage(_class, "_getProperty",
                        "wrong parameter: " + property);
            }

            if (true == withUnit)
                result += 'px';
        }
        if (false == withUnit)
            result = parseFloat(result);

        return result;
    }

//}}}
//function _registerEvents()//{{{

    /**
     * Set up all events of this window.
     *
     * TODO: add recursion?
     */

    function _registerEvents()
    {
        var events = _node['events'];
        var length = events.length;
        for (var i = 0; i < length; ++i) { // event type
            for (e in events[i]) { // event
                if (typeof(events[i][e]) != 'object')
                    continue;
                var action = events[i][e]['action'];
                switch (action) {
                case 'focus':
                    var fn = _this.focus;
                    break;
                case 'move':
                    var fn = _startMove;
                    break;
                case 'resize':
                    var fn = _startResize;
                }
                var path = events[i][e]['path'];
                var node = traceNode(path, _node['main']);
                node[e] = fn;
            }
        }
        delete(_node['events']);
    }

//}}}
//function _setClassForMode(mode)//{{{


    /**
     * Enables move- or resize mode for all nodes.
     *
     * Changes the class names of all nodes of this window that have
     * the attribute move="change" or resize="change" to the
     * appropriate class name.
     *
     * @param mode string Mode that should be set. "move" or "resize"
     * @return void
     */

    function _setClassForMode(mode)
    {
        switch (mode) {
        case 'move':
            var key  = 'chmove';
            var suffix = '_moving';
            break;

        case 'resize':
            var key  = 'chresize';
            var suffix = '_resizing';
            break;

        default: 
            ajaxWM.debugMessage(_class, '_setClassForMode', 'Wrong mode '+mode);
            return;
        }
        // for each element that should change its class
        var length = _node[key].length;
        for (var i = 0; i < length; ++i) {
            var node = _node[key][i];

            // continue if the current element has the suffix already added
            if (-1 != node.className.indexOf(suffix))
                continue;

            // does this node has the size set manually?
            var setSize = false;
            if (-1 != _node['size']['set'].indexOf(node)) {
                var setSize = true;
                var width   = _getProperty(node, 'width', true);
                var height  = _getProperty(node, 'height', true);
            }

            node.className += suffix;

            if (setSize) {
                node.style.width  = width;
                node.style.height = height;
            }
        }
    }

//}}}
//function _unsetClassForMode(mode)//{{{

    /**
     * Disables move- or resize mode for all nodes.
     *
     * @see AjaxWM_Window::_setClassForMode()
     * @param mode string Mode that should be unset. "move" or "resize"
     * @return void
     */

    function _unsetClassForMode(mode)
    {
        switch (mode) {
        case 'move':
            var key  = 'chmove';
            var suffix = /_moving/;
            break;

        case 'resize':
            var key  = 'chresize';
            var suffix = /_resizing/;
            break;

        default: 
            ajaxWM.debugMessage(_class, '_unsetClassForMode', 'Wrong mode '+mode);
            return;
        }

        var length = _node[key].length;
        for (var i = 0; i < length; ++i) {
            _node[key][i].className =
            _node[key][i].className.replace(suffix, '');
        }
    }

//}}}
//{{{ function _onMove(e)

    /**
     * Moves the window to the current mouse position.
     * Event handler for onmousemove if move mode is enabled.
     *
     * @param e onmousemove event that contains new mouse position
     * @return false to finish this event
     */

    function _onMove(e)
    {
        // substract difference between window border and mouse pointer
        // which was in the window while entering move mode
        _this.moveTo(e.pageX - _mouseX, e.pageY - _mouseY);

        return false;
    }

//}}}
// {{{function _startMove(e) 

    /**
     * Enters move-mode to move this window around.
     *
     * Stores the current position of the cursor in this window,
     * sets up event handlers for displaying new window state and
     * fixing window on that position.
     *
     * @param e onmousedown onmousedown-event that initiated move mode
     * @return false to finish this event
     */

    function _startMove(e) 
    {
        ajaxWM.debugMessage(_class, "_startMove",
                            "Window " + _id + ": Move mode entered.");

        _this.focus();

        // calculate distance between window border and
        // mouse pointer (in window)
        _mouseX = e.pageX - _getProperty(_node['main'], "left");
        _mouseY = e.pageY - _getProperty(_node['main'], "top");

        _setClassForMode('move');

        // event handlers
        document.onmousemove = _onMove;
        document.onmouseup   = _stopMove;

        // block event from further execution:
        return false;
    }

// }}}
//{{{function _stopMove(e)

    /**
     * Fixes the window on the new position.
     *
     * Leaves move mode by resetting onmouse*-handlers.
     *
     * @param e onmouseup event that should stop window from being moved
     * @return false to finish this event
     */

    function _stopMove(e)
    {
        ajaxWM.debugMessage(_class, "_startMove",
                            "Window " + _id + " stopped moving.");

        _unsetClassForMode('move');

        // reset handlers
        document.onmousemove = null;
        document.onmouseup   = null;

        // block event:
        return false;
    }

//}}}
//{{{//function _onResize(e)

    /**
     * Resizes the window.
     * 
     * Resizing is only done step by step, every in-/decrease is equal
     * to the width/height of a character (terminal-like ;) ).
     * Changes window position if window should be resized to the north
     * or west so that the other borders will remain on their current
     * positions.
     *
     * @param e onmousemove Onmousemove-event that launched this function
     * @return true To not block the event from further effects
     */

    function _onResize(e)
    {
        var pixelsX = 0;
        var pixelsY = 0;
        var chars   = 0;
        var lines   = 0;
    // get amount of pixels the window should be resized by//{{{

        switch (_direction) {
        case 'ne':
            pixelsX = e.pageX - _mouseX;
        case 'n':
            pixelsY = _mouseY - e.pageY;
            break;

        case 'se':
            pixelsY = e.pageY - _mouseY;
        case 'e':
            pixelsX = e.pageX - _mouseX;
            break;

        case 'sw':
            pixelsX = _mouseX - e.pageX;
        case 's':
            pixelsY = e.pageY - _mouseY;
            break;

        case 'nw':
            pixelsY = _mouseY - e.pageY;
        case 'w':
            pixelsX = _mouseX - e.pageX;
            break;
        }

    //}}}
    // calculate amount of chars/lines the window should be resized by//{{{

        if (isNaN(_charWidth) || isNaN(_lineHeight))
            _calcCharSize();

        if (pixelsX < 0) {
            // TODO: remove Math.abs() and add Math.ceil()?
            chars = - Math.floor(Math.abs(pixelsX)/_charWidth);
        } else if (pixelsX > 0) {
            chars = Math.floor(pixelsX/_charWidth);
        }

        if (pixelsY < 0) {
           // TODO: remove Math.abs() and add Math.ceil()?
            lines  = - Math.floor(Math.abs(pixelsY)/_lineHeight);
        } else if (pixelsY > 0) {
            lines = Math.floor(pixelsY/_lineHeight);
        }

    //}}}
    // (possibly) move window//{{{

        if (-1 != _direction.indexOf('w') || -1 != _direction.indexOf('n')) {
            var x = _getProperty(_node['main'], 'left') - pixelsX;
            var y = _getProperty(_node['main'], 'top' ) - pixelsY;
            _this.moveTo(x, y);
        }

    //}}}
    
        _this.resizeTo(chars * _charWidth, lines * _lineHeight);
    
        return true;
    }

//}}}
//{{{ function _startResize(e)

    /**
     * Starts resizing this window.
     * Event handler for onmousedown on HTML nodes that should be able
     * to resize this window.
     *
     * The mouse position and the direction to where the window should
     * be resized are stored as object properties. The direction will
     * be extracted from the className of the node that initiated this
     * event. Resize-mode is active until an onmouseup-event is launched.
     *
     * @param e event Onmousedown event, launched by an HTML node
     * @return false;
     */

    function _startResize(e)
    {
        try {
            // store mouse position
            _mouseX = e.pageX;
            _mouseY = e.pageY;

            _this.focus();

            // get direction from class name, e.g. border_n or border_ne
            var direction = this.className.substr(-2);
            if ('_' == direction[0])
                direction = direction[1];
            else if ('n' != direction[0] && 's' != direction[0])
                throw 'class';

            _direction = direction;

            // set up handler that finally resizes this window
            document.onmouseup  = _stopResize;
            document.onmousmove = _onResize;

            _setClassForMode('resize');
        } catch (ex) {
            switch (ex) {
            case 'class':
                var error = 'Wrong class name: ' + this.className;
                break;

            default:
                var error = 'Letting unhandled exception bubble up.';
                ajaxWM.debugMessage(_class, '_startResize', error);
                throw ex;
            }
            ajaxWM.debugMessage(_class, '_startResize', error);
        } finally {
            // block further execution
            return false;
        }
    }

//}}}
//{{{ function _stopResize(e)

    /**
     * Stops resizing mode and finally resizes the window.
     * Event handler for onmouseup after _startResize() was called.
     *
     * @param e event Onmouseup event
     * @return false;
     */

    function _stopResize(e)
    {
        document.onmouseup   = null;
        document.onmousemove = null;
        ajaxWM.syncWindow(_this, true);
        _unsetClassForMode('resize');
        return false;
    }

//}}}

    // set up properties, nodes, event handlers, timeouts
    _init();
    _sync();
    // draw this window
    _draw();
}

//-----------------------------------------------------------------------------
// Ensure that the code does not exceed 79 characters per line using an indent
// of 4 spaces/level. Use NO TABS at all. Leave the mode line as it is. For
// further information see file docs/CodingConvention.
// vim: et ts=4 sw=4 fdm=marker
