/*
 * PiAnoS (Picture Annotation System), A fingerprint examiner training tool
 * Copyright (C) 2011-2021 University of Lausanne, Switzerland
 * https://ips-labs.unil.ch/pianos
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */

/**
 * The main object representing the interface (*needs* to be a singleton)
 *
 * @constructor
 */
function Pianos4Backend(params)
{
    udb.inherit(this, udb.application, params, 'Pianos4Backend');

    // ------------------------------
    // Constructor
    // ------------------------------
    params = params || {};
    if (!params.divid) params.divid = 'pianos-backend';
    if (!params.user) params.user = { id: 0, name: 'Unnamed user', tutor: false };
    if (!params.icons) params.icons = {
        folder: ['css/icons/backend/folder.gif','css/icons/backend/folderopen.gif'],
        pgroup: ['css/icons/backend/Address-book16.png','css/icons/backend/Address-book-open16.png'],
        exercise: 'css/icons/backend/page.gif',
        person: 'css/icons/backend/Person16.png',
        person_inactive: 'css/icons/backend/Person16.png',
        _delete: 'css/icons/backend/Delete16.png',
        adduser: 'css/icons/backend/NewPerson16.png',
        addgroup: 'css/icons/backend/NewAddress-book16.png',
        refresh: 'css/icons/backend/arrow_refresh.png',
        management: 'css/icons/backend/cog_edit.png',
        image: 'css/icons/backend/images.png',
        message: ['css/icons/backend/page.png','css/icons/backend/page_add.png','css/icons/backend/page_edit.png','css/icons/backend/page_delete.png'],
    };
    if (typeof(params.hasSearchBox) == 'undefined') params.hasSearchBox = false;
    if (typeof params.showResultsPreview == 'undefined') params.showResultsPreview = false;

    // ------------------------------
    // Public methods
    // ------------------------------

    this.run = function()
    {
        self.__super__.run();

        model = new udb.model({
            apiUrlArgs: [['action','rpc']],
        });
        self.setModel(model);

        buildUI();
    }

    this.cache = function(key, value)
    {
        if (typeof(value) == 'undefined')
        {
            if (!(key in _cache))
                return null;
            return _cache[key];
        }
        else
        {
            _cache[key] = value;
        }
    }

    this.refreshTreePart = function(branch)
    {
        tree.iterate(function(item) {
            if (item.data && item.data.type == branch)
            {
                tree.reload(item.id);
                return true;
            }
        });
    }

    // ------------------------------
    // Private variables
    // ------------------------------

    var self = this;
    var layoutObj = null;
    var tree = null;
    var api = null;
    var panels = {};
    var model = null;
    var _cache = {};
    var searchResults = null;
    var treeFilters = {
        showInactiveUsers: false,
        showPersonalFolders: false,
    };
    var treeItems = {
    };

    // ------------------------------
    // Private functions
    // ------------------------------


    var buildUI = function()
    {
        var mainDiv = $('#'+params.divid);

        mainDiv
            .append(buildTopPane())
            .append(buildLeftPane())
            .append(buildCenterPane());

        mainDiv.css('height', '100%');

        var state = Pianos4Backend.getOrCreateStateCookie();
        var onSidePaneResized = function(direction, pane, item) {
            var state = Pianos4Backend.getOrCreateStateCookie();
            state.layout[direction] = item.size;
            Pianos4Backend.saveStateCookie(state);
        }

        layoutObj = mainDiv.layout({
                defaults: {
                    fxName: 'none',
                },
                center: {
                    onresize: function() { onWindowSizeChanged(false); },
                },
                west: {
                    size: state.layout.west,
                    onresize: onSidePaneResized,
                },
                north: {
                    resizable: false,
                    closable: false,
                    spacing_open: 0,
                    size: 32,
                },
            });

        mainDiv.addClass('evt_udb-tree-selchanged').bind('udb-tree-selchanged', function(event, data) {
            onTreeItemSelected(data.cur);
        });

        $(window).bind('hashchange', onHashChange);
        $( document ).bind( "contextmenu", function( e ){
            e.preventDefault();
            e.stopPropagation();
        } );

        // Call onWindowSizeChanged now to perform the initial layout
        onWindowSizeChanged(true);

        onHashChange();

        // Log the usernname to the logs of the server
        $.ajax( {
            url: '?action=username&value=' + params.user.name
        } );
    }

    var buildTopPane = function()
    {
        var div = $('<div />');
        div.addClass('ui-layout-north ui-widget');
        div.append(
            $('<div />').addClass('ui-widget-content')
                .append( $( '<a href="?" class="pianos4_title">PiAnoS 4</a>' ) )
                .append( $( '<img class="loading" src="css/throbber-18-black-on-white.gif" title="Initializing" alt="Loading ..." />' ) )
                .append( $( '<span />' ).addClass( 'main_links' ) )
        );

        var ml = div.find('.main_links');

        if( params.user.tutor & params.view.show_switchUser )
        {
            ml.append( $('<a />').css( 'cursor', 'pointer' ).text( 'Switch user' ).click(
                function(){
                    var complete = function( data )
                    {
                        var dlgopts = {
                            autoOpen : true,
                            modal : true,
                            width : 800,
                            title : 'All active users',
                            close : function()
                            {
                                $( this ).remove();
                            },
                            buttons : {
                                'Close' : function()
                                {
                                    $( this ).dialog( 'close' );
                                }
                            },
                            open: function()
                            {
                                $( ".ui-widget-overlay" ).bind( "click", function()
                                {
                                    $( "#switch_user_dialog" ).dialog( "close" );
                                } );
                            },
                        };

                        var div = $( '<div />' );
                        
                        if( !data || data.error )
                        {
                            div.addClass( 'ui-state-error' ).css( 'padding', '0 0.5em' ).append( $( '<h3 />' ).text( 'error' ) );
                            if( data && data.message )
                                div.append( $( '<pre />' ).text( data.message ) );
                        } else {
                            var alternate_bg_colors = [ "#f7f7f7", "#ffffff" ];

                            var divsearch = $( "<div />" )
                                .css( "margin-bottom", "10px" )
                                .append( $( "<span />" ).text( "Search for a user:" ) )
                                .append(
                                    $( "<input />" )
                                        .attr( "id", "usersearchinput" )
                                        .css( "border-radius", "20px" )
                                        .css( "margin-left", "10px" )
                                        .css( "padding-left", "10px" )
                                        .css( "outline-width", "0px" )
                                        .on( "keyup", function( e )
                                        {
                                            var searched = $( "#usersearchinput" ).val().toLowerCase();
                                            var i = 0;
                                            data.forEach( function( user ){
                                                var tmp = [];
                                                for( key in user )
                                                {
                                                    tmp.push( user[ key ].toLowerCase().indexOf( searched ) );
                                                }
                                                if( Math.max( ...tmp ) != -1 )
                                                {
                                                    i += 1;

                                                    $( "#user_id_" + user.id )
                                                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                                                        .show();
                                                    $( "#user_username_" + user.id )
                                                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                                                        .show();
                                                    $( "#user_name_" + user.id )
                                                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                                                        .show();

                                                } else {
                                                    $( "#user_id_" + user.id ).hide();
                                                    $( "#user_username_" + user.id ).hide();
                                                    $( "#user_name_" + user.id ).hide();
                                               }
                                            } );
                                        } )
                                );
                            div.append( divsearch );

                            divtable = $( '<div />' )
                                .attr( "id", "all_users_active_dialog" )
                                .css( "display", "grid" )
                                .css( "grid-template-columns", "auto auto auto" )
                                .css( "grid-gap", "5px" )
                                .css( "border", "1px solid #aaaaaa" )
                                .css( "padding", "3px" );
                            
                            divtable.append( $( "<div />" )
                                .css( "background-color", "#eeeeee" )
                                .css( "font-weight", "bold" )
                                .text( "User id" )
                            );
                            divtable.append( $( "<div />" )
                                .css( "background-color", "#eeeeee" )
                                .css( "font-weight", "bold" )
                                .text( "Username" )
                            );
                            divtable.append( $( "<div />" )
                                .css( "background-color", "#eeeeee" )
                                .css( "font-weight", "bold" )
                                .text( "Name" )
                            );
                            
                            var i = 0;
                            data.forEach( function( user ){
                                i += 1;

                                divtable.append( $( "<div />" )
                                    .attr( "id", "user_id_" + user.id )
                                    .css( "background-color", alternate_bg_colors[ i % 2 ] )
                                    .text( user.id )
                                );
                                divtable.append( $( "<div />" )
                                    .attr( "id", "user_username_" + user.id )
                                    .css( "background-color", alternate_bg_colors[ i % 2 ] )
                                    .append( $( "<a />" )
                                        .text( user.username )
                                        .attr( 'href', '?action=switchUser&user=' + user.id )
                                    )
                                );
                                divtable.append( $( "<div />" )
                                    .attr( "id", "user_name_" + user.id )
                                    .css( "background-color", alternate_bg_colors[ i % 2 ] )
                                    .text( user.name )
                                );
                            } );
                            div.append( divtable );
                            
                            var dlg = $( "<div />" )
                                .attr( "id", "switch_user_dialog" )
                                .addClass( "pianos4_dialog" )
                                .css( "max-height", $( window ).height() - 140 )
                                .append( div );

                            dlg.dialog( dlgopts );
                        }
                    }

                    model.rpc('getUserList', { only_users: 1 }, function( users ) {
                        complete( users );
                    } )
                }
            ) );
        }

        if( ! params.user.tutor && params.user.switchedUser )
        {
            ml.append(
                $( '<a />' )
                    .css( 'margin-left', '30px' )
                    .attr( 'href', '?action=switchUser' )
                    .text( 'Switch back as tutor' )
            );
        }

        ml.append(
            $( '<a />' )
                .css( 'margin-left', '30px' )
                .attr( 'href', '?action=logout' )
                .text( 'Logout (' + params.user.name + ( params.user.tutor ? ' - Tutor' : '' ) + ')' )
        );

        return div;
    }

    var processDir = function( folder, contextMenuFunc )
    {
        if( folder.type == "folder" )
        {
            return {
                label : folder.name,
                icon : params.icons.folder,
                hasChildren : parseInt( folder.numexercisesandfolders ) > 0,
                data : {
                    type : 'folder',
                    entity : folder
                },
                cmenu : contextMenuFunc,
                load : [
                    function( cb ) {
                        model.rpc(
                            'getFolderExercisesAndSubFolders',
                            {
                                countExercises: true,
                                showPersonal: treeFilters.showPersonalFolders,
                                folderID: folder.id
                            },
                            cb
                        );

                    },

                    function( ex )
                    {
                        return processDir( ex, contextMenuFunc )
                    }
                ]
            }

        } else if( folder.type == "exercise" ) {
            return {
                label : folder.name,
                icon : params.icons.exercise,
                data : {
                    type : 'exercise',
                    entity : folder
                },
                cmenu : contextMenuFunc,
            }
        }
    }

    var buildLeftPane = function()
    {
        var div = $('<div />');
        div.addClass('ui-layout-west ui-widget');
        var inner = $('<div />').appendTo(div);

        var udbNode = new udb.node({element:inner});
        tree = new udb.tree({
            parent: udbNode,
            title:'PiAnoS database',
            onItemExpanded: function(item) { onTreeItemExpanded(item); },
            onItemCollapsed: function(item) { onTreeItemCollapsed(item); },
        });

        if( params.user.tutor && params.hasSearchBox )
        {
            var search = new udb.panel( {
                parent: udbNode,
                title: 'Search'
            } );
            
            var build_search_dialog_box = function( data )
            {
                var dlgopts = {
                    autoOpen : true,
                    modal : true,
                    width : 800,
                    title : "Search in all available data",
                    close : function()
                    {
                        $( this ).remove();
                    },
                    buttons : {
                        'Close' : function()
                        {
                            $( this ).dialog( 'close' );
                        }
                    },
                    open: function()
                    {
                        $( ".ui-widget-overlay" ).bind( "click", function()
                        {
                            $( "#search_dialog" ).dialog( "close" );
                        } );
                    },
                };

                var search_input_field = $( '<div />' )
                    .append(
                        $( "<span />" )
                            .css( "margin-right", "10px" )
                            .text( "Search data:" )
                    ).append(
                        $( "<input />" )
                            .attr( "id", "usersearchinput" )
                            .css( "border-radius", "20px" )
                            .css( "margin-left", "10px" )
                            .css( "padding-left", "10px" )
                            .css( "outline-width", "0px" )
                            .on( "keyup", function( e )
                            {
                                var searched = $( "#usersearchinput" ).val().toLowerCase();
                                for( key in data )
                                {
                                    data[ key ].forEach( function( d )
                                    {
                                        for( col in d )
                                        {
                                            if( d[ col ].toLowerCase().indexOf( searched ) != -1 )
                                            {
                                                $( "#" + key + "_id_" + d.id ).show();
                                                $( "#" + key + "_username_" + d.id ).show();
                                                $( "#" + key + "_name_" + d.id ).show();
                                                $( "#" + key + "_id_" + d.exid ).show();
                                                $( "#" + key + "_name_" + d.exid ).show();
                                                $( "#" + key + "_foldername_" + d.exid ).show();
                                                
                                                break;
                                                
                                            } else {
                                                $( "#" + key + "_id_" + d.id ).hide();
                                                $( "#" + key + "_username_" + d.id ).hide();
                                                $( "#" + key + "_name_" + d.id ).hide();
                                                $( "#" + key + "_id_" + d.exid ).hide();
                                                $( "#" + key + "_name_" + d.exid ).hide();
                                                $( "#" + key + "_foldername_" + d.exid ).hide();
                                            }
                                        }
                                    } );
                                }
                            } )
                );
                
                var alternate_bg_colors = [ "#f7f7f7", "#ffffff" ];
                
                // User table list
                var userlistdivtable = $( '<div />' )
                    .css( "display", "grid" )
                    .css( "grid-template-columns", "auto auto auto" )
                    .css( "grid-gap", "5px" )
                    .css( "border", "1px solid #aaaaaa" )
                    .css( "padding", "3px" );
                
                userlistdivtable.append( $( "<div />" )
                    .css( "background-color", "#eeeeee" )
                    .css( "font-weight", "bold" )
                    .text( "User id" )
                );
                userlistdivtable.append( $( "<div />" )
                    .css( "background-color", "#eeeeee" )
                    .css( "font-weight", "bold" )
                    .text( "Username" )
                );
                userlistdivtable.append( $( "<div />" )
                    .css( "background-color", "#eeeeee" )
                    .css( "font-weight", "bold" )
                    .text( "Name" )
                );
                
                var i = 0;
                data[ "users" ].forEach( function( user ){
                    i += 1;
                    
                    userlistdivtable.append( $( "<div />" )
                        .attr( "id", "users_id_" + user.id )
                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                        .text( user.id )
                    );
                    userlistdivtable.append( $( "<div />" )
                        .attr( "id", "users_username_" + user.id )
                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                        .css( "text-decoration", "underline" )
                        .css( "cursor", "pointer" )
                        .text( user.username )
                        .on( "click", function(){
                            window.location = '?#person/' + user.id;
                            $( "#search_dialog" ).dialog( "close" );
                            $( "#search_dialog" ).remove();
                        } )
                    );
                    userlistdivtable.append( $( "<div />" )
                        .attr( "id", "users_name_" + user.id )
                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                        .text( user.name )
                    );
                } );
                
                // Exercises table list
                var exlistdivtable = $( '<div />' )
                    .css( "display", "grid" )
                    .css( "grid-template-columns", "auto auto auto" )
                    .css( "grid-gap", "5px" )
                    .css( "border", "1px solid #aaaaaa" )
                    .css( "padding", "3px" );
                
                exlistdivtable.append( $( "<div />" )
                    .css( "background-color", "#eeeeee" )
                    .css( "font-weight", "bold" )
                    .text( "Exercise id" )
                );
                exlistdivtable.append( $( "<div />" )
                    .css( "background-color", "#eeeeee" )
                    .css( "font-weight", "bold" )
                    .text( "Folder name" )
                );
                exlistdivtable.append( $( "<div />" )
                    .css( "background-color", "#eeeeee" )
                    .css( "font-weight", "bold" )
                    .text( "Name" )
                );
                
                var i = 0;
                data[ "exercises" ].forEach( function( ex ){
                    i += 1;
                    
                    exlistdivtable.append( $( "<div />" )
                        .attr( "id", "exercises_id_" + ex.exid )
                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                        .text( ex.exid )
                    );
                    exlistdivtable.append( $( "<div />" )
                        .attr( "id", "exercises_foldername_" + ex.exid )
                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                        .css( "text-decoration", "underline" )
                        .css( "cursor", "pointer" )
                        .text( ex.foldername )
                        .on( "click", function(){
                            window.location = '?#folder/' + ex.folderid;
                            $( "#search_dialog" ).dialog( "close" );
                            $( "#search_dialog" ).remove();
                        } )
                    );
                    exlistdivtable.append( $( "<div />" )
                        .attr( "id", "exercises_name_" + ex.exid )
                        .css( "background-color", alternate_bg_colors[ i % 2 ] )
                        .css( "text-decoration", "underline" )
                        .css( "cursor", "pointer" )
                        .text( ex.exname )
                        .on( "click", function(){
                            window.location = '?#exercise/' + ex.exid;
                            $( "#search_dialog" ).dialog( "close" );
                            $( "#search_dialog" ).remove();
                        } )
                    );
                } );
                
                var dlg = $( "<div />" )
                    .attr( "id", "search_dialog" )
                    .addClass( "pianos4_dialog" )
                    .css( "max-height", $( window ).height() - 140 )
                    .append( search_input_field )
                    .append( userlistdivtable )
                    .append( exlistdivtable );

                dlg.dialog( dlgopts );
            };
            
            var search_for_data = function()
            {
                $.ajax( {
                    url: '?action=rpc&method=getSearchableData',
                    type: 'GET',
                    dataType: 'JSON',
                    success: function( data ){
                        if( ! data.error )
                        {
                            build_search_dialog_box( data.ret );
                        } else {
                            toastr.error( "Server error while loading the list of user" );
                        }
                    },
                    error: function(){
                        toastr.error( "Server error while loading the list of user" );
                    }
                } );
            };
            
            var searchbutton = $( "<button />" )
                .attr( "id", "pianos4_search_box" )
                .text( "Search" )
                .on( "click", search_for_data );
            
            search
                .dom( true )
                .append( searchbutton );
        }

        var filters = null;
        if (params.user.tutor)
        {
            filters = new udb.panel({parent:udbNode, title:null});
            var form = $('<div />').attr('id','pianos4_tree_filters').css('font-size','0.8em')
                .append($('<form />').bind('submit', function(e) { e.preventDefault(); })
                    .append($('<input />').attr('id', 'p4_tree_filter_inactive').attr('type','checkbox').click(function() { onTreeFiltersChanged(); }))
                    .append($('<label />').attr('for', 'p4_tree_filter_inactive').text('Inactive users'))
                    .append($('<input />').attr('id', 'p4_tree_filter_personal').attr('type','checkbox').click(function() { onTreeFiltersChanged(); }))
                    .append($('<label />').attr('for', 'p4_tree_filter_personal').text('Personal folders'))
                );
            filters.dom(true).append(form);
        }

        var sz = new udb.sizer('vertical');
        sz.add(tree, 1, udb.sizer.EXPAND);
        if (filters)
            sz.add(filters, 0, udb.sizer.EXPAND);
        if (search)
            sz.add(search, 0, udb.sizer.EXPAND);
        udbNode.setSizer(sz);

        var contextMenuFunc = params.user.tutor ? onTreeContextMenu : null;

        tree_exercises = tree.addEx({
            parent: tree.root(),
            label: 'All exercises',
            icon: params.icons.folder,
            data: {type:'allex'},
            cmenu: contextMenuFunc,
            load: [    function(cb) {
                        model.rpc('getFolders', {'countExercises':true, 'showPersonal':treeFilters.showPersonalFolders}, cb);
                    },

                    function( folder )
                    {
                        return processDir( folder, contextMenuFunc )
                    }
            ],
        });
        treeItems.folders = tree_exercises;

        var autoLoadExCallback = null;
        if (params.user.tutor)
        {
            autoLoadExCallback = function(item) {
                var state = Pianos4Backend.getOrCreateStateCookie();
                tree.iterate(function(item) {
                    if (item.data && item.data.type == 'folder' && item.data.entity.id in state.tree.folders)
                        tree.load(item.id);
                });
            };
        }
        else
        {
            autoLoadExCallback = function(item) {
                var folders = tree.getItemChildren(item.id);
                for (var i in folders)
                    tree.load(folders[i].id);
            };
        }

        tree.load(tree_exercises, null, null, autoLoadExCallback);

        if (params.user.tutor)
        {
            var tree_users = tree.addEx({
                parent: tree.root(),
                label: 'All users',
                icon: params.icons.pgroup,
                data: {type:'allusers'},
                cmenu: contextMenuFunc,
                load: [    function(cb) {
                            model.rpc('getUserGroups', {'countUsers':true, 'showInactive':treeFilters.showInactiveUsers}, cb);
                        },
                        function(group) {
                            return {
                                label: group.name,
                                icon: params.icons.pgroup,
                                hasChildren: parseInt(group.nummembers) > 0,
                                data: {type:'pgroup', entity: group},
                                cmenu: contextMenuFunc,
                                load: [function(cb) {
                                            model.rpc('getUsers', {'groupID':group.id, 'showInactive':treeFilters.showInactiveUsers}, cb);
                                        },
                                        function(user) {
                                            return {
                                                label: user.name,
                                                icon: function() {
                                                    return params.icons.person;
                                                },
                                                data: {type:'person', entity: user},
                                                cmenu: contextMenuFunc,
                                            };
                                        }
                                ],
                            };
                        }
                ],
            });
            treeItems.pgroups = tree_users;

            var autoLoadGroupsCallback = function(item) {
                var state = Pianos4Backend.getOrCreateStateCookie();
                tree.iterate(function(item) {
                    if (item.data && item.data.type == 'pgroup' && item.data.entity.id in state.tree.pgroups)
                        tree.load(item.id);
                });
            }

            tree.load(tree_users, null, null, autoLoadGroupsCallback);

            var tree_management = tree.addEx({
                parent: tree.root(),
                label: 'Management',
                icon: params.icons.management,
                data: {type:'management'},
            });
            var tree_management_images = tree.addEx({
                parent: tree_management,
                label: 'Images',
                icon: params.icons.image,
                data: {type:'manage_images',hash:'manage/images'},
            });
            var tree_management_messages = tree.addEx({
                parent: tree_management,
                label: 'Messages',
                icon: params.icons.message[0],
                data: {type:'manage_images',hash:'manage/messages'},
            });
        }

        return div;
    }

    var buildCenterPane = function()
    {
        var div = $('<div />');
        div.addClass('ui-layout-center ui-widget');
        div.append($('<div />').addClass('ui-widget-content'));
        return div;
    }

    var onWindowSizeChanged = function(isInitial)
    {
        var left = $('.ui-layout-west');
        var leftsz = [left.width(),left.height()];
        log(leftsz);
        tree.getParent().dom().width(leftsz[0]).height(leftsz[1]);
        tree.getParent().layout();
    }

    this.has_updateUI = true;
    this.updateUI = function(operations)
    {
        var loading = $('#'+params.divid+' .loading');

        if (operations.length > 0)
        {
            loading.removeCSS('visibility');
            var label = 'Working ...';
            for (var op in operations) { if (op=='length') continue; label = operations[op]; break; }
            setStatus(label);
        }
        else
        {
            loading.css('visibility','hidden');

            setStatus('Ready');
        }
    }

    var setStatus = function(status)
    {
        log('Status: '+status);
        var loading = $('#'+params.divid+' .loading');
        loading.attr('title', status);
    }

    var onHashChange = function(event)
    {
        var hash = location.hash;
        log("onHashChange() : '"+hash+"'");
        var constructor = null;
        var opts = null;
        if (hash.length > 1)
        {
            var path = hash.substr(1, hash.length);
            log('QUERY='+path);
            var tokens = path.split('/');
            if (tokens.length >= 2)
            {
                var id = parseInt(tokens[1]);
                var objtype = tokens[0];
                if (id > 0)
                {
                    switch (objtype)
                    {
                    case 'exercise':
                        if (tokens.length > 2)
                        {
                            var clone = tokens[2] == 'clone';
                            if (tokens[2] == 'edit' || clone)
                            {
                                constructor = Pianos4Backend.CreateExercise;
                                opts = {
                                    onCreate: function(id) {
                                        // tree.reload(item.parent);
                                        tree.reload(tree_exercises);
                                        location.hash = 'exercise/'+id;
                                    },
                                    id: id,
                                    user: params.user,
                                    clone: clone,
                                };
                            }
                        }

                        if (!constructor)
                            constructor = params.user.tutor ? Pianos4Backend.AdminExercise : Pianos4Backend.UserExercise;

                        if (!opts)
                            opts = {id: id, user: params.user, showResultsPreview: params.showResultsPreview};

                        break;

                    case 'createexercise':
                        constructor = Pianos4Backend.CreateExercise;
                        opts = {
                            onCreate: function(exid) {
                                refreshTree('folder', id);
                                location.hash = 'exercise/'+exid;
                            },
                            folder: id,
                            user: params.user,
                        };
                        break;

                    case 'folder':
                        constructor = Pianos4Backend.AdminFolder;
                        opts = {
                            id: id,
                            user: params.user
                        };
                        break;

                    case 'person':
                        constructor = Pianos4Backend.AdminUser;
                        opts = {
                            id: id,
                        };
                        break;
                    }
                }
                else if (objtype == 'manage')
                {
                    switch (tokens[1])
                    {
                    case 'images':
                        constructor = Pianos4Backend.ImageManager;
                        var state = Pianos4Backend.getOrCreateStateCookie();
                        opts = {
                            pageLength: state.imageman.page,
                        };
                        break;

                    case 'messages':
                        constructor = Pianos4Backend.MessageManager;
                        opts = {
                            icons: params.icons.message,
                        };
                        break;
                    }
                }
            }
        }

        if (!constructor)
        {
            if (params.user.tutor)
                constructor = Pianos4Backend.AdminHome;
            else
                constructor = Pianos4Backend.UserHome;
            opts = {
                user: params.user,
                view: params.view
            }
        }

        if (constructor)
            showPanel(constructor, opts);
    }

    var showPanel = function(constructor, params)
    {
        var pane = $('.ui-layout-center>.ui-widget-content');
        pane.html('');
        var container = $('<div />').appendTo(pane);
        var panel = new constructor(container, params);
        panel.init();
    }

    var getPopupOptions = function( item )
    {
        switch( item.data.type )
        {
            case 'allex':
                return _getPopupOptions( [
                    ['Refresh', onRefreshNode, params.icons.refresh],
                    '--',
                    ['Create new folder', onCreateNewFolder, params.icons.folder[0]]
                ] );
                break;

            case 'folder':
                return _getPopupOptions( [
                    ['Refresh', onRefreshNode, params.icons.refresh],
                    '--',
                    ['Create new exercise', function(item){ location.hash = 'createexercise/'+item.data.entity.id; }, params.icons.exercise],
                    '--',
                    ['Create new sub-folder', onCreateNewFolder, params.icons.folder[0]],
                    ['Rename folder', onEditFolder, params.icons.exercise],
                    ['Move folder', onMoveFolder, params.icons.management],
                    ['Delete folder', onDeleteFolder, params.icons._delete]
                ] );
                break;

            case 'allusers':
                return _getPopupOptions( [
                    ['Refresh', onRefreshNode, params.icons.refresh],
                    '--',
                    ['Create new group', onCreateNewGroup, params.icons.addgroup]
                ] );
                break;

            case 'pgroup':
                return _getPopupOptions( [
                    ['Refresh', onRefreshNode, params.icons.refresh],
                    '--',
                    ['Create new user', onCreateNewUser, params.icons.adduser],
                    ['Create multiple users', onCreateNewMultiUser, params.icons.adduser],
                    ['Get default password list', getDefaultPasswordListForUsers, params.icons.adduser],
                    '--',
                    ['Rename group', onEditUserGroup, params.icons.person],
                    '--',
                    ['Delete group', onDeleteGroup, params.icons._delete]
                ] );
                break;

            case 'person':
                return _getPopupOptions( [
                    ['Edit user', onEditUser, params.icons.person],
                    '--',
                    ['Delete user', onDeleteUser, params.icons._delete]
                ] );
                break;

            case 'exercise':
                return _getPopupOptions( [
                    ['Edit exercise', function(item){ location.hash = 'exercise/'+item.data.entity.id+'/edit'; }, params.icons.exercise],
                    ['Move exercise', onMoveExercise, params.icons.management],
                    '--',
                    ['Delete exercise', onDeleteExercise, params.icons._delete]
                ] );
                break;

            default:
                return [];
                break;
        }
    }

    var _getPopupOptions = function(items)
    {
        var opt = [];
        for (var i in items)
        {
            var item = items[i];
            if (typeof(item) == 'string' || !item.length)
                opt.push(item);
            else
            {
                var o = { label:item[0], action:item[1] };
                if (item.length>2)
                    o.icon = item[2];
                opt.push(o);
            }
        }
        return opt;
    }

    var onTreeContextMenu = function(item, evt)
    {
        assert(params.user.tutor);

        var options = getPopupOptions(item);

        if (options.length == 0)
            return;

        var w = $(window);
        var cancel = $('<div />').css('position','absolute').css('top',0).css('left',0).css('width',w.width()).css('height',w.height()).css('z-index','9').bind('mousedown', function(e) {
            menu.remove();
            cancel.remove();
            $(document).unbind('mousedown', cancel);
        }).appendTo('body');


        var menu = $('<ul />').css('position','absolute').css('top',evt.clientY).css('left',evt.clientX).css('z-index','10');
        for (var i in options) (function(opt) {
            if (typeof(opt) == 'string')
            {
                menu.append($('<li />').addClass('ui-menu-item').append($('<div />').css('width','100%').css('height','1px').css('margin','3px').css('background-color','#dddddd')));
            }
            else
            {
                var icon = !opt.icon ? '' : $('<span />').css('display','inline-block').css('vertical-align','middle').append($('<img />').attr('src',opt.icon));
                menu.append($('<li />').append($('<a />').append($('<div />').append(icon).append($('<span />').text(opt.label).css('display','inline-block').css('margin-left','0.5em').css('vertical-align','middle'))).attr('href','#').bind('click', function(e) {
                    e.preventDefault();
                    log('chose '+opt.label);
                    menu.remove();
                    cancel.remove();
                    opt.action(item);
                })));
            }
        })(options[i]);
        menu.menu().appendTo('body');
    }

    var onRefreshNode = function(item)
    {
        tree.reload(item.id);
    }

    var onCreateNewFolder = function( item )
    {
        var validate = function()
        {
            var name = $( '#casename' ).val();

            if( name == "" )
            {
                toastr.error( "need a name for the new folder" );
            } else {
                var childof = null;
                if( item.data.type != "allex" )
                    childof = item.data.entity.id;

                model.rpc( 'createFolder', { 'name': name, 'childof': childof }, function()
                {
                    tree.reload( item.id );
                } );

                $( '#newfolderui' ).dialog( 'close' );
            }
        }

        var createnewfolder = $( '<div />' )
            .css( 'padding-top', '10px' )
            .css( 'padding-bottom', '10px' )
            .append(
                $( '<div />' )
                    .css( 'display', 'grid' )
                    .css( 'grid-template-columns', '1fr 2fr' )
                    .css( 'grid-gap', '2px' )
                    .css( 'height', '100%' )
                    .css( 'width', '100%' )
                    .append( $( '<div />' ).text( "Name" ) )
                    .append(
                        $( '<input />' )
                            .attr( 'type', 'input' )
                            .attr( 'id', 'casename' )
                            .on( 'keydown', function( e )
                            {
                                if ( e.which == 13 )
                                {
                                    e.preventDefault();
                                    validate();
                                }
                            } )
                    )
            );

        var dlgopts = {
            autoOpen: true,
            modal: true,
            width: 300,
            resizable: true,
            title: "New folder",
            buttons: {
                'OK': function()
                {
                    validate();
                },
                'Close': function()
                {
                    $( this ).dialog( 'close' );
                },
            }
        }

        $( '<div />' )
            .attr( 'id', 'newfolderui' )
            .addClass( 'pianos4_dialog' )
            .append( createnewfolder )
            .dialog( dlgopts );
    }

    var onMoveFolder = function( item )
    {
        onMoveAny( item, 'moveFolder' );
    }
    var onMoveExercise = function( item )
    {
        onMoveAny( item, 'moveExercise' );
    }
    var onMoveAny = function( item, func )
    {
        var pushmodification = function( id, tofolder )
        {
            $.ajax( {
                url: '?action=rpc&method=' + func,
                data: {
                    id: id,
                    tofolder: tofolder
                },
                type: "POST",
                dataType: 'json',

                success: function( data )
                {
                    if( ! data.error )
                    {
                        toastr.success( "Folder moved" );
                        location.reload();

                    } else {
                        toastr.error( data.message, "Server side error" );
                    }
                },
                error: function( data )
                {
                    toastr.error( "Network error" );
                }
            } );
        }

        $( '#moveFolderChosen' ).remove();
        $( '#movefolderui' ).remove();

        var movefolderui = $( '<div />' )
            .css( 'padding-top', '20px' );

        $( '<div />' )
            .text( "Move to:" )
            .append(
                $( '<div />' )
                    .css( 'height', '100%' )
                    .css( 'width', '100%' )
                    .css( 'margin-top', '2em' )
                    .append( $( '<select/>' )
                        .attr( 'id', 'moveFolderChosen' )
                        .attr( 'data-placeholder', 'List of folders' )
                        .attr( 'class', 'chosen-select' )
                        .append( $( '<option value="">Loading folders</option>' ) )
                )
            )
            .appendTo( movefolderui );

        var dlgopts = {
            autoOpen: true,
            modal: true,
            width: 300,
            height: 500,
            resizable: true,
            title: "Move",
            buttons: {
                'OK': function()
                {
                    pushmodification( item.data.entity.id, $( '#moveFolderChosen' ).val() );
                    $( '#movefolderui' ).remove()
                },
                'Close': function()
                {
                    $( this ).dialog( 'close' );
                    $( '#movefolderui' ).remove()
                },
            }
        }

        var movefolderui = $( '<div />' )
            .attr( 'id', 'movefolderui' )
            .addClass( 'pianos4_dialog' )
            .append( movefolderui )
            .dialog( dlgopts );

        $( '#moveFolderChosen' )
            .chosen( {
                search_contains: true,
                width: "100%",
                "max-height": "100px"
            } );

        $.ajax( {
            url: '?action=rpc&method=getFolders',
            data: {
                subFolders: true,
                showPersonal: true,
                notFolder: item.data.entity.id
            },
            type: "POST",
            dataType: 'json',

            success: function( data )
            {
                if( ! data.error )
                {
                    $( '#moveFolderChosen' ).empty();

                    for( var i in data.ret )
                    {
                        var d = data.ret[ i ];

                        $( '#moveFolderChosen' )
                            .append(
                                $( '<option/>' )
                                    .attr( 'id', 'option_' + d.name )
                                    .attr( 'value', d.id )
                                    .text( d.name )
                                    .trigger( 'chosen:updated' )
                            );
                    }

                    $( '#moveFolderChosen' ).trigger("chosen:updated");

                } else {
                    toastr.error( data.message, "Server side error" );
                }
            },
            error: function( data )
            {
                toastr.error( "Network error" );
            }
        } );
    }

    var _onEditItemName = function(item, api_func)
    {
        var dlgopts = {
            title: "Rename '"+item.data.entity.name+"'",
            fields: [
                { type: 'text', name: 'name', label: 'Name', value: item.data.entity.name, constraints: ['not-empty'] },
            ],
            ok: function(data) {
                assert('name' in data);
                model.rpc(api_func, {'id': item.data.entity.id, 'name': data.name}, function() {
                    tree.reload(item.parent);
                });
            },
        };

        Pianos4Backend.showFormDialog(dlgopts, true);
    }

    var onEditFolder = function(item)
    {
        log('editing folder');
        log(item);
        _onEditItemName(item, 'setFolderName')
    }

    var onEditUserGroup = function(item)
    {
        log('editing user group');
        log(item);
        _onEditItemName(item, 'setUserGroupName')
    }

    var onCreateNewGroup = function(item)
    {
        var dlgopts = {
            title: 'New user group',
            fields: [
                { type: 'text', name: 'name', label: 'Name', constraints: ['not-empty'] },
            ],
            ok: function(data) {
                assert('name' in data);
                model.rpc('createUserGroup', {'name': data.name}, function() {
                    self.cache('acl-groups',null);
                    tree.reload(item.id);
                });
            },
        };

        Pianos4Backend.showFormDialog(dlgopts);
    }

    var getDefaultPasswordListForUsers = function( item )
    {
        $( "#get_default_password_list_for_users" ).dialog().remove();

        var div = $( "<div />" )
            .attr( "id", "get_default_password_list_for_users" )
            .append(
                $( "<div />" )
                    .css( "display", "grid" )
                    .css( "grid-template-columns", "1fr 4fr" )
                    .css( "grid-gap", "10px" )
                    .append( $( "<div />" ).text( "default passwords" ) )
                    .append( $( "<div />" ).append(
                        $( "<textarea />" )
                            .css( "width", "100%" )
                            .css( "height", "100%" )
                            .attr( "rows", "10" )
                            .attr( "id", "password_list" )
                            .text( "loading" )
                    ) )
                    .append( $( "<div />" ) )
                    .append( $( "<div />" ).text( "This list is only valid for users that did not changed the password (mendatory while login for the first time)" ) )
            )
            .dialog( {
                autoOpen: true,
                modal: true,
                width: Math.min( $( window ).width() * 0.5, 500 ),
                resizable: true,
                title: "List of password for new users",
                open: function()
                {
                    $( ".ui-widget-overlay" ).bind( "click", function()
                    {
                        $( "#get_default_password_list_for_users" ).dialog( "close" );
                        $( "#get_default_password_list_for_users" ).remove();
                    } );
                    
                    $.ajax( {
                        url: '?action=rpc&method=getDefaultPasswordForUsers',
                        type: 'POST',
                        dataType: 'JSON',
                        data: {
                            folder: item.data.entity.id
                        },
                        success: function( data ){
                            if( ! data.error )
                            {
                                var password_list_content = "";
                                data.ret.forEach( function( user ){
                                    password_list_content += user[ "username" ] + ";" + user[ "password" ] + "\n";
                                } );
                                $( "#password_list" ).val( password_list_content );
                            } else {
                                $( "#password_list" ).val( "Error while loading the list of password." );
                            }
                        },
                        error: function() {
                            toastr.error( "Error while loading the data" );
                        }
                    } );
                },
                buttons: {
                    "Close": function(){
                        $( this ).dialog( "close" );
                        $( this ).remove();
                    }
                }
            } );
    }

    var onCreateNewMultiUser = function(item)
    {
        var users_list = Array();
        var users_to_add_list = Array();

        $.ajax( {
            url: '?action=rpc&method=getUserList',
            type: 'GET',
            dataType: 'JSON',
            success: function( data ){
                if( ! data.error )
                {
                    data.ret.forEach( function( user ){
                        users_list.push( user.username );
                    } );
                } else {
                    toastr.error( "Server error while loading the list of user" );
                }
            },
            error: function(){
                toastr.error( "Server error while loading the list of user" );
            }
        } );

        var validate_user_list = function()
        {
            var usernames = $( "#usernames_list" ).val() || null;
            users_to_add_list = Array();
            var has_invalid_users = false;
            var invalid_username = null;
            var has_non_allowed_character = false;
            var non_allowed_character_user = null;
            var regex = /^[A-Za-z0-9@\-\._]+$/;
            
            if( usernames !== null )
            {
                usernames = usernames.split( "\n" );
                usernames.forEach( function( user ){
                    if( user !== "" )
                    {
                        if( users_list.indexOf( user ) >= 0 )
                        {
                            has_invalid_users = true;
                            invalid_username = user;
                        } else {
                            if( regex.test( user ) )
                            {
                                users_to_add_list.push( user );
                            } else {
                                has_non_allowed_character = true;
                                non_allowed_character_user = user;
                            }
                        }
                    }
                } );
                
                $( "#add_new_users_button > span" )
                    .text( "Add new users (" + users_to_add_list.length + ")" );
                
                if( has_invalid_users )
                {
                    $( "#add_new_users_button" )
                        .prop( "disabled", true )
                        .addClass( "ui-state-disabled" );

                    $( "#usernames_list_error_message" ).text( invalid_username + " already exists" );

                    return false;
                } else if( has_non_allowed_character ) {
                    $( "#add_new_users_button" )
                        .prop( "disabled", true )
                        .addClass( "ui-state-disabled" );

                    $( "#usernames_list_error_message" ).text( non_allowed_character_user + ": can only contain a-z, A-Z, 0-9, @, . or _" );

                    return false;
                } else {
                    $( "#add_new_users_button" )
                        .prop( "disabled", false )
                        .removeClass( "ui-state-disabled" );
                    
                    $( "#usernames_list_error_message" ).text( "" );
                    
                    return users_to_add_list;
                }
                
                if( users_to_add_list.length == 0 )
                {
                    $( "#add_new_users_button" )
                        .prop( "disabled", true )
                        .addClass( "ui-state-disabled" );
                }
                
            } else {
                if( users_to_add_list.length == 0 )
                {
                    $( "#add_new_users_button" )
                        .prop( "disabled", true )
                        .addClass( "ui-state-disabled" );
                    
                    $( "#add_new_users_button > span" )
                        .text( "Add new users" );
                }
                
                $( "#usernames_list_error_message" ).text( "" );
                
                return null;
            }
        }

        var process_new_users = function()
        {
            usernames = validate_user_list();
            
            var folder = item.data.entity.id

            $.ajax( {
                url: '?action=rpc&method=createMultiUser',
                type: "POST",
                data: {
                    usernames: JSON.stringify( usernames ),
                    folder: folder
                },
                dataType: 'JSON',
                
                success: function( data )
                {
                    if( ! data.error )
                    {
                        toastr.success( "Success" );
                        $( "#newmultiuserdialog" ).dialog( "close" );
                        show_new_users_password_list( data.ret );
                    } else {
                        toastr.error( "Server error while creating the users" );
                    }
                },
                error: function( data )
                {
                    toastr.error( "Network error" );
                }
            } );
        }

        var show_new_users_password_list = function( password_list )
        {
            var div = $( "<div />" )
                .attr( "id", "show_new_users_password_list" )
                .append(
                    $( "<div />" )
                        .css( "display", "grid" )
                        .css( "grid-template-columns", "1fr 4fr" )
                        .css( "grid-gap", "10px" )
                        .append( $( "<div />" ).text( "passwords" ) )
                        .append( $( "<div />" ).append(
                            $( "<textarea />" )
                                .css( "width", "100%" )
                                .css( "height", "100%" )
                                .attr( "rows", "10" )
                                .attr( "id", "password_list" )
                        ) )
                )
                .dialog( {
                    autoOpen: true,
                    modal: true,
                    width: Math.min( $( window ).width() * 0.5, 500 ),
                    resizable: true,
                    title: "List of password for new users",
                    open: function()
                    {
                        $( ".ui-dialog-titlebar-close" ).remove();
                        
                        var list_content = "";
                        
                        password_list.forEach( function( user ){
                            list_content += user.username + ";" + user.password + "\n";
                        } );
                        
                        $( "#password_list" ).val( list_content );
                    },
                    buttons: {
                        gotit: {
                            id: "gotit_button",
                            class: ".ui-button .ui-button-text",
                            text: "Got it!",
                            click: function(){
                                $( "#show_new_users_password_list" ).dialog( "close" );
                                tree.reload( item.id );
                            }
                        },
                    }
                } );
        }
        
        var get_help_add_new_users = function()
        {
            $( "<div />" )
                .attr( "id", "new_users_help" )
                .append(
                    $( "<div />" )
                        .css( "margin-bottom", "10px" )
                        .text( "to add a list of users to a particular folder, simply paste in the field a list of usernames separated by a new line, as follows:" )
                )
                .append(
                    $( "<textarea />" )
                        .css( "margin-bottom", "10px" )
                        .css( "width", "100%" )
                        .attr( "rows", "5" )
                        .text( "user1\nuser2\nuser3\nthis_is_a_user" )
                )
                .append(
                    $( "<div />" )
                        .text( "by clicking the 'Add new users' button, a set of users account will be generated. The password will be displayed in the next dialog." )
                )
                .dialog( {
                    title: "Add new users",
                    width: Math.min( $( window ).width() * 0.5, 500 ),
                    modal: true,
                    buttons: {
                        "got it!": function()
                        {
                            $( this ).dialog( "close" );
                        },
                    },
                    close: function()
                    {
                        $( this ).remove();
                    }
                } );
        }
        
        var div = $( "<div />" )
            .attr( "id", "newmultiuserdialog" )
            .append(
                $( "<div />" )
                    .css( "display", "grid" )
                    .css( "grid-template-columns", "1fr 4fr" )
                    .css( "grid-gap", "10px" )
                    .append( $( "<div />" ).text( "usernames" ) )
                    .append( $( "<div />" ).append(
                        $( "<textarea />" )
                            .css( "width", "100%" )
                            .css( "height", "100%" )
                            .attr( "rows", "10" )
                            .attr( "id", "usernames_list" )
                            .on( "keyup", validate_user_list )
                    ) )
                    .append( $( "<div />" ) )
                    .append( $( "<div />" )
                        .attr( "id", "usernames_list_error_message" )
                    )
            )
            .dialog( {
                autoOpen: true,
                modal: true,
                width: Math.min( $( window ).width() * 0.5, 500 ),
                resizable: true,
                title: "Add multiple users",
                open: function()
                {
                    $( ".ui-widget-overlay" ).on( "click", function()
                    {
                        $( "#newmultiuserdialog" ).dialog( "close" );
                        $( "#newmultiuserdialog" ).remove();
                    } );
                    
                    $( "#add_new_users_button" )
                        .prop( "disabled", true )
                        .addClass( "ui-state-disabled" );
                    
                    $( "#add_new_users_button > span" )
                        .text( "Add new users" );
                },
                buttons: {
                    help: {
                        class: "left_button",
                        text: "Get help!",
                        click: get_help_add_new_users
                    },
                    cancel: {
                        text: "Cancel",
                        click: function(){
                            $( this ).dialog( 'close' );
                            $( this ).remove();
                        }
                    },
                    newusers: {
                        id: "add_new_users_button",
                        class: ".ui-button .ui-button-text",
                        text: "Add new users",
                        click: process_new_users
                    },
                }
            } );
    }

    var onCreateNewUser = function(item)
    {
        var dlgopts = {
            title: 'New user',
            fields: [
                { type: 'text', name: 'username', label: 'Username', constraints: ['not-empty'] },
                { type: 'text', name: 'name', label: 'Display name', constraints: ['not-empty'] },
                { type: 'password', name: 'password', label: 'Password', constraints: ['not-empty']  },
                { type: 'password', name: 'password2', label: 'Repeat password', constraints: {'not-empty':true,'must-match':['password','Password and confirmation must match']} },
            ],
            ok: function(data) {
                assert('username' in data && 'name' in data && 'password' in data);
                model.rpc('createUser', {'username':data.username, 'name': data.name, 'password':data.password, 'group':item.data.entity.id}, function() {
                    tree.reload(item.id);
                });
            },
        };

        Pianos4Backend.showFormDialog(dlgopts);

    }

    var onDeleteGroup = function(item)
    {
        var dlgopts = {
            title: 'Confirm user group deletion',
            fields: [
                { type: 'static', label: 'Are you sure you wish to delete the user group '+item.label+' ?' },
            ],
            ok: function(data) {
                model.rpc('deleteUserGroup', {'id': item.data.entity.id}, function() {
                    self.cache('acl-groups', null);
                    tree.reload(tree.getItemParent(item.id).id);
                });
            },
        };

        Pianos4Backend.showFormDialog(dlgopts);
    }

    var onDeleteUser = function(item)
    {
        var datadiv = $('<div />').append($('<img class="loading" src="css/throbber-18-black-on-white.gif" alt="Loading ..." />')).append($('<span />').text('Gathering user data ...'));

        var dlgopts = {
            title: 'Confirm user deletion',
            fields: [
                { type: 'static', label: 'Are you sure you wish to delete the user '+item.label+' ?' },
                { type: 'dom', content: datadiv },
            ],
            ok: function(data) {
                model.rpc('deleteUser', {'id': item.data.entity.id}, function() {
                    tree.reload(tree.getItemParent(item.id).id);
                });
            },
        };

        Pianos4Backend.showFormDialog(dlgopts);

        model.rpc('getUserDataSummary', {'id': item.data.entity.id}, function(summary) {
            datadiv.html('').append($('<div />').css('font-weight','bold').text('User data that will be deleted:'))
                .append($('<div />').text(summary.assignments+' assignment(s)'));
        });
    }

    var onEditUser = function(item)
    {
        assert(item.data && item.data.entity);
        var user = item.data.entity;
        log(user);
        assert(user.groupids.length == user.groups.length && user.groupids.length == 1);
        var groupid = user.groupids[0];

        var showDialog = function(groups)
        {
            var dlgopts = {
                title: 'Edit user',
                fields: [
                    { type: 'text', name: 'username', label: 'Username', value: user.username, disabled:true },
                    { type: 'text', name: 'name', label: 'Display name', constraints: ['not-empty'], value: user.name },
                    { type: 'select', name: 'group', label: 'Group', values: groups, value: groupid },
                    { type: 'checkbox', name: 'active', label: 'Active', values: groups, checked: user.active },
                ],
                ok: function(data) {
                    assert('name' in data);
                    log(data);
                    var mods = [];
                    if (user.name != data.name)
                        mods.push(['name',data.name]);
                    if (groupid != data.group)
                        mods.push(['group',data.group]);
                    var isActive = 'active' in data && data.active == 'on';
                    if (user.active != isActive)
                        mods.push(['active',isActive]);
                    if (mods.length)
                    {
                        var p = {id: item.data.entity.id};
                        for (var i in mods)
                            p[mods[i][0]] = mods[i][1];
                        model.rpc('updateUser', p, function() {
                            if ('name' in p || 'group' in p || 'active' in p)
                                tree.reload(item.parent);
                            if ('group' in p)
                            {
                                tree.iterate(function(item) {
                                    if (item.data && item.data.entity && item.data.entity.id == data.group)
                                    {
                                        tree.reload(item.id);
                                        return true;
                                    }
                                });
                            }
                        });
                    }
                },
            };
            Pianos4Backend.showFormDialog(dlgopts);
        }


        var cachedGroups = udb.app().cache('all-groups');
        if (!cachedGroups)
        {
            udb.app().getModel().rpc('getUserGroups', function(groups) {
                udb.app().cache('all-groups', groups);
                showDialog(groups);
            });
        }
        else
        {
            showDialog(cachedGroups);
        }
    }

    var onDeleteExercise = function(item)
    {
        var datadiv = $('<div />').append($('<img class="loading" src="css/throbber-18-black-on-white.gif" title="Initializing" alt="Loading ..." />')).append($('<span />').text('Gathering exercise data ...'));

        var dlgopts = {
            title: 'Confirm exercise deletion',
            fields: [
                { type: 'static', label: 'Are you sure you wish to delete the exercise '+item.label+' ?' },
                { type: 'dom', content: datadiv },
            ],
            ok: function(data) {
                model.rpc('deleteExercise', {'id': item.data.entity.id}, function() {
                    tree.reload(tree.getItemParent(item.id).id);
                });
            },
        };

        Pianos4Backend.showFormDialog(dlgopts);

        model.rpc('getExerciseDataSummary', {'id': item.data.entity.id}, function(summary) {
            datadiv.html('').append($('<div />').css('font-weight','bold').text('Exercise data that will be deleted:'))
                .append($('<div />').text(summary.assignments+' assignment(s)'));
        });
    }

    var onDeleteFolder = function(item)
    {
        var datadiv = $('<div />').append($('<img class="loading" src="css/throbber-18-black-on-white.gif" title="Initializing" alt="Loading ..." />')).append($('<span />').text('Gathering folder data ...'));

        var dlgopts = {
            title: 'Confirm folder deletion',
            fields: [
                { type: 'static', label: 'Are you sure you wish to delete the folder '+item.label+' ?' },
                { type: 'dom', content: datadiv },
            ],
            ok: function(data) {
                model.rpc('deleteFolder', {'id': item.data.entity.id}, function() {
                    tree.reload(tree.getItemParent(item.id).id);
                });
            },
        };

        Pianos4Backend.showFormDialog(dlgopts);

        model.rpc('getFolderDataSummary', {'id': item.data.entity.id}, function(summary) {
            datadiv.html('').append($('<div />').css('font-weight','bold').text('Exercise data that will be deleted:'))
                .append($('<div />').text(summary.exercises+' exercises(s)'))
                .append($('<div />').text(summary.assignments+' assignment(s)'))
                .append($('<div />').append($('<a href="#" />').text('More ...').click(function(e){
                    e.preventDefault();
                    var details = $(this).parent().next();
                    details.toggle();
                    $(this).text(details.is(':visible') ? 'Less' : 'More ...');
                })))
                .append($('<div />').hide().qtable({
                    columns: ['ID','Name','Num. assignments'],
                    data: summary.all,
                }))
                ;
        });
    }

    var onTreeItemSelected = function(id)
    {
        var item = tree.getItem(id);
        log(item);
        if (item.data && item.data.type && item.data.entity)
        {
            location.hash = '#'+item.data.type+'/'+item.data.entity.id;
        }
        else if (item.data && item.data.hash)
        {
            location.hash = '#'+item.data.hash;
        }
        else
            location.hash = '';
    }

    var refreshTree = function(type, id)
    {
        tree.iterate(function(item) {
            if (item.data && item.data.type == type && item.data.entity.id == id)
            {
                if (type == 'exercise')
                {
                    assert(item.parent);
                    tree.reload(item.parent);
                }
                else
                {
                    tree.reload(item.id);
                }
                return true;
            }
        });
    }

    var onSearch = function(what, endCallback)
    {
        assert(params.hasSearchBox);

        if (!searchResults)
            searchResults = (function() {
                var create = function() {
                    div = $('<div />').attr('id', 'pianos4_search_results').css('display','none')
                        .addClass('ui-widget ui-widget-content ui-corner-all')

                    label = $('<div />').appendTo(div);

                    $('body').append(div);
                };
                var set = function(what, results)
                {
                    div.find('.table').remove();
                    var message;
                    if (results.length == 0)
                    {
                        message = "No results match '"+what+"'";
                        label.text('').removeClass('title').addClass('nomatch').text(message);
                    }
                    else
                    {
                        message = "Search results for '"+what+"'";
                        label.text('').removeClass('nomatch').addClass('title').text(message);
                        var geticon = function(type) {
                            assert(type in params.icons);
                            if (typeof(params.icons[type]) == 'string') return params.icons[type];
                            else return params.icons[type][0];
                        }
                        $('<div />').addClass('table').qtable({
                            data: results,
                            columnMode: 'mapping',
                            showHeaders: false,
                            columns: [
                                { title: 'Rank', render: function(data, i, rowIndex, cell) { cell.text(parseInt(i,10)+1); } },
                                { title: 'Type', render: function(data, i, rowIndex, cell) {
                                    var icon;
                                    if (data[i].type == 'person' && data[i].desc.indexOf('Inactive') >= 0)
                                        icon = 'person_inactive';
                                    else
                                        icon = data[i].type;
                                    cell.append($('<img />').attr('src', geticon(icon)));
                                } },
                                { key: 'name', title: 'Name', render: function(data, i, rowIndex, cell) {
                                    cell.append($('<a />').text(data[i][rowIndex]).attr('href','#'+data[i].type+'/'+data[i].id));
                                } },
                                { key: 'desc', title: 'Description' },
                            ],
                        }).appendTo(div);
                    }
                };
                var dochandler = function(e) {
                    assert(shown);
                    log('doc handler');
                    log(e.target);
                    var $e = $(e.target);
                    if (!$e.is('a') && ($e.is('#pianos4_search_results') || $e.parents('#pianos4_search_results').length))
                        return;
                    api.hide();
                };
                var self = this;
                var shown = false;
                var div = null;
                var label = null;
                var api = {
                    show: function(what, results) {
                        if (!div) create();
                        var sb = $('#pianos4_search_box');
                        var sboffs = sb.offset();
                        div.css('left', (sboffs.left + sb.outerWidth() + 5)+'px')
                            .css('bottom', (5)+'px');
                        set(what, results);
                        div.show();
                        $(document).bind('click', dochandler);
                        shown = true;
                    },
                    hide: function() {
                        $(document).unbind('click', dochandler);
                        div.hide();
                        shown = false;
                    },
                };
                return api;
            })();

        udb.app().getModel().rpc('search', {what:what}, {
            onSuccess: function(result) {
                log(result);
                searchResults.show(what, result);
            },
            onComplete: function(success) {
                if (!success)
                    searchResults.hide();
                if (endCallback) endCallback();
            },
        });
    }

    var onTreeFiltersChanged = function()
    {
        var showInactiveUsers = $('#p4_tree_filter_inactive').prop('checked');
        if (showInactiveUsers != treeFilters.showInactiveUsers)
        {
            treeFilters.showInactiveUsers = showInactiveUsers;
            tree.reload(treeItems.pgroups);
        }

        var showPersonalFolders = $('#p4_tree_filter_personal').prop('checked');
        if (showPersonalFolders != treeFilters.showPersonalFolders)
        {
            treeFilters.showPersonalFolders = showPersonalFolders;
            tree.reload(treeItems.folders);
        }

    }

    var onTreeItemExpanded = function(item)
    {
        if (item.data && item.data.entity)
        {
            var state = Pianos4Backend.getOrCreateStateCookie();
            var t = item.data.type+'s';
            state.tree[t][item.data.entity.id] = true;
            Pianos4Backend.saveStateCookie(state);
        }
    }

    var onTreeItemCollapsed = function(item)
    {
        if (item.data && item.data.entity)
        {
            var state = Pianos4Backend.getOrCreateStateCookie();
            var t = item.data.type+'s';
            delete state.tree[t][item.data.entity.id];
            Pianos4Backend.saveStateCookie(state);
        }
    }
}
