/*
 * Copyright 1997-2008 Day Management AG
 * Barfuesserplatz 6, 4001 Basel, Switzerland
 * All Rights Reserved.
 *
 * This software is the confidential and proprietary information of
 * Day Management AG, ("Confidential Information"). You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Day.
 */
/*
 * Copyright 1997-2008 Day Management AG
 * Barfuesserplatz 6, 4001 Basel, Switzerland
 * All Rights Reserved.
 *
 * This software is the confidential and proprietary information of
 * Day Management AG, ("Confidential Information"). You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Day.
 */

// create tagging package
CQ.tagging = {};
/*
 * Copyright 1997-2008 Day Management AG
 * Barfuesserplatz 6, 4001 Basel, Switzerland
 *
 * All Rights Reserved.
 * This software is the confidential and proprietary information of
 * Day Management AG, ("Confidential Information"). You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Day.
 */

// calls the TagJsonServlet on the server (meta data for one tag)
CQ.tagging.TAG_JSON_SUFFIX = ".tag.json";

// calls the TagTreeServlet on the server (full tree of namespace)
CQ.tagging.TAG_TREE_JSON_SUFFIX = ".tagtree.json";

// calls the TagListServlet on the server (list of tags below certain tag with all details)
CQ.tagging.TAG_LIST_JSON_SUFFIX = ".tags.json";

// helper for iterating over objects (TODO: move to CQ.Utils)
CQ.forEach = function(obj, fn, scope) {
    for (var o in obj) {
        if (obj.hasOwnProperty(o)) {
            if (fn.call(scope || obj[o], o, obj[o], obj) === false) {
                return o;
            }
        }
    }
};

// private - splits tagID into namespace and local (also works for title paths)
CQ.tagging.parseTag = function(tag) {
    var tagInfo = {
        namespace: null,
        local: tag
    };

    // parse tag pattern: namespace:local
    var colonPos = tag.indexOf(':');
    if (colonPos > 0) {
        // the first colon ":" delimits a namespace
        // don't forget to trim the strings (in case of title paths)
        tagInfo.namespace = tag.substring(0, colonPos).trim();
        tagInfo.local = tag.substring(colonPos + 1).trim();
    }
    
    return tagInfo;
};

// private - same as parseTag(), but only suited for tagIDs and returns namespace = "default" if no namespace is given
CQ.tagging.parseTagID = function(tagID) {
    var tagInfo = CQ.tagging.parseTag(tagID);
    if (tagInfo.namespace === null) {
        tagInfo.namespace = "default";
    }
    return tagInfo;
};

/**
 * Visual representation of a tag.
 */
CQ.tagging.TagLabel = CQ.Ext.extend(CQ.Ext.BoxComponent, {
    constructor: function(config) {
        CQ.Util.applyDefaults(config, {
            "cls": "taglabel",
            
            "showPath": true,
            "text": "",
            "tag": {},
            "type": "set"
        });
        
        CQ.tagging.TagLabel.superclass.constructor.call(this, config);
    },
    
    initComponent: function() {
        // first call initComponent on the superclass:
        CQ.tagging.TagLabel.superclass.initComponent.call(this);
        
        this.addEvents(
            /**
             * @event remove
             * Fires when a the remove button was clicked
             * @param {CQ.tagging.TagLabel} label This tag label component
             */
            'remove'
        );
    },
    
    onRender : function(ct, position){
        if(!this.el) {
            // build html structure that allows background images for
            // the 4 corners + top and bottom side (tl, tc, tr, bl, bc, br)
            
            // NOTE: only a table works here across all browsers (IE). if we would use
            // simple div's internally with floating, we would have to set a fixed
            // width on the whole tag label to let it float around normally with other
            // taglabels. but we want the width to be "auto", depending on the text
            // inside this label
            var htmlPrefix = "<table>" +
                            "<tr>" +
                                "<td class='taglabel-tl'></td>" +
                                "<td class='taglabel-tc' colspan='2'></td>" +
                                "<td class='taglabel-tr'></td>" +
                            "</tr>" +
                            "<tr>" +
                                "<td class='taglabel-ml'></td>" +
                                "<td class='taglabel-mc'>";
                                
            var htmlSuffix =    "</td>" +
                                "<td class='taglabel-tool-cell'>" +
                                    "<div class='taglabel-tool taglabel-tool-remove'></div>" +
                                "</td>" +
                                "<td class='taglabel-mr'></td>" +
                            "</tr>" +
                            "<tr>" +
                                "<td class='taglabel-bl'></td>" +
                                "<td class='taglabel-bc' colspan='2'></td>" +
                                "<td class='taglabel-br'></td>" +
                            "</tr>" +
                         "</table>";
                         
            var parent = "";
            var local = this.text;
            var pos;
            if (local.indexOf('/') > 0) {
                pos = local.lastIndexOf('/');
                parent = local.substring(0, pos+1);
                local = local.substring(pos+1);
            } else if (local.indexOf(':') > 0) {
                pos = local.lastIndexOf(':');
                parent = local.substring(0, pos+1);
                local = local.substring(pos+1);
            }
            
            var nameCls = "tagname";
            if (!this.showPath || !parent || parent === "") {
                nameCls += " no-parentpath";
            }
            
            if (!this.showPath) {
                parent = "";
            }
            
            var html = htmlPrefix + "<div class='taglabel-body'>";
            html += "<div class='parentpath'>" + parent + "</div>";
            html += "<div class='" + nameCls + "'>" + local + "</div>";
            html += "</div>" + htmlSuffix;
            
            // create element
            this.el = ct.createChild({
                "tag": "div",
                "id": this.getId(),
                "html": html
            }, position);
            
            this.setType(this.type);
            
            // provide remove event
            this.removeBtn = this.getEl().child(".taglabel-tool-remove");
            this.removeBtn.on('click', function() {
                this.fireEvent("remove", this);
            }, this);
        }
        
        CQ.tagging.TagLabel.superclass.onRender.call(this, ct, position);
    },
    
    setType: function(type) {
        if (!this.el) {
            // in case the label wasn't render yet, just store the value
            this.type = type;
        } else {
            this.el.removeClass(this.type + "tag");
            this.type = type;
            this.el.addClass(this.type + "tag");
        
            this.createToolTip();
        }
    },
    
    createToolTip: function() {
        if (this.tip) {
            this.tip.destroy();
        }
        
        // tool tip
        var tipCfg = {
            "target": this.getEl().child(".taglabel-body"),
            "title": this.text,
            "dismissDelay": 0, // never dismiss
            "maxWidth": 1000,
            // HACK: IE does not like "auto" as width in quicktips
            "width": "auto" //document.all ? 400 : "auto"
        };
        
        // add namespace to tooltip (and extract if not present yet)
        if (!this.namespace) {
            // TODO: get proper namespace title here (pass in config, fetched from tagTree before)
            this.namespace = CQ.tagging.parseTagID(this.text).namespace;
        }
        tipCfg.html = "<span style='font-style: italic'>" + CQ.I18n.getMessage("Namespace") + ":</span> " + this.namespace;
        
        if (this.tag.tagID) {
            tipCfg.html += "<br><span style='font-style: italic'>" + CQ.I18n.getMessage("Tag ID") + ":</span> " + this.tag.tagID;
        }
        if (this.tag.titlePath) {
            tipCfg.html += "<br><span style='font-style: italic'>" + CQ.I18n.getMessage("Full Title") + ":</span> " + this.tag.titlePath;
        }
        if (this.tag.description) {
            tipCfg.html += "<br><span style='font-style: italic'>" + CQ.I18n.getMessage("Description") + ":</span> " + this.tag.description;
        }
        
        if (this.type == "added") {
            tipCfg.html += "<br><br>" + CQ.I18n.getMessage("You added this existing tag. Submit the form to save.");
        } else if (this.type == "new") {
            tipCfg.html += "<br><br>" + CQ.I18n.getMessage("You added this new tag. It will be created and saved when you submit the form.");
        } else if (this.type == "denied") {
            tipCfg.html += "<br><br>" + CQ.I18n.getMessage("You added this new tag, but are not allowed to create it. Please remove it before submitting the form.");
        }
        
        this.tip = new CQ.Ext.ToolTip(tipCfg);
    },
    
    // @public
    highlight: function() {
        this.getEl().child(".parentpath").highlight("#ffff9c", {attr: "color", duration: 1.5});
        this.getEl().child(".tagname").highlight("#ffff9c", {attr: "color", duration: 1.5});
    }
    
});

// register xtype
CQ.Ext.reg("taglabel", CQ.tagging.TagLabel);

/**
 * <code>CQ.tagging.TagInputField</code> is a form widget for entering tags. It has a popup menu
 * for selecting from existing tags, includes auto-completion and many other features.
 * 
 * @class CQ.tagging.TagField
 * @extends CQ.form.CompositeField
 */
CQ.tagging.TagInputField = CQ.Ext.extend(CQ.form.CompositeField, {
    
    // -----------------------------------------------------------------------< config options >
    
    /**
     * @cfg {Array} namespaces  A list of the tag namespaces that should be displayed and allowed.
     * If empty, all available namespaces will be allowed. Otherwise either an array of Strings
     * (the namespace names) or for more configuration an array of objects as this:
     * <pre><code>
 {
    name: "<namespace-name>",
    maximum: 1, // maximum number of tags allowed from this namespace; if -1 no limit (default)
    displayAs: "tree|cloud", // NOT IMPLEMENTED YET tag view variant ("tree" or "cloud") to display (default cloud)
    allowDisplayChange: true, // NOT IMPLEMENTED YET wether it is possible to change the tag view variant (default true)
 }
       </pre></code>
     */
    namespaces: [],
    
    /**
     * @cfg {Boolean} displayTitles  Whether to display tag titles instead of the pure tag IDs in
     * the input field, autocompletion, tree or cloud view. Default is <code>true</code>.
     */
    displayTitles: true,
    
    /**
     * @cfg {Boolean} showPathInLabels  Whether to display the complete path for namespace and/or container tags
     * (eg. "Newsletter : Company / News") in the labels or just the title (eg. "News" in this example). Default
     * is <code>true</code>.
     */
    showPathInLabels: true,
    
    /**
     * @cfg {String} tagsBasePath  The base path for the tag storage on the server (defaults to /etc/tags).
     * Should not contain a trailing "/".
     */
    tagsBasePath: "/etc/tags",
    
    /**
     * @cfg {Number} popupWidth  The initial width of the popup menu for selecting tags from the existing tag
     * namespaces. Defaults to 500.
     */
    popupWidth: 500,

    /**
     * @cfg {Number} popupHeight  The initial height of the popup menu for selecting tags from the existing tag
     * namespaces. Defaults to 300.
     */
    popupHeight: 300,

    /**
     * @cfg {String} popupResizeHandles  An {@link CQ.Ext.Resizable} handles string for specifying the sides of the popup
     * that should display resize handles. Set to <code>null</code> to disable resizing for the popup menu.
     * Defaults to <code>sw se</code> (bottom left and bottom right only).
     */
    popupResizeHandles: "sw se",
    
    /**
     * @cfg {String} popupAlignment An {@link CQ.Ext.Element.alignTo} anchor position to use for the popup menu relative
     * to the text field
     */
    popupAlignTo: "tl-bl",
    
    // -----------------------------------------------------------------------< private properties >

    // private
    tags: [],
    
    // private
    hiddenTagIDs: [],
    
    /**
     * Full json of all namespaces (hash of namespaces). Will include all the data
     * from the according <code>namespaces</code> config option (if present) as "option" member.
     * @private
     */
    tagTree: null,
	
	// private
	tagTreeLoaded: false,
	
	// private
	allNamespacesAllowed: false,
	
	// private
	allowedNamespaces: {},
    
    // private - CQ.Ext.Panel, the big, dummy input box that contains labels and the real text field
    dummyInput: null,
    
    // private - CQ.Ext.form.ComboBox, the main input field 
    textField: null,
    
    // private - CQ.Ext.menu.Menu, the popup menu for selecting from existing tags
    popupMenu: null,
    
    // private - CQ.Ext.TabPanel, the main tab widget in the popup menu (one tab per tag namespace)
    namespacesTabPanel: null,
    
    // private - remembers the actual menu visibility state before the trigger button was clicked
    menuIsVisible: false,
    
    /**
     * List of hidden form fields that get dynamically updated
     * when the textField changes. Used to hold the array of tags
     * when the form is submitted.
     * 
     * @private
     * @type Object (Array<CQ.Ext.form.Hidden>)
     */
    hiddenFields: [],
    
    // private - CQ.Ext.data.MemoryProxy() holding the auto-completion data
    autoCompletionProxy: null,
    
    // private - default config for entries in namespaces[] config
    namespacesDefaultConfig: {
        "maximum": -1, // any number allowed
        "displayAs": "cloud",
        "allowDisplayChange": true
    },
    
    // -----------------------------------------------------------------------< constructor >
    
    /**
     * Creates a new <code>CQ.tagging.TagInputField</code>.
     * Example:
     * <pre><code>
var myComp = new CQ.tagging.TagInputField({
    "id": "tagInputField",
    "fieldLabel": "Tags / Keywords",
    "name": "./cq:tags",
    
    "displayTitles": false,
    "namespaces": []
});
       </pre></code>
     * @constructor
     * @param {Object} config The config object
     */
    constructor: function(config) {
        CQ.Util.applyDefaults(config, {
            // TagInputField config
            "tagsBasePath": "/etc/tags",
            "displayTitles": true,
            "showPathInLabels": true,
            "namespaces": [],
            "popupWidth": 500,
            "popupHeight": 300,
            "popupResizeHandles": "sw se", // bottom left and bottom right resize handles only
            "popupAlignTo": "tl-bl",
            
            // inherited config
            "border": false,
            "layout": "fit"
        });
        
        // for use of "this" in closures
        var tagInputField = this;
        
        this.autoCompletionProxy = new CQ.Ext.data.MemoryProxy(null);
        
        this.textField = new CQ.Ext.form.ComboBox({
            "wrapCls": "floating", // special config, see on render handler below
            "cls": "invisible-input",
            "hideLabel": true,
            "hideTrigger": true,
            "resizable": true,
            "autoCreate": {tag: "input", type: "text", size: "18", autocomplete: "off"},
            "name": CQ.Sling.IGNORE_PARAM, // let sling ignore this field
            "displayField": config.displayTitles ? "titlePath" : "tagID",
            "minChars": 2,
            "mode": "local", // we pre-load the store manually (see initComponent)
            "typeAhead": false, // type ahead in combo only works for single value completion
            "title": CQ.I18n.getMessage("Matching tags"),
            "listWidth": 500,
            
            "store": new CQ.Ext.data.Store({
                "proxy": this.autoCompletionProxy,
                // sample data structure (built inside initComponent)
                // { tags: [ { tagID: "foo", title: "Foo Bar", path: "/etc/tags/default/foo", ...}, ... ] }
                "reader": new CQ.Ext.data.JsonReader({
                    "root": "tags",
                    "fields": [ "tagID", "titlePath" ]
                })
            })
        });
        
        // add the special config "wrapCls" to wrapper div
        this.textField.on('render', function(textField) {
            textField.wrap.addClass(textField.wrapCls);
        });
        
        this.inputDummy = new CQ.Ext.Panel({
            "cls": "dummy-input",
            "border": false
        });
        
        config.items = [
            this.inputDummy
        ];
        
        this.namespacesTabPanel = new CQ.Ext.TabPanel({
            "enableTabScroll": true,
            "width": config.popupWidth,
            "height": config.popupHeight,
            "border": false,
            "deferredRender": false // needed for the tree views inside the non-visible tabs on start to be displayed
        });

        CQ.tagging.TagInputField.superclass.constructor.call(this, config);
    },
    
    // -----------------------------------------------------------------------< component init & rendering >
    
    /**
     * Initializes the component.
     * @see CQ.Ext.Component#initComponent
     */
    initComponent: function() {
        // first call initComponent on the superclass:
        CQ.tagging.TagInputField.superclass.initComponent.call(this);

        this.addEvents(
            /**
             * @event addtag
             * Fires when a tag was added to the value of this field (not fired upon setValue)
             * @param {CQ.tagging.TagInputField} field This tag input field
             * @param {Object} tag The newly added tag (with tagID, title, name, path, description, etc. members)
             */
            'addtag',
            /**
             * @event removetag
             * Fires when a tag was removed from the value of this field
             * @param {CQ.tagging.TagInputField} field This tag input field
             * @param {Object} tag The newly added tag (with tagID, title, name, path, description, etc. members)
             */
            'removetag'
        );
        
        // for use of "this" in closures
        var tagInputField = this;

		this.initAllowedNamespaces();
		        
        this.inputDummy.add(this.textField);
        
        this.menuIsVisible = false;
        // store and use the actual visible state in our trigger click handler below
        var storeMenuVisibilityHandler = function() {
            tagInputField.menuIsVisible = tagInputField.popupMenu.isVisible();
        };
        this.textField.on('keydown', storeMenuVisibilityHandler);
        
        // resize handling copied from TriggerField
        this.inputDummy.deferHeight = true;
        this.inputDummy.getResizeEl = function(){
            return this.wrap;
        };
        this.inputDummy.getPositionEl = function(){
            return this.wrap;
        };
        
        this.inputDummy.on("render", function(comp) {
            // wrap + trigger button
            comp.wrap = comp.el.wrap({cls: "x-form-field-wrap"});
            comp.trigger = comp.wrap.createChild({tag: "img", cls: "arrow-trigger", src: CQ.Ext.BLANK_IMAGE_URL});
            if(!comp.width){
                comp.wrap.setWidth(comp.el.getWidth() + comp.trigger.getWidth());
            }

            comp.trigger.on("click", function() {
                if (tagInputField.disabled) return;
            
				if (!tagInputField.tagTreeLoaded) {
			        tagInputField.loadTagTree();
				}
				
                if (tagInputField.menuIsVisible) {
                    tagInputField.popupMenu.hide();
                } else {
                    tagInputField.popupMenu.show(comp.el, tagInputField.popupAlignTo);
                }
            });

            comp.trigger.on("mousedown", storeMenuVisibilityHandler);
            
            // pass clicks on to real input field
            comp.getEl().on("click", function() {
                this.textField.focus();
            }, this);
        }, this);
        
        this.inputDummy.onResize = function(w, h) {
            // change width and height before passing to Panel.onResize
            
            // panel is smaller because of the trigger button on the right
            if (typeof w == 'number') {
                w = w - this.trigger.getWidth();
            }
            // height should depend on inner contents
            h = "auto";
            
            CQ.Ext.Panel.prototype.onResize.call(this, w, h);
            
            // now set the width of the wrap depending on what Panel set + the trigger width
            this.wrap.setWidth(this.el.getWidth() + this.trigger.getWidth());
        };
        
        // select existing tag from autocompletion (override onSelect method of combo box)
        this.textField.onSelect = function(record, index) {
            if (this.fireEvent('beforeselect', this, record, index) !== false) {
                var tag = tagInputField.getTagDefinition(record.data.tagID);
                // new add the tag object belonging to the selected tagID
                tagInputField.comingFromTextField = true;
                if (tagInputField.addTag(tag, true)) {
                    this.setValue("");
                }
                
                this.collapse();
                this.fireEvent('select', this, record, index);
            }
        };
		
        // mark the focus on the dummy input field
        this.textField.on("focus", function(textField) {
            this.inputDummy.addClass("dummy-input-focus");
            this.inputDummy.trigger.addClass("trigger-focus");
			
			if (!this.tagTreeLoaded) {
		        this.loadTagTree();
			}
        }, this);
        this.textField.on("blur", function(textField) {
            this.inputDummy.removeClass("dummy-input-focus");
            this.inputDummy.trigger.removeClass("trigger-focus");
        }, this);
        
        this.textField.enableKeyEvents = true;
        this.textField.on("keydown", function(textField, e) {
            if (e.getKey() == e.ENTER) {
                // try to add the textfield's value as tag on ENTER
                
                // differentiate between enters with a autocomplete popup open or without
                if (!this.textField.isExpanded()) {
                    if (!this.readTextField()) {
                        this.textField.focus();
                    }
                }
                
            } else if (e.getKey() == e.BACKSPACE) {
                var text = this.textField.getValue();
                
                // NOTE: disabling backspace because it is annoying when using backspace
                // to delete chars in the input field and suddenly removing the previous
                // label - for which there is no undo. Proper solution should "select"
                // the label first and a second backspace should delete it.
                //// delete the previous tag label on backspace
                //if (!text && this.tags.length) {
                //    var tagObj = this.tags[this.tags.length - 1];
                //    this.removeTag(tagObj.tag);
                //}
                // ensure the autocompletion popup is closed when the input field becomes empty
                if (text && text.length == 1) {
                    this.textField.collapse();
                }
            }
        }, this);
        
        // autocompletion: always search for any match + be case insensitive
        this.textField.store.filter = function(property, value /*, anyMatch, caseSensitive*/) {
            var fn = this.createFilterFn(property, value, true, false);
            return fn ? this.filterBy(fn) : this.clearFilter();
        };
        
        // build menu, wrap the namespaces TabPanel inside a menu item adapter
        this.popupMenu = new CQ.Ext.menu.Menu({ cls: "x-tagging-menu" });
        this.popupMenu.add(new CQ.Ext.menu.Adapter(this.namespacesTabPanel, { hideOnClick: false }));
        
        if (this.popupResizeHandles) {
            // make the menu resizable
            var menuResizer = new CQ.Ext.Resizable(this.popupMenu.getEl(), {
                "pinned": true,
                "handles": this.popupResizeHandles
            });
            menuResizer.on('resize', function(r, width, height) {
                // Note: 4px border for tabpanel + menuadapter depend on the styles used (and configs)
                this.namespacesTabPanel.setSize(width - 4, height - 4);
            }, this);
        }
    },
    
    /**
     * Handler for the rendering event. Used to subscribe to events of
     * the parentdialog, ie. to create tags before the submit.
     * @private
     */
    onRender: function(e) {
        CQ.tagging.TagInputField.superclass.onRender.call(this, e);
        
        // register handler for creating tags
        var parentDialog = this.findParentByType("dialog");
        if (parentDialog) {
            parentDialog.on("beforesubmit", this.prepareSubmit, this);
        }
    },
	
    // -----------------------------------------------------------------------< public methods >
    
    /**
     * Adds the given tag object to the value of this field (which is a list of tags).
     * This must be an existing tag.
     * @param {String/Object} tag  a tag object (as returned from <code>getTagDefinition()</code>
     *                             or a plain entered tag string
     * @param {Boolean} doFx  whether to animate an existing tag when it is tried to re-add (optional)
     * @param {Boolean} syncCheck  if the can-create-tag check on the server should be done synchronously (optional)
     * @return {Boolean} true if the tag could be added, false if it was not allowed
     * @public
     */    
    addTag: function(tag, doFx, syncCheck) {
        if (!tag) return false;
        
        if (this.hasTag(tag)) {
            if (doFx) {
                this.getTag(tag).label.highlight();
            }
            return false;
        }
        
        var type;
        if (typeof tag === "string") {
            // newly entered tag, plain string
            if (!this.checkMaximum(tag)) {
                return false;
            }
            type = "new";
            
            var tagObj = this.internalAddTag(tag);
            
            // user might not be allowed to create this new tag
            this.runCanCreateTagCheck(tagObj, syncCheck);

        } else {
            // existing tag
            if (!this.checkMaximum(tag)) {
                return false;
            }
            type = "added";
            
            this.internalAddTag(tag);
        }
        
        this.inputDummy.doLayout();
        
        this.fireEvent('addtag', this, tag);
        
        return true;
    },

    /**
     * Removes the given tag from the value of this field (which is a list of tags).
     * @param {String/Object} tag  a tag object (as returned from <code>getTagDefinition()</code>
     *                             or a plain entered tag string
     * @public
     */    
    removeTag: function(tag) {
        if (!tag) return;
        
        // don't run update and events if the tag is already gone
        var tagObj = this.getTag(tag);
        if (tagObj !== null) {
            this.internalRemoveTag(tagObj);
            this.inputDummy.doLayout();
            
            this.fireEvent('removetag', this, tag);
        }
    },

    /**
     * Overridden setter for the value that expects an Array of String,
     * ie. a list of tagIDs.
     * @param {Array} value An Array of String, one for each tag
     * @public
     */
    setValue: function(valueArray) {
        // value must be an array<string>            
        this.value = valueArray;
		
		this.clear();
				
        for (var i=0, iEnd = valueArray.length; i < iEnd; i++) {
            var tagID = valueArray[i];
            var tagInfo = CQ.tagging.parseTagID(tagID);
			
			// load single tag data from server
			var tag = this.loadJson(this.tagsBasePath + "/" + tagInfo.namespace + "/" + tagInfo.local + CQ.tagging.TAG_JSON_SUFFIX);

            if (tag === null || !this.isAllowedNamespace(tagInfo.namespace)) {
                // not allowed namespace, keep pure tagID in the background
                this.hiddenTagIDs.push(valueArray[i]);
            } else {
                // allowed => display
                this.internalAddTag(tag, "set");
            }
        }
		
        this.inputDummy.doLayout();
    },

    /**
     * Overridden getter for the value that returns an Array of String,
     * ie. a list of tagIDs.
     * @return {Array} value An Array of String, one for each tag
     * @public
     */
    getValue: function() {
        this.value = [];
        
        // return the tag ids only of valid, existing tags
        for (var i=0; i < this.tags.length; i++) {
            var tag = this.tags[i].tag;
            if (tag.tagID) {
                this.value.push(tag.tagID);
            }
        }
        
        this.value = this.value.concat(this.hiddenTagIDs);
       
        return this.value;
    },
    
    // -----------------------------------------------------------------------< private >
    
    // private
    hasTag: function(tag) {
        return this.getTag(tag) !== null;
    },
    
    // private
    getTag: function(tag) {
        for (var i=0; i < this.tags.length; i++) {
            if (this.tags[i].equals(tag)) {
                return this.tags[i];
            }
        }
        return null;
    },
    
    // private
    internalAddTag: function(tag, type) {
        type = type || (typeof tag === "string" ? "new" : "added");
        
        // create ui label
        var tagLabel = new CQ.tagging.TagLabel({
            "text": typeof tag === "string" ? tag : (this.displayTitles && tag.titlePath ? tag.titlePath : tag.tagID),
            "tag": tag,
            "namespace": null,
            "type": type,
            "showPath": this.showPathInLabels
        });
        
        tagLabel.on("remove", function() {
            this.removeTag(tag);
            this.textField.focus();
        }, this);
        
        // insert before the last element, the real input field
        this.inputDummy.insert(this.inputDummy.items.getCount()-1, tagLabel);
        
        var tagObj = {
            "label": tagLabel,
            "tag": tag,
            "type": type,
			equals: function(otherTag) {
				if (typeof this.tag === "string") {
					return this.tag == otherTag;
				} else {
					return this.tag.tagID == otherTag.tagID;
				}
			}
        };
        this.tags.push(tagObj);
        
        return tagObj;
    },
    
    // private
    internalRemoveTag: function(tagObj) {
        this.inputDummy.remove(tagObj.label);
        
        for (var i=0; i < this.tags.length; i++) {
            if (this.tags[i].equals(tagObj.tag)) {
                this.tags.splice(i, 1);
                break;
            }
        }
    },
    
	// private	
	clear: function() {
        for (var i=0; i < this.tags.length; i++) {
			this.inputDummy.remove(this.tags[i].label);
        }

		this.tags = [];
		this.hiddenTagIDs = [];
	},
    
    // private
    toggleTag: function(tag, doFx) {
        if (this.hasTag(tag)) {
            this.removeTag(tag);
        } else {
            this.addTag(tag, doFx);
        }
        
        // TODO: handle this in event handler (find node + change class)
        //node.getUI().removeClass("tagging-node-selected");
        // TODO: handle this in event handler (find node + change class)
        //node.getUI().addClass("tagging-node-selected");
    },
    
    // private
    checkMaximum: function(tag) {
        var namespace;
        var ns;
        if (this.displayTitles) {
			if (tag.tagID) {
				namespace = CQ.tagging.parseTagID(tag.tagID).namespace;
			} else {
				namespace = CQ.tagging.parseTag(tag).namespace;
			}
            ns = this.getNamespaceByTitle(namespace);
        } else {
            namespace = CQ.tagging.parseTagID(tag.tagID || tag).namespace;
            ns = this.tagTree[namespace];
        }
        
        if (ns === null) {
            // if the namespace is not found, we can't change for a maximum and have to accept it
            return true;
        }
        
        // check maximum setting
		var max = this.getNamespaceConfig(ns.name).maximum;
        
        // infinity
        if (max == -1) {
            return true;
        }
        
        // count both existing and to-be-created tags
        var count;
        if (this.displayTitles) {
            count = this.countTagsOfNamespaceTitle(ns.title);
        } else {
            count = this.countTagsOfNamespace(ns.name);
        }
        
        if (count >= max) {
            // Note: this method is asynchronous (hence using the callback fn)
            CQ.Ext.Msg.show({
                cls: "x-above-menu",
                title: CQ.I18n.getMessage("Cannot add tag"),
                msg: CQ.I18n.getMessage("You can have only a maximum of '{0}' tags for the namespace '{1}'.", [max, namespace]),
                buttons: CQ.Ext.Msg.OK,
                
                fn: this.focusBackAfterMsgBox,
                scope: this
            });
            return false;
        }
        return true;
    },
    
    // private
    focusBackAfterMsgBox: function() {
        if (this.comingFromTextField) {
            this.textField.focus();
        } else {
            this.popupMenu.show(this.inputDummy.getEl(), this.popupAlignTo);
        }
    },
    
    // private - counts the number of selected tags that are of namespace ns
    countTagsOfNamespace: function(nsName) {
        var count = 0;
        for (var i=0; i < this.tags.length; i++) {
            var tag = this.tags[i].tag;
            // looking at tagID here
			var namespaceName = CQ.tagging.parseTagID(tag.tagID || tag).namespace;
            if (namespaceName == nsName) {
                count++;
            }
        }
        return count;
    },
    
    // private - counts the number of selected tags that are of namespace title nsTitle (pass null for default ns)
    countTagsOfNamespaceTitle: function(nsTitle) {
        var count = 0;
        for (var i=0; i < this.tags.length; i++) {
            var tag = this.tags[i].tag;
            // we look at titles here
            var namespaceTitle = CQ.tagging.parseTag(tag.titlePath || tag).namespace;
            // default namespace would be null here
            if (namespaceTitle == nsTitle) {
                count++;
            }
        }
        return count;
    },
    
    // -----------------------------------------------------------------------< submit + creating tags >

    // private
    readTextField: function(syncCheck) {
        var text = this.textField.getRawValue().trim();
        if (text === "") {
            return true;
        }
        
        try {
            var tag;
            if (this.displayTitles) {
                tag = this.getTagDefinitionByTitlePath(text);
            } else {
                tag = this.getTagDefinition(text);
            }
        } catch (e) {
            CQ.Ext.Msg.alert(CQ.I18n.getMessage("Error"), typeof e === "string" ? e : e.message, function() {
                this.textField.focus();
            }, this);
            return false;
        }
        
        this.comingFromTextField = true;
        this.textField.setValue("");
        return this.addTag(tag || text, true, syncCheck);
    },
    
    // private
    prepareSubmit: function() {
        // check the text field for contents
        if (!this.readTextField(true)) {
            return false;
        }
        
        var tagIDs = [];
        var newTags = [];
        var denied = [];
        
        // go over all tags and categorize them
        for (var i=0; i < this.tags.length; i++) {
            var tagObj = this.tags[i];
            if (tagObj.type == "set" || tagObj.type == "added") {
                tagIDs.push(tagObj.tag.tagID);
            } else if (tagObj.type == "new") {
                newTags.push(tagObj.tag);
            } else if (tagObj.type == "denied") {
                denied.push(tagObj.tag);
            }
        }
        
        // first check if there are denied new tags and stop the submit in this case
        if (denied.length > 0) {
            CQ.Ext.Msg.alert(
                CQ.I18n.getMessage("Error"),
                CQ.I18n.getMessage("You are not allowed to create these new tag(s):<br><br>{0}<br><br>Please remove before submitting the form.", [ denied.join("<br>") ])
            );
            return false;
        }
        
        // (try to) create the new tags on the server
        if (newTags.length > 0) {
            var result = this.createTags(newTags);
            if (result.failed.length > 0) {
                CQ.Ext.Msg.alert(
                    CQ.I18n.getMessage("Error from Server"),
                    CQ.I18n.getMessage("Could not create tag(s):<br><br>{0}<br><br>The form was not saved.", [ result.failed.join("<br>") ])
                );
                // stop form submit, don't set the tags on the current content
                return false;
            }
            // add the newly created tags (tagIDs) to the tags array
            tagIDs = tagIDs.concat(result.created);
            
            // please reload the tag tree next time
            this.tagTreeLoaded = false;
        }
        
        // don't forget to pass through the hidden tagIDs
        tagIDs = tagIDs.concat(this.hiddenTagIDs);
        
        // prepare the form dom for the submit (with hidden fields)
        this.updateHiddenFields(tagIDs);
        
        return true;
    },
    
    // private
    createTags: function(tags) {
        // create new tags
        var result = {
            created: [],
            failed: []
        };
        
        CQ.forEach(tags, function(t, tag) {
            // create tag on server
            var response = CQ.HTTP.post(
                "/bin/tagcommand",
                undefined, // synchronous execution
                {
                    "cmd": this.displayTitles ? "createTagByTitle" : "createTag",
                    "tag": tag,
                    "_charset_": "utf-8"
                }
            );

            // collect all tags that could not be created.
            if (!CQ.HTTP.isOk(response)) {
                result.failed.push("'" + tag + "': " + response.headers[CQ.HTTP.HEADER_MESSAGE]);
            } else {
                // the tag ID is stored in the Path parameter of the html reponse
                var tagID = response.headers[CQ.HTTP.HEADER_PATH];
                result.created.push(tagID);
            }
        }, this);
        
        return result;
    },

    // private
    runCanCreateTagCheck: function(tagObj, syncCheck) {
        function handleResponse(options, success, xhr) {
            if (!success) {
                tagObj.type = "denied";
                tagObj.label.setType("denied");
            }
        }
        
        // check on server
        var response = CQ.HTTP.post(
            "/bin/tagcommand",
            // decide between sync and async response handling
            syncCheck ? undefined : handleResponse,
            {
                "cmd": this.displayTitles ? "canCreateTagByTitle" : "canCreateTag",
                "tag": tagObj.tag,
                "_charset_": "utf-8"
            }
        );
        
        if (syncCheck) {
            handleResponse(null, CQ.utils.HTTP.isOk(response), null);
        }
    },
    
    // -----------------------------------------------------------------------< tag store lookup >
    
    /**
     * Retrieves a full tag object (with tagID, name, title, path, description, etc.) by
     * the given tagID. Note: this will only work for the namespaces given in the
     * <code>namespaces</code> config, because otherwise no appropriate tag data is loaded.
     * @param {String} tagID  a tagID string (eg. "newsletter:company")
     * @private
     */    
    getTagDefinition: function(tagID) {
        try {
            var tagInfo = CQ.tagging.parseTagID(tagID);
            
            // get tag from tag tree
            var pathElements = tagInfo.local.split("/");
            // get namespace
            var tag = this.tagTree[tagInfo.namespace];
            if (!tag) {
                throw CQ.I18n.getMessage("Namespace '{0}' does not exist.", [ tagInfo.namespace ]);
            }
            // traverse tags
            for (var i = 0, iEnd = pathElements.length; i < iEnd; i++) {
                tag = tag.tags[pathElements[i]];
                
                if (!tag) {
                    // not found
                    return null;
                }
            }
        
            return tag;
        } catch(e) {
        }
        return null;
    },
    
    // private
    getNamespaceByTitle: function(nsTitle) {
        // scan namespace titles for a match
        for (var n in this.tagTree) {
            if (!this.tagTree.hasOwnProperty(n)) continue;
            
            if (nsTitle == this.tagTree[n].title) {
                return this.tagTree[n];
            }
        }
        return null;
    },
    
    // private
    // returns a tag object if found or null if tag has to be created
    getTagDefinitionByTitlePath: function(titlePath) {
        var tagInfo = CQ.tagging.parseTag(titlePath);
        
        var ns;
        if (tagInfo.namespace === null) {
            // special case: default namespace
            ns = this.tagTree["default"];
        } else {
            // try to find existing namespace with this title
            ns = this.getNamespaceByTitle(tagInfo.namespace);
        }
        
        if (ns === null) {
            throw CQ.I18n.getMessage("Namespace with title '{0}' does not exist.", [ tagInfo.namespace ]);
        }
        
        // split local tag path
        var titles = tagInfo.local.split("/");
        var tag = ns;
        
        // walk over path elements (titles) and look for existing tags
        for (var i=0; i < titles.length; i++) {
            var title = titles[i].trim(); // trim titles!
            
            var found = false;
            for (var t in tag.tags) {
                if (!tag.tags.hasOwnProperty(t)) continue;
                
                if (title == tag.tags[t].title) {
                    tag = tag.tags[t];
                    found = true;
                    break;
                }
            }
            if (!found) {
                return null;
            }
        }
        
        return tag;
    },
    
    // -----------------------------------------------------------------------< namespace config >

	// private    
	initAllowedNamespaces: function() {
		this.allowedNamespaces = {};
		
		if (this.namespaces.length == 0) {
			this.allNamespacesAllowed = true;
			return;
		}
            
        for (var i = 0, iEnd = this.namespaces.length; i < iEnd; i++) {
			var ns = this.namespaces[i];
			// can be just a string with the namespace name or a full config object
			if (typeof ns == "string") {
				ns = { name: ns };
			}
			
			CQ.Util.applyDefaults(ns, this.namespacesDefaultConfig);
			this.allowedNamespaces[ns.name] = ns;
		}
	},
	
    // private
    isAllowedNamespace: function(ns) {
        return this.allNamespacesAllowed || this.allowedNamespaces[ns];
    },
    
    // private
    getNamespaceConfig: function(ns) {
        if (this.allNamespacesAllowed) {
            return this.namespacesDefaultConfig;
        } else {
			return this.allowedNamespaces[ns];
        }
    },
    
    // -----------------------------------------------------------------------< loading json >
    
    /**
     * Helper function that loads a json from the given URL. If there is no
     * response or any other error, it will be logged and <code>null</code> returned.
     * @private
     */
    loadJson: function(url) {
        try {
            if (url) {
				// escape paths (eg. when + is present)
				url = CQ.Util.escapePath(url);
                var response = CQ.HTTP.get(url);
                if (response) {
                    return CQ.Util.eval(response);
                } else {
                    CQ.Log.debug("CQ.tagging.TagInputField#loadTags: no response for {0}, empty data}", url);
                    return null;
                }
            }
        } catch (e) {
            CQ.Log.warn("CQ.tagging.TagInputField#loadTags: {0}", e.message);
            return null;
        }
        
    },
    
    // private
    loadTagTree: function() {
        // load tag store data from server
        if (this.namespaces.length) {
            // load the data only for the configured namespaces
            this.tagTree = {};
                
            for (var i = 0, iEnd = this.namespaces.length; i < iEnd; i++) {
                var ns = this.namespaces[i];
                // can be just a string with the namespace name or a full config object
                if (typeof ns == "string") {
                    ns = { name: ns };
                }
				
                // get the tag tree for the specific namespace only (ns.name)
                var nsTagTree = this.loadJson(this.tagsBasePath + '/' + ns.name + CQ.tagging.TAG_TREE_JSON_SUFFIX);
                
                if (nsTagTree && nsTagTree.namespaces && nsTagTree.namespaces[ns.name]) {
                    this.tagTree[ns.name] = nsTagTree.namespaces[ns.name];
                }
            }
        } else {
            // load everything
            this.tagTree = this.loadJson(this.tagsBasePath + CQ.tagging.TAG_TREE_JSON_SUFFIX).namespaces;
        }
		
		this.tagTreeLoaded = true;
		
        // handle tag store data
        if (this.tagTree !== null) {
            // set up combobox autocompletion store
            this.setupAutoCompletion();
            
            // create tab panels incl. TagTree and TagCloud widgets, all based on tagTree
            this.setupPopupMenu();
        }
        
        // load autocompletion data
        this.textField.store.load();
    },
    
    // -----------------------------------------------------------------------< gui >
    
    // private
    setupAutoCompletion: function() {
        // join all tags of all namespaces (from tree structure) into a flat array
        var allTags = [];
        for (var ns in this.tagTree) {
            if (!this.tagTree.hasOwnProperty(ns)) continue;
            this.collectTagsInFlatList(this.tagTree[ns], allTags);
        }
        // we can set memoryproxy.data later (it's only read deferred on Store.load,
        // which is only called inside ComboBox.doQuery)
        this.autoCompletionProxy.data = { tags : allTags };
    },
    
    // private
    collectTagsInFlatList: function(tag, allTags) {
        for (var t in tag.tags) {
            if (!tag.tags.hasOwnProperty(t)) continue;
            
            var kid = tag.tags[t];
            
            allTags.push(kid);
            this.collectTagsInFlatList(kid, allTags);
        }
    },
    
    // private GUI
    setupPopupMenu: function() {
        // since the tree is used to add/remove tags by clicks, we disable GUI selections altogher
        var noSelectionsHandler = function(selectionModel, oldSel, newSel) {
            return false;
        };
        
        for (var n in this.tagTree) {
            if (!this.tagTree.hasOwnProperty(n)) continue;
            var ns = this.tagTree[n];
            
            var rootNode = new CQ.Ext.tree.TreeNode({
                "name": ns.name,
                "text": ns.title ? ns.title : ns.name
            });
            
            this.addTagChildNodes(rootNode, ns.tags);
            
            var treePanel = new CQ.Ext.tree.TreePanel({
                "root": rootNode,
                "rootVisible": false,
                
                "autoScroll": true,
                "containerScroll": true
            });
            
            treePanel.on('click', this.onTagNodeClicked, this);
            treePanel.getSelectionModel().on('beforeselect', noSelectionsHandler);
            
            this.namespacesTabPanel.add({
                "title": this.displayTitles && ns.title ? ns.title : ns.name,
                "tabTip": CQ.I18n.getMessage("Namespace", [], "Tag Namespace") + ": " + (!this.displayTitles && ns.title ? ns.title : ns.name),
                
                // wrap treepanel in a simple panel for fit layout + scrolling
                "xtype": "panel",
                "layout": "fit",
                "border": false,
                "items": treePanel
                
            });
        }
        this.namespacesTabPanel.setActiveTab(0);
    },
    
    // private GUI
    addTagChildNodes: function(parentNode, tags) {
        for (var t in tags) {
            if (!tags.hasOwnProperty(t)) continue;
            var tag = tags[t];
            
            var qTipCfg = {
                // tool tip display the opposite of the text (below)
                "title": !this.displayTitles && tag.title ? tag.title : tag.tagID,
                // HACK: IE does not like "auto" as width in quicktips
                "width": document.all ? 200 : "auto"
            };
            
            // a tag.description of null would be displayed as null in IE
            if (tag.description) {
                qTipCfg.text = tag.description;
            }
            
            var node = new CQ.Ext.tree.TreeNode({
                "id": tag.name,
                "name": tag.name,
                "text": this.displayTitles && tag.title ? tag.title : tag.name,
                "qtipCfg": qTipCfg
            });
            // store reference to tag object for later use
            node.tag = tag;
            
            parentNode.appendChild(node);
            
            this.addTagChildNodes(node, tag.tags);
        }
    },
    
    // private
    onTagNodeClicked: function(node, event) {
        this.comingFromTextField = false;
        this.toggleTag(node.tag, true);
        // reposition popup in case the inputDummy has resized
        this.popupMenu.show(this.inputDummy.getEl(), this.popupAlignTo);
    },
    
    /**
     * Updates the hidden form fields according to the values passed.
     * @param {Object} values an Array of Strings with the tag ids.
     * @private
     */
    updateHiddenFields: function(values) {
        for (var i=0; i < this.hiddenFields.length; i++) {
            this.remove(this.hiddenFields[i]);
        }
        
        this.hiddenFields = [];
        
        // ensure the field is deleted before the values are set
        // (in case no values are present => empty text field)
        var deleteHiddenField = new CQ.Ext.form.Hidden({
            name: this.getName() + CQ.Sling.DELETE_SUFFIX
        });
        this.add(deleteHiddenField);
        this.hiddenFields.push(deleteHiddenField);

        // ensure multivalue property (when only one value is set)
        var typeHintHiddenField = new CQ.Ext.form.Hidden({
            name: this.getName() + "@TypeHint",
            value: "String[]"
        });
        this.add(typeHintHiddenField);
        this.hiddenFields.push(typeHintHiddenField);
        
        for (i=0; i < values.length; i++) {
            var hiddenField = new CQ.Ext.form.Hidden({
                name: this.getName(), // all hidden fields have the name of this field
                value: values[i]
            });
            this.add(hiddenField);
            this.hiddenFields.push(hiddenField);
        }
        this.doLayout();
    }
    
//    ,
//    
//  TODO: form validation does not work because validate() is not called on this widget for some reason
//    
//    // -------------------------------------------------< custom validation methods >
//    
//    /**
//     * Returns whether or not the field value is currently valid
//     * @param {Boolean} preventMark True to disable marking the field invalid
//     * @return {Boolean} True if the value is valid, else false
//     */
//    isValid : function(preventMark){
//        //console.log("isValid called");
//        if(this.disabled){
//            return true;
//        }
//        var restore = this.preventMark;
//        this.preventMark = preventMark === true;
//        
//        var valid = true;
//        // go over all tags and categorize them
//        for (var i=0; i < this.tags.length; i++) {
//            var tagObj = this.tags[i];
//            if (tagObj.type == "denied") {
//                valid = false;
//                break;
//            }
//        }
//        this.preventMark = restore;
//        //console.log("returns " + valid);
//        return valid;
//    },
//
//    /**
//     * Validates the field value
//     * @return {Boolean} True if the value is valid, else false
//     */
//    validate : function(){
//        //console.log("validate called");
//        if(this.disabled || this.isValid()){
//            this.clearInvalid();
//            return true;
//        }
//        return false;
//    },
//
//    // -------------------------------------------------< validation copied from Field >
//    
//    /**
//     * Mark this field as invalid, using {@link #msgTarget} to determine how to display the error and 
//     * applying {@link #invalidClass} to the field's element.
//     * @param {String} msg (optional) The validation message (defaults to {@link #invalidText})
//     */
//    markInvalid : function(msg){
//        if(!this.rendered || this.preventMark){ // not rendered
//            return;
//        }
//        this.el.addClass(this.invalidClass);
//        msg = msg || this.invalidText;
//        switch(this.msgTarget){
//            case 'qtip':
//                this.el.dom.qtip = msg;
//                this.el.dom.qclass = 'x-form-invalid-tip';
//                if(CQ.Ext.QuickTips){ // fix for floating editors interacting with DND
//                    CQ.Ext.QuickTips.enable();
//                }
//                break;
//            case 'title':
//                this.el.dom.title = msg;
//                break;
//            case 'under':
//                if(!this.errorEl){
//                    var elp = this.getErrorCt();
//                    this.errorEl = elp.createChild({cls:'x-form-invalid-msg'});
//                    this.errorEl.setWidth(elp.getWidth(true)-20);
//                }
//                this.errorEl.update(msg);
//                CQ.Ext.form.Field.msgFx[this.msgFx].show(this.errorEl, this);
//                break;
//            case 'side':
//                if(!this.errorIcon){
//                    var elp = this.getErrorCt();
//                    this.errorIcon = elp.createChild({cls:'x-form-invalid-icon'});
//                }
//                this.alignErrorIcon();
//                this.errorIcon.dom.qtip = msg;
//                this.errorIcon.dom.qclass = 'x-form-invalid-tip';
//                this.errorIcon.show();
//                this.on('resize', this.alignErrorIcon, this);
//                break;
//            default:
//                var t = CQ.Ext.getDom(this.msgTarget);
//                t.innerHTML = msg;
//                t.style.display = this.msgDisplay;
//                break;
//        }
//        this.fireEvent('invalid', this, msg);
//    },
//    
//    // private
//    getErrorCt : function(){
//        return this.el.findParent('.x-form-element', 5, true) || // use form element wrap if available
//            this.el.findParent('.x-form-field-wrap', 5, true);   // else direct field wrap
//    },
//
//    // private
//    alignErrorIcon : function(){
//        this.errorIcon.alignTo(this.el, 'tl-tr', [2, 0]);
//    },
//
//    /**
//     * Clear any invalid styles/messages for this field
//     */
//    clearInvalid : function(){
//        if(!this.rendered || this.preventMark){ // not rendered
//            return;
//        }
//        this.el.removeClass(this.invalidClass);
//        switch(this.msgTarget){
//            case 'qtip':
//                this.el.dom.qtip = '';
//                break;
//            case 'title':
//                this.el.dom.title = '';
//                break;
//            case 'under':
//                if(this.errorEl){
//                    CQ.Ext.form.Field.msgFx[this.msgFx].hide(this.errorEl, this);
//                }
//                break;
//            case 'side':
//                if(this.errorIcon){
//                    this.errorIcon.dom.qtip = '';
//                    this.errorIcon.hide();
//                    this.un('resize', this.alignErrorIcon, this);
//                }
//                break;
//            default:
//                var t = CQ.Ext.getDom(this.msgTarget);
//                t.innerHTML = '';
//                t.style.display = 'none';
//                break;
//        }
//        this.fireEvent('valid', this);
//    }
    
});

// register xtype
CQ.Ext.reg("tags", CQ.tagging.TagInputField);

/*
 * Copyright 1997-2008 Day Management AG
 * Barfuesserplatz 6, 4001 Basel, Switzerland
 * All Rights Reserved.
 *
 * This software is the confidential and proprietary information of
 * Day Management AG, ("Confidential Information"). You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Day.
 */

/**
 * The <code>CQ.TagAdmin</code> class provides the admin console for 
 * WCM tag administration.
 * @class
 * @extends CQ.Ext.Viewport
 */
CQ.TagAdmin = CQ.Ext.extend(CQ.Ext.Viewport, {
    /**
     * Configuration of tree loader used for tree of available pages.
     * @cfg {Object} treeLoader Configures the tree loader. Must be a valid {CQ.Ext.tree.TreeLoader} configuration.
     */

    /**
     * @cfg {String} tagsBasePath  The base path for the tag storage on the server (defaults to /etc/tags).
     * Should not contain a trailing "/".
     */
    tagsBasePath: "/etc/tags",
    
    /**
     * Creates a new <code>CQ.TagAdmin</code> instance.
     *
     * Example:
     * <pre><code>
var admin = new CQ.TagAdmin({
    treeLoader: { dataUrl:'content/tree.json' },
    treeRoot: { name:"content" }
});
       </pre></code>
     * @constructor
     * @param {Object} config The config object
     */
    constructor: function(config) {
        this.debug = config.debug;
        var admin = this;

        // init path prefix
        this.pathPrefix = "";
        if (config.pathPrefix != null) {
            this.pathPrefix = config.pathPrefix;
        }
        
        CQ.Util.applyDefaults(config, {
            "tagsBasePath": "/etc/tags"
        });

        // actions
        var actions = [];
        var disabledActions = [];
        var tagActions = [];
        actions.push({
            "id":"cq-tagadmin-grid-refresh",
            "text":CQ.I18n.getMessage("Refresh"),
            "handler":this.reloadAll,
            "scope":this,
            "tooltip": {
                "title":CQ.I18n.getMessage("Refresh Page List"),
                "text":CQ.I18n.getMessage("Refreshs the list of pages"),
                "autoHide":true
            }
        });
        actions.push("-");
        if (config.actions) {
            actions = actions.concat(
                    this.formatActions(config.actions, disabledActions, tagActions));
        }    

        actions.push("->");
        
        // tree config
        var treeLdrCfg = CQ.Util.applyDefaults(config.treeLoader, {
            "baseParams": { "predicate":"nosystem"},
            "requestMethod":"GET",
            "dataUrl":"/bin/tree/ext.json",
            "baseAttrs": {
                "singleClickExpand":false
            },
            "listeners": {
                "beforeload": function(loader, node) {
                    if (node.getPath() == "/") {
                        this.baseParams.path = config.tagsBasePath;
                    } else {
                        this.baseParams.path = admin.convertTreeToTagPath(node.getPath());
                    }
                }
            },
            createNode: function(attr) {
                // only display tag nodes (namespaces and tags)
                if (attr.type && attr.type == "tagging/tag") {
                    // COPY from CQ.Ext.tree.TreeLoader.createNode START
                    // apply baseAttrs, nice idea Corey!
                    if(this.baseAttrs){
                        CQ.Ext.applyIf(attr, this.baseAttrs);
                    }
                    if(this.applyLoader !== false){
                        attr.loader = this;
                    }
                    if(typeof attr.uiProvider == 'string'){
                       attr.uiProvider = this.uiProviders[attr.uiProvider] || eval(attr.uiProvider);
                    }
                    return(attr.leaf ?
                                    new CQ.Ext.tree.TreeNode(attr) :
                                    new CQ.Ext.tree.AsyncTreeNode(attr));
                    // COPY END
                }
                return null;
            }
        });
        this.treeRootCfg = CQ.Util.applyDefaults(config.treeRoot, {
            "name": "ROOT",
            "text": CQ.I18n.getMessage("Tags"),
            "draggable":false,
            "expanded":true
        });

        // grid config
        var cm = new CQ.Ext.grid.ColumnModel([
              new CQ.Ext.grid.RowNumberer(),
              {
                  "header":CQ.I18n.getMessage("Name"),
                  "dataIndex":"name"
              },{
                  "header":CQ.I18n.getMessage("Title"),
                  "dataIndex":"title"
              },{
                  "header":CQ.I18n.getMessage("Description"),
                  "dataIndex":"description"
              },{
                  "header":CQ.I18n.getMessage("TagID"),
                  "dataIndex":"tagID"
              },{
                  "header":CQ.I18n.getMessage("Count"),
                  "dataIndex":"count"
//              },{
//                  "header":CQ.I18n.getMessage("Modifier"),
//                  "dataIndex":"lastModifiedBy"
//              },{
//                  "header":CQ.I18n.getMessage("Modification Date"),
//                  "dataIndex":"lastModified"
              },{
                  "header":CQ.I18n.getMessage("Publication Date"),
                  "dataIndex":"pubDate"
              },{
                  "header":CQ.I18n.getMessage("Publisher"),
                  "dataIndex":"publisher"
              }
          ]);
          cm.defaultSortable = true;

          var sm = new CQ.Ext.grid.RowSelectionModel({
              "singleSelect":true,
              "listeners": {
                  "selectionchange": function(sm) {
                      for (var i=0; i<disabledActions.length; i++) {
                          disabledActions[i].setDisabled(!sm.hasSelection());
                      }
                  }
              }
          });

          var storeConfig = CQ.Util.applyDefaults(config.store, {
              "autoLoad":false,
              "proxy": new CQ.Ext.data.HttpProxy({
                  "url":admin.pathPrefix + this.treeRootCfg.name + CQ.tagging.TAG_LIST_JSON_SUFFIX,
                  "method":"GET"
              }),
              "reader": new CQ.Ext.data.JsonReader({
                  "totalProperty": "results",
                  "root": "tags",
                  "id": "path",
                  "fields": [ "name", "title", "description", "tagID", "count",
                        "lastModified", "lastModifiedBy", "pubDate", "publisher"]
              }),
              "baseParams": {
                  "predicate":"nosystem"
              }
          });
          var store = new CQ.Ext.data.GroupingStore(storeConfig);

        // init component by calling super constructor
        CQ.TagAdmin.superclass.constructor.call(this, {
            "id":"cq-tagadmin",
            "layout":"border",
            "renderTo":"CQ",
            "stateful":true,
            "stateEvents": [ "pathselected" ],
            "items": [
                {
                    "id":"cq-tagadmin-wrapper",
                    "xtype":"panel",
                    "layout":"border",
                    "region":"center",
                    "border":false,
                    "items": [
                        {
                            "id":"cq-header",
                            "xtype":"container",
                            "autoEl":"div",
                            "region":"north",
                            "items": [
                                {
                                    "xtype":"panel",
                                    "border":false,
                                    "layout":"column",
                                    "cls": "cq-header-toolbar",
                                    "items": [
                                        new CQ.Switcher({}),
                                        new CQ.UserInfo({})
                                    ]
                                }
                            ]
                        },{
                            "xtype":"treepanel",
                            "id":"cq-tagadmin-tree",
                            "region":"west",
                            "margins":"5 0 5 5",
                            "width": CQ.themes.TagAdmin.TREE_WIDTH,
                            "autoScroll":true,
                            "containerScroll":true,
                            "collapsible":true,
                            "collapseMode":"mini",
                            "animate":true,
                            "split":true,
                            "enableDD":false,
                            "ddScroll":true,
                            "ddGroup":CQ.TagAdmin.DD_GROUP,
                            "loader": new CQ.Ext.tree.TreeLoader(treeLdrCfg),
                            "root": new CQ.Ext.tree.AsyncTreeNode(this.treeRootCfg),
                            "rootVisible": true,
                            "tbar": [
                                {
                                    "id":"cq-tagadmin-tree-refresh",
                                    "text":CQ.I18n.getMessage("Refresh"),
                                    "handler":function(){
                                        CQ.Ext.getCmp("cq-tagadmin-tree").getRootNode().reload();
                                    },
                                    "tooltip": {
                                        "title":CQ.I18n.getMessage("Refresh Page Tree"),
                                        "text":CQ.I18n.getMessage("Refreshs the page tree"),
                                        "autoHide":true
                                    }
                                }
                            ],
                            "listeners": {
                                "click": function(node, event) {
                                    admin.loadPath(node.getPath());
                                },
                                "movenode": function(tree, node, oldParent, newParent, index) {
                                    if (admin.copyNode) {
                                        admin.performCopy(tree, node, oldParent, newParent, index);
                                    } else {
                                        admin.performMoveOrReorder(tree, node, oldParent, newParent, index);
                                    }
                                },
                                "beforenodedrop": function(dropEvent) {
                                    admin.copyNode = dropEvent.rawEvent.browserEvent.ctrlKey;
                                },
                                "append": function(tree, parent, node, index) {
                                    if (node.getDepth() > 1) {
                                        node.attributes.cls = "tag";
                                    } else {
                                        node.attributes.cls = "namespace";
                                    }
                                    node.ui.render();
                                }
                            }
                        },{
                            "xtype": "grid",
                            "id":"cq-tagadmin-grid",
                            "region":"center",
                            "margins":"5 5 5 0",
                            "loadMask":true,
                            "stripeRows":true,
                            "cm":cm,
                            "sm":sm,
                            "viewConfig": new CQ.Ext.grid.GroupingView({
                                "forceFit":true,
                                "groupTextTpl": '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "'
                                    + CQ.I18n.getMessage("Pages") + '" : "'
                                    + CQ.I18n.getMessage("Page") + '"]})'
                            }),
                            "store":store,
                            "tbar": actions,
                            "listeners": {
                                "rowcontextmenu": function(grid, index, e) {
                                    if (!this.contextMenu && (tagActions.length > 0)) {
                                        this.contextMenu = new CQ.Ext.menu.Menu({
                                            "items": tagActions
                                        });
                                    }
                                    var xy = e.getXY();
                                    this.contextMenu.showAt(xy);
                                    e.stopEvent();
                                },
                                "rowdblclick": function() { CQ.TagAdmin.editTag.call(admin) }
                            }
                        }
                    ]
                }
            ]
        });
        this.loadPath();
    },

    initComponent : function() {
        CQ.TagAdmin.superclass.initComponent.call(this);

        this.addEvents(
            /**
             * @event pathselected
             * Fires when a path in the pages tree was selected.
             * @param {CQ.Ext.TagAdmin} this
             * @param {String} path The selected path
             */
            "pathselected"
        );
    },

    performCopy: function(tree, copy, oldParent, newParent, index) {
        var path = copy.getPath().split("/");
        var name = path[path.length - 1];

        var params = {
            "cmd":"copyPage",
            "srcPath":oldParent.getPath() + "/" + name,
            "destParentPath":newParent.getPath()
        };
        if (newParent.childNodes[index + 1]) {
            params.before = newParent.childNodes[index + 1].getPath();
        }
        var successHandler = function(options, success, response) {
            var loader = tree.getLoader();
            loader.load(oldParent);
            oldParent.expand();
        };
        CQ.HTTP.post("/bin/cmscommand", successHandler, params, this);
    },

    performMoveOrReorder: function(tree, node, oldParent, newParent, index) {
        var successHandler = function(options, success, response) {};
        var nodePath = this.convertTreeToTagPath(node.getPath());
        var oldParentPath = this.convertTreeToTagPath(oldParent.getPath());
        var path = nodePath.split("/");
        var name = path[path.length - 1];
        var params = {
            "cmd":"movePage",
            "srcPath": oldParentPath + "/" + name,
            "destPath": nodePath
        };
        if (newParent.childNodes[index + 1]) {
            params.before = newParent.childNodes[index + 1].getPath();
        }
        CQ.HTTP.post("/bin/cmscommand", successHandler, params, this);
    },

    loadPath: function(path) {
        // if path is not given, simply select the root (eg. on startup)
        this.treePath = path ? path : "/ROOT";
        // workaround for buggy selectPath(): when selecting the root node "/ROOT", no slash must be at the beginning
        CQ.Ext.getCmp("cq-tagadmin-tree").selectPath(this.treePath == "/ROOT" ? "ROOT" : this.treePath, "name");

        // now load the data from the server
        path = this.convertTreeToTagPath(this.treePath);
        
        var store = CQ.Ext.getCmp("cq-tagadmin-grid").getStore();
        store.proxy.conn.url = CQ.Util.escapePath(path) + CQ.tagging.TAG_LIST_JSON_SUFFIX;
        store.reload();

        this.fireEvent("pathselected", this, path);
    },

    getState:function() {
        return { "treePath": this.treePath };
    },

    /**
     * Custom renderer for the title column.
     * @private
     */
    renderPageTitle: function(value, p, record) {
        var url = CQ.Util.externalize(record.id);
        if (url.indexOf(".") == -1) {
            url += ".html";
        }
        return String.format('<a href="{0}" target="_blank">{1}</a>', url, value);
    },

    // the ext tree path (eg. /ROOT/default/bla)
    getCurrentTreePath: function() {
        var tree = CQ.Ext.getCmp("cq-tagadmin-tree");
        var node = tree.getSelectionModel().getSelectedNode();
        if (node != null) {
            return node.getPath();
        }
    },
    
    convertTreeToTagPath: function(treePath) {
        // ROOT is the placeholder id/name of the root node
        // convert eg. /ROOT/default/bla to /etc/tags/default/bla
        return treePath.replace("/ROOT", this.tagsBasePath)
    },

    getSelectedTags: function() {
        var grid = CQ.Ext.getCmp("cq-tagadmin-grid");
        return grid.getSelectionModel().getSelections();
    },

    reloadAll: function() {
        CQ.Ext.getCmp("cq-tagadmin-tree").getRootNode().reload();
        this.loadPath(this.treePath);
    },

    // private
    formatActions: function(actionCfgs, disabledActions, tagActions) {
        var actions = [];
        for (var a in actionCfgs) {
            if (typeof(actionCfgs[a]) != "object") {
                continue;
            }
            // check for separators, splits, ...
            if (actionCfgs[a].xtype == "separator") {
                actions.push(actionCfgs[a].value);
                tagActions.push(actionCfgs[a].value);
            } else {
                if (actionCfgs[a].menu) {
                    actionCfgs[a].menu = new CQ.Ext.menu.Menu({
                        "items":this.formatActions(actionCfgs[a].menu,
                                disabledActions, tagActions)
                    });
                }
                var actionCfg = this.formatActionConfig(actionCfgs[a]);
                var action = new CQ.Ext.Action(actionCfg);
                actions.push(action);

                if (actionCfg.disabled) {
                    disabledActions.push(action);
                }

                tagActions.push(action);
            }
        }
        return actions;
    },

    // private
    formatActionConfig: function(config) {
        if (!config.scope) {
            config.scope = this;
        }
        if (typeof(config.handler) == "string") {
            config.handler = eval(config.handler);
        }
        if (config.text) {
            config.text = CQ.I18n.getMessage(config.text);
        }
        if (config.tooltip && config.tooltip.text) {
            config.tooltip.text = CQ.I18n.getMessage(config.tooltip.text);
        }
        if (config.tooltip && config.tooltip.title) {
            config.tooltip.title = CQ.I18n.getMessage(config.tooltip.title);
        }
        return config;
    }
});

CQ.Ext.reg("tagadmin", CQ.TagAdmin);

// constants
CQ.TagAdmin.DD_GROUP = "cq.tagadmin.tree";
/*
 * Copyright 1997-2008 Day Management AG
 * Barfuesserplatz 6, 4001 Basel, Switzerland
 * All Rights Reserved.
 *
 * This software is the confidential and proprietary information of
 * Day Management AG, ("Confidential Information"). You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Day.
 */
CQ.TagAdmin.dialogItems = {
    "jcr:primaryType": "cq:Panel",
    "items": {
        "jcr:primaryType": "cq:WidgetCollection",
        "title": {
            "fieldLabel":CQ.I18n.getMessage("Title"),
            "allowBlank":false,
            "name":"jcr:title"
        },
        "desc": {
            "fieldLabel":CQ.I18n.getMessage("Description"),
            "name":"jcr:description"
        }
    }
};

CQ.TagAdmin.dialogItemsCreate = {
    "jcr:primaryType": "cq:Panel",
    "items": {
        "jcr:primaryType": "cq:WidgetCollection",
        "tag": {
            "fieldLabel":CQ.I18n.getMessage("Tag Name"),
            "allowBlank":false,
            "name":"tag"
        },
        "title": {
            "fieldLabel":CQ.I18n.getMessage("Title"),
            "allowBlank":false,
            "name":"jcr:title"
        },
        "desc": {
            "fieldLabel":CQ.I18n.getMessage("Description"),
            "name":"jcr:description"
        }
    }
};

CQ.TagAdmin.dialogNamespaceCreate = {
    "jcr:primaryType": "cq:Panel",
    "items": {
        "jcr:primaryType": "cq:WidgetCollection",
        "tag": {
            "fieldLabel":CQ.I18n.getMessage("Namespace Name"),
            "allowBlank":false,
            "name":"tag"
        },
        "title": {
            "fieldLabel":CQ.I18n.getMessage("Title"),
            "allowBlank":false,
            "name":"jcr:title"
        },
        "desc": {
            "fieldLabel":CQ.I18n.getMessage("Description"),
            "name":"jcr:description"
        }
    }
};

CQ.TagAdmin.createTag = function() {
    var dialogConfig;
    var tagPath = this.getCurrentTreePath();
    if (tagPath == "/ROOT") {
        dialogConfig = {
            "jcr:primaryType": "cq:Dialog",
            "title":CQ.I18n.getMessage("Create Namespace"),
            "formUrl":"/bin/tagcommand",
            "params": {
                "cmd": "createTag",
                // not setting parentTagID here => create namespace instead of tag
                "_charset_":"utf-8"
            },
            "items": CQ.TagAdmin.dialogNamespaceCreate,
            "okText":"Create"
       }
    } else {
        var parentTagID = tagPath.substring("/ROOT/".length);
        
        // ensure the first path element (namespace) ends with ":"
        if (parentTagID.indexOf("/") > 0) {
            // replace (first) slash after namespace and end it with a slash
            parentTagID = parentTagID.replace("/", ":") + "/";
        } else {
            // add colon after namespace
            parentTagID = parentTagID + ":";
        }

        dialogConfig = {
            "jcr:primaryType": "cq:Dialog",
            "title":CQ.I18n.getMessage("Create Tag"),
            "formUrl":"/bin/tagcommand",
            "params": {
                "cmd":"createTag",
                "parentTagID": parentTagID,
                "_charset_":"utf-8"
            },
            "items": CQ.TagAdmin.dialogItemsCreate,
            "okText":"Create"
       }
    }

    var dialog = CQ.WCM.getDialog(dialogConfig);
    dialog.responseScope = this;
    dialog.success = this.reloadAll;
    dialog.failure = function(form, action) {
        // TODO: proper HTML responses to have sensful error messages in statusText
        //CQ.Ext.Msg.alert("Error", "Could not create tag.\n" + action.response.statusText)
        CQ.Ext.Msg.alert("Error", "Could not create tag.")
    };
    dialog.show();
}

CQ.TagAdmin.deleteTag = function() {
	var msg = CQ.I18n.getMessage("You are going to delete: ") + "<br/>";
	var selections = this.getSelectedTags();
	for (var i=0; i<selections.length; i++) {
		msg += selections[i].id + "<br/>";
	}
	msg += "<br/>" + CQ.I18n.getMessage("Are you sure?");

    var title;
    if (this.getCurrentTreePath() == "/ROOT") {
    	title = (selections.length > 1) ?
			CQ.I18n.getMessage("Delete Namespaces?")
			: CQ.I18n.getMessage("Delete Namespace?");
    } else {
    	title = (selections.length > 1) ?
			CQ.I18n.getMessage("Delete Tags?")
			: CQ.I18n.getMessage("Delete Tag?");
    }

	var admin = this;
	CQ.Ext.Msg.show({
            "title":title,
            "msg":msg,
            "buttons":CQ.Ext.Msg.YESNO,
            "icon":CQ.Ext.MessageBox.QUESTION,
            "fn":function(btnId) {
                if (btnId == "yes") {
                    for (var i=0; i<selections.length; i++) {
                        var selection = selections[i];
                        CQ.HTTP.post(
                                "/bin/tagcommand",
                                function(options, success, response) {
                                        if (success) {
                                                admin.reloadAll();
                                        }
                                },
                                {
                                        "path":selection.id,
                                        "cmd":"deleteTag",
                                        "_charset_":"utf-8"
                                }
                        );
                    }
                }
            },
            "scope":this
     });
}

CQ.TagAdmin.editTag = function() {
    var selections = this.getSelectedTags();
    var path = selections[0].id;
    var dialogConfig = {
        "jcr:primaryType": "cq:Dialog",
        "title":CQ.I18n.getMessage("Edit Tag"),
        "params": {
            "_charset_":"utf-8",
            "path": path
        },
        "items": CQ.TagAdmin.dialogItems
   }

    var dialog = CQ.WCM.getDialog(dialogConfig);
    dialog.responseScope = this;
    dialog.success = this.reloadAll;
    dialog.failure = function(){CQ.Ext.Msg.alert("Error", "Could not save tag.")};
    dialog.loadContent(CQ.Util.escapePath(path));
    dialog.show();
}

CQ.TagAdmin.activateTag = function() {
	var admin = this;
	var selections = this.getSelectedTags();
	for (var i=0; i<selections.length; i++) {
		var selection = selections[i];
		CQ.HTTP.post(
			"/bin/replicate.json",
			function(options, success, response) {
				if (success) {
					admin.reloadAll();
				}
			},
			{ "_charset_":"utf-8", "path":selection.id, "cmd":"Activate" }
		);
	}
}

CQ.TagAdmin.activateTree = function() {
	var admin = this;
	var selections = this.getSelectedTags();
	for (var i=0; i<selections.length; i++) {
		var selection = selections[i];
		CQ.HTTP.post(
			"/bin/replicate.json",
			function(options, success, response) {
				if (success) {
					admin.reloadAll();
				}
			},
			{ "_charset_":"utf-8", "path":selection.id, "cmd":"Activate", "tree":"true" }
		);
	}
}

CQ.TagAdmin.deactivateTag = function() {
	var admin = this;
	var selections = this.getSelectedTags();
	for (var i=0; i<selections.length; i++) {
		var selection = selections[i];
		CQ.HTTP.post(
			"/bin/replicate.json",
			function(options, success, response) {
				if (success) {
					admin.reloadAll();
				}
			},
			{ "_charset_":"utf-8", "path":selection.id, "cmd":"Deactivate" }
		);
	}
}

CQ.TagAdmin.listTaggedItems = function() {
    var selections = this.getSelectedTags();
    var path = selections[0].id;
    var renderPageTitle = function(value, p, record) {
    	var url = CQ.Util.externalize(record.json.path);
    	if (url.indexOf(".") == -1) {
    		url += ".html";
    	}
    	return String.format('<a href="{0}" target="_blank">{1}</a>', url, value);
    }
    var grid = new CQ.Ext.grid.GridPanel({
        store: new CQ.Ext.data.GroupingStore({
            proxy: new CQ.Ext.data.HttpProxy({
                url: "/bin/tagcommand",
                method: "GET"
            }),
            baseParams: { "cmd":"list", "path": path},
            autoLoad:true,
            reader: new CQ.Ext.data.JsonReader({
                root: 'taggedItems',
                totalProperty: 'results',
                id: 'item',
                fields: [
                    'title',
                    'itemPath'
                ]
            })
        }),
        cm:new CQ.Ext.grid.ColumnModel([
            new CQ.Ext.grid.RowNumberer(),
            {
                header: CQ.I18n.getMessage("Title"),
                dataIndex: 'title',
                renderer: renderPageTitle
            },{
                header: CQ.I18n.getMessage("Path"),
                dataIndex: 'itemPath'
            }
        ]),
        viewConfig: {
            forceFit: true,
            groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Items" : "Item"]})'
        },
        sm: new CQ.Ext.grid.RowSelectionModel({singleSelect:true})
    });
    win = new CQ.Ext.Window({
        title:CQ.I18n.getMessage("Items tagged with") + path,
        width:800,
        height:400,
        autoScroll:true,
        items: grid,
        layout:'fit',
        maximizable:true,
        minimizable:true,
        y:200
    }).show();
}

