var HTML_EditableBlock_Factory = {    
    processorUrl: null, // defined dynamically
    uploadCount: 0,

    /**
     * Initialize object. Must be called before first usage.
     */
    init: function(processorUrl) {
        this.processorUrl = processorUrl;
    },

    /**
     * Must be called on each editable block.
     * Such blocks will be replaced by form element(s) on double click.
     */
    prepareBlock: function(cont, coord) {
        var th = this;
        
        cont.className = (cont.className? cont.className + " " : "") + "HTML_EditableBlock";
            
        cont.onedit = function() {
            // Fetch element type (block or non-block).
            var blockElt = this;
            while (blockElt && (th.getCurStyle(blockElt, 'display')||'').toLowerCase() == 'inline') {
                blockElt = blockElt.parentNode;
            }
            // Set width and height of the element.
            coord.w = blockElt.offsetWidth;
            coord.h = this.offsetHeight;
            
            // We must do it here, because while initialization there is no innerHTML filled yet.
            cont.oldInnerHTML = cont.innerHTML;

            // Send AJAX request to load form elements.
            coord.value = null;
            th.ajaxRequest(
                'load', cont, 'GET', th.processorUrl, { 'coord': coord }, 
                function(data) {
                    cont.replaceByForm(data.html);
                }
            )

            return false;
        }
        
        // Called on double click.
        // Assign on timeout for Safari (why? don't know, experimented).
        setTimeout(function() {
            cont.ondblclick = function(e) {
                cont.onedit();
                return th.cancelEvent(e);
            }
        }, 10);
        
        // Set callback handler for apply value.
        cont.applyValue = function(value) {
            // Send AJAX request to save the data.
            var upload = null;
            if (value && value.tagName && value.tagName.toLowerCase() == 'input' && value.type.toLowerCase() == "file") {
                coord.value = 'upload';
                upload = value;
            } else {
                coord.value = value;
            }
            th.onbeforechange(this);
            th.ajaxRequest(
                'save', cont, 'POST', th.processorUrl, { coord: coord, upload: upload }, 
                function(data) {
                    // Find where to place result of AJAX loading.
                    var target = data.target;
                    if (target == '') {
                        cont.ownerDocument.location.reload();
                        return;
                    } else if (target) {
                        target = document.getElementById(target);
                    } else {
                        target = null;
                    }
                    if (!target) target = cont; // by default - to same container
                    target.innerHTML = data.html;
                    th.runScripts(target.getElementsByTagName('SCRIPT'));
                    th.onafterchange(target);
                    if (!data.target) {
                        th.prepareBlock(cont, coord);
                    }
                }
            )
        }
        
        // Cancel editing. 
        cont.cancel = function() {
            if (this.oldInnerHTML != null) {
                this.innerHTML = this.oldInnerHTML;
            }
            th.prepareBlock(this, coord);
        }
        
        // Replace content of container cont with form field(s) html.
        // All needed handlers (like Esc key etc.) are also set.
        cont.replaceByForm = function(html) {
            this.innerHTML = html;
            
            // Execute JS scripts.
            th.runScripts(this.getElementsByTagName('SCRIPT'));
            
            // Assign reference to container to each subnode.
            var foundFormElt = false;
            th.foreachDescendant(this, function(e) {
                e.editable = cont;
                if (!foundFormElt) {
                    if (e.tagName == 'INPUT' || e.tagName == 'SELECT' || e.tagName == 'TEXTAREA') {
                        e.focus();
                        foundFormElt = true;
                    }
                }
            });
            
            // Set Esc handler.
            this.onkeypress = function(e) {
                if (!e) e = window.event;
                if (e.keyCode == 27) {
                    this.cancel();
                }
            }
            
            // Disable double-click if form is shown.
            this.ondblclick = null;
        }
        
        cont.prepared = true;
    },
    
    /**
     * Send a query to specified class object.
     * Type of query is determined by 'data' parameter and not interpreted.
     */
    query: function(cls, id, data, callback){
        this.ajaxRequest(
            'query', null, 'GET', this.processorUrl, 
            { 'coord': { 'class': cls, 'id': id, 'query': data } }, 
            function (data) { callback(data.result) }
        );
    },    
    
    /**
     * Send AJAX request for element cont.
     * Call callback function 'callback' on data ready.
     */
    ajaxRequest: function(action, cont, method, url, data, callback) {
        var th = this;
        var req = new JsHttpRequest();
        req.onreadystatechange = function() {
            if (req.readyState == 4) {
                if (cont) th.setLoadingStatus(cont, '', action);
                // if (window.hackerConsole) window.hackerConsole.out('Response: ' + th._dump(req.responseJS), '', 'AJAX');
                var msg = (req.responseText||"").replace(/^\s+$/, '').replace(/ /g, '\xA0'); // &nbsp;
                if (msg || !req.responseJS) {
                    alert(msg);
                    return true;
                }
                if (req.responseJS && req.responseJS.error) {
                    alert(req.responseJS.error);
                    return;
                }
                callback(req.responseJS);
            } else if (!req.readyState) {
                if (cont) th.setLoadingStatus(cont, '', action);
                var msg = req.responseText.replace(/ /g, '\xA0');
                alert(msg);
                return true;
            }
        }
        req.caching = false;
        req.open(method, url, true);
        req.send(data);
        if (cont) this.setLoadingStatus(cont, 'wait', action); // run this AFTER request sending! Damn Safari!
        // if (window.hackerConsole) window.hackerConsole.out('Request: ' + this._dump(data), '', 'AJAX');
    },
    
    /**
     * Return current style property for element e.
     * Parameter 'name' is style name, parameter 'propName' is property name
     * for this style element.
     */
	getCurStyle: function(e, name, propName) {
	    if (!propName) propName = name;
		var value = null;
		var doc = e.ownerDocument;
		if (doc.defaultView) {
			value = doc.defaultView.getComputedStyle(e, "").getPropertyValue(name);
		} else if (e.currentStyle) {
			value = e.currentStyle[propName];
		}
		return value;
	},


    /**
     * Run scripts specified by array of SCRIPT tags.
     */
    runScripts: function(scripts) {
        if (!scripts) return false;
        for (var i = 0; i < scripts.length; i++) {
            var thisScript = scripts[i];            
            var text = null;
            if (thisScript.src) {
                var newScript = document.createElement("script");
                newScript.type = thisScript.type;       
                newScript.language = thisScript.language;
                newScript.src = thisScript.src;             
                document.body.appendChild(newScript);   
            } else if (text = thisScript.text || thisScript.innerHTML) {
                var text = text.replace(/^\s*<!\-\-/, '').replace(/\-\->\s*$/, '');
                eval(text);
            }
        }
    },


    /**
     * Debug dump of the variable.
     */
    _dump: function(d, l) {
        if (l == null) l = 1;
        if (l > 5) {
            alert(0);
            return '* RECURSION *';
        }
        var s = '';
        if (d && d.tagName && d.parentNode) {
            s += d.tagName + "\n";
        } else if (d instanceof Object) {
            s += typeof(d) + " {\n";
            for (var k in d) {
                for (var i=0; i<l; i++) s += "  ";
                s += k+": " + this._dump(d[k],l+1);
            }
            for (var i=0; i<l-1; i++) s += "  ";
            s += "}\n"
        } else {
            s += "" + d + "\n";
        }
        s = s.replace(/\n/g, '<br>');
        s = s.replace(/ /g, '&nbsp;');
        return s;
    },
    
    
    /**
     * Run callback function for each descendant of the node.
     */
    foreachDescendant: function(e, callback) {
        if (e.childNodes) {
            for (var i=0; i<e.childNodes.length; i++) {
                this.foreachDescendant(e.childNodes[i], callback);
            }
        }
        if (e.tagName) callback(e);
    },
    
    /**
     * This function must be overriden in caller code.
     * It is called when new HTML representation is assigned to container.
     */
    onafterchange: function(e) {
    },

    
    /**
     * This function must be overriden in caller code.
     * It is called after new value is submitted, but before new
     * HTML representation resopnse arrives.
     */
    onbeforechange: function(e) {
    },

    
    /**
     * Called when we need to set different mouse cursor while loading.
     */
    setLoadingStatus: function(e, type, action) {
        this.foreachDescendant(e, function(e) {
            if (type == 'wait') {
                e.style.cursor = 'wait';
            } else {
                e.style.cursor = '';
            }
        });
    },
    
    cancelEvent: function(e) {
        if (!e) e = window.event;
    	e.cancelBubble = true;
    	if (e.stopPropagation) e.stopPropagation();
        return e.returnValue = false;    
    }
}

