/*
    davbrowser.js - allows one to browse a DAV-enabled server from a webpage
    Copyright (C) 2004 Guido Wesdorp
    email johnny@debris.demon.nl

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

    $Id: minisax.js,v 1.5 2004/07/31 00:10:15 johnny Exp $

*/

var global = this;
this.davbrowser = new function davbrowser() {
    var davbrowser = this;

    //-------------------------------------------------------------------------
    // some helper functions
    //-------------------------------------------------------------------------

    if (!global._) {
        global._ = function _(s, d) {
            /* remove once i18n is in place */
            for (var attr in d) {
                s = s.replace('${' + attr + '}', d[attr]);
            };
            return s;
        };
    };

    /* this used to be in helpers.js */
    this.AsyncArrayWalker = function AsyncArrayWalker(
            array, elhandler, elhandlercontext, endhandler, endhandlercontext,
            elhandlerargs, endhandlerargs) {
        /* a rather specific solution for a rather specific problem

            this allows calling function elhandler with each element of array 
            in context elhandlercontext, and calls endhandlercontext when done

            elhandler will be called with args 
            (array[currentid], continuehandler, elhandlerargs[0],
            elhandlerargs[1], ...)

            endhandler will be called with args
            (resultarray, endhandlerargs[0], endhandlerargs[1], ...)
        */
        var currentid = 0;
        var resultarray = [];
        var self = this;
        var execute = this.execute = function() {
            var continuehandler = function(el) {
                resultarray.push(el);
                execute.call(self);
            };
            if (currentid >= array.length) {
                var args = [resultarray];
                args = args.concat(endhandlerargs);
                endhandler.apply(endhandlercontext, args);
                return;
            };
            var el = array[currentid];
            var args = [el, continuehandler];
            args = args.concat(elhandlerargs);
            elhandler.apply(elhandlercontext, args);
            currentid++;
        };
    };

    function sort_items(a, b) {
        aisdir = a.id.charAt(a.id.length - 1) == '/';
        bisdir = b.id.charAt(b.id.length - 1) == '/';
        if (aisdir && !bisdir) {
            return -1;
        } else if (bisdir && !aisdir) {
            return 1;
        } else {
            return (a.id > b.id) ? 1 : -1;
        };
    };
        
    var BrowserRoot = this.BrowserRoot = function BrowserRoot(
            doc, rootElement, path, fs, icons) {
        if (doc) {
            this._initialize(doc, rootElement, path, fs, icons);
        };
    };

    BrowserRoot.prototype = new tree.Root;

    BrowserRoot.prototype._initialize = function(
            doc, rootElement, path, fs, icons) {
        tree.Root.prototype._initialize.call(this, doc, rootElement,
                                        icons['closed-collection'],
                                        icons['opened-collection'],
                                        icons['resource']);
        if (path.charAt(0) == '/') {
            path = path.substr(1);
        };
        this.path = path;
        this.fs = fs;
        this.icons = icons;

        this.selected = null;
        // will be overwritten later
        this.extensions = [];

        // some special attributes to function as sort of a handler-queue
        // when getItems is called while another 'thread' is waiting
        // for its results, the handler for the new call will be stored
        // and called when the first call is done (to allow caching)
        // XXX note that this is not safe, JS lacks locking so it can 
        // never be entirely water-tight :(
        this._working = {}; // key path, value boolean
        this._waiting_handlers = {}; // key path, value handler

        //this._preloadIcons();
    };

    BrowserRoot.prototype.getItems = function(item, handler, reload) {
        /* generate a list of subitems items to display, when done call handler

            this is probably the most godawful case of callback hell I've ever
            encountered, up to the point where a list of items is traversed
            with async callbacks calling each other when they're done (see the
            graceful AsyncArrayWalker solution below, for more info see
            helpers.js), the reason for this mess is that we want to have the
            items in an as complete state as possible (so including properties
            and filetype).
        */
        if (item.root == item) {
            // here's where the root gets filled
            var davroot = new BrowserNode(item.path, item);
            davroot.init();
            function handlePropsCall(error, content) {
                if (error) {
                    alert(_('Error loading properties: ' + error));
                };
                davroot.properties = content;
                davroot.filetype = this.getFileType(item);
                davroot.isdavroot = true;
                this.davroot = davroot;
                // optional extension method 'rootItems' to allow adding items
                // from outside
                var items = [davroot];
                if (this.extendRootItems) {
                    var extensions = this.extendRootItems();
                    items = items.concat(extensions);
                    this.extensions = extensions;
                };
                this._continueOpen(items);
            };
            this.fs.getProps(davroot.getPath(), handlePropsCall, this);
            return;
        };
        // this bit is necessary to prevent simultaneous calls to fs.listDir()
        var path = item.getPath();
        if (!reload && this._working[path]) {
            // store the handler until the working call is done, then we can
            // call it with cached items
            if (!this._waiting_handlers[path]) {
                this._waiting_handlers[path] = [];
            };
            this._waiting_handlers[path].push(handler);
            return;
        };
        this._working[path] = true;
        var browserroot = this;
        function HandlerWrapper(handler, orghandler, orgcontext) {
            this.execute = function(error, response) {
                handler.call(browserroot, error, response, orghandler,
                             orgcontext);
            };
        };
        var wrapper = new HandlerWrapper(this._handleListDirResponse, handler,
                                         item);
        this.fs.listDir(path, wrapper.execute, wrapper, !reload);
    };

    BrowserRoot.prototype._handleListDirResponse = function(error, response,
                                                            handler, item) {
        if (error) {
            alert(_('Error loading directory listing: ${error}',
                    {'error': error}));
            this.doc.getElementsByTagName('body')[0].style.cursor = 'default';
            return;
        };
        var items = [];
        var starttime = new Date().getTime();
        for (var i=0; i < response.length; i++) {
            var itempath = response[i];
            var chunks = itempath.split('/');
            var itemid = string.urldecode(chunks[chunks.length - 1]);
            if (itemid == '') {
                itemid = chunks[chunks.length - 2] + '/';
            };
            var child = new BrowserNode(itemid, item);
            child.init();
            items.push(child);
        };
        items = items.sort(sort_items);

        // now we want to walk through all items, do a listProps and set
        // certain properties according to those, so we grab an
        // AsyncArrayWalker and hope for the best...
        var browserroot = this;
        function ListPropsHandlerWrapper(item, handler, handlercontext,
                                         items) {
            var self = this;
            this.execute = function(curritem, continuehandler) {
                // the handler here is DavFS.getProps
                function getPropsCallback(error, content) {
                    if (error) {
                        curritem.properties = null;
                        curritem.filetype = 'application/unknown';
                    } else {
                        curritem.properties = content;
                        curritem.filetype = browserroot.getFileType(curritem);
                    };
                    continuehandler(curritem);
                };
                handler.call(handlercontext, curritem.getPath(), 
                             getPropsCallback, self, true);
            };
        };
        var listpropshandlerwrapper = new ListPropsHandlerWrapper(
            item, this.fs.getProps, this.fs, items);
        var aw = new davbrowser.AsyncArrayWalker(
                                items, listpropshandlerwrapper.execute, 
                                listpropshandlerwrapper,
                                this._handleArrayWalkerResponse, this, 
                                [], [handler, item]);
        aw.execute();
    };

    BrowserRoot.prototype._handleArrayWalkerResponse = function(items, handler,
                                                                item) {
        handler.call(item, items);
        var path = item.getPath();
        while (this._waiting_handlers[path] &&
                this._waiting_handlers[path].length) {
            var waiting = this._waiting_handlers[path].pop();
            waiting.call(item, items);
        };
        this._working[path] = false;
    };

    BrowserRoot.prototype.getPath = function() {
        return '';
    };

    BrowserRoot.prototype.getItemByPath = function(path) {
        /* return an item by its path */
        if (path.charAt(0) == '/') {
            path = path.substr(1);
        };
        if (path == '') {
            return this;
        };
        var root = this.davroot;
        // path = path.substr(rootid.length);
        /*
        if (path == '' || path == '/') {
            return root;
        };
        */
        var chunks = path.split('/');
        if (chunks[chunks.length - 1] == '') {
            chunks.pop();
        };
        if (chunks[0] == root.id) {
            if (chunks.length == 1) {
                return root;
            };
            chunks.shift();
        };
        if (chunks.length == 1) {
            for (var i=0; i < this.extensions.length; i++) {
                if (this.extensions[i].id == path) {
                    return this.extensions[i];
                };
            };
        };
        return root.getItemByPath(path);
    };

    BrowserRoot.prototype.getIcon = function(item) {
        /* return an img object that represents the file type */
        var img = this.doc.createElement('img');
        var src = item.icon_src;
        if (!src) {
            if (item.root == item) {
                src = this.icons['browser'];
            } else {
                src = this.getIconSource(item);
            };
        };
        img.setAttribute('src', src);
        return img;
    };

    BrowserRoot.prototype.getIconSource = function(item) {
        var src = null;
        if (this.icons[item.filetype]) {
            src = this.icons[item.filetype];
        } else {
            var chunks = item.filetype.split('/');
            src = this.icons[chunks[0] + '/*'];
            if (!src) {
                src = this.icons['*'];
            };
        };
        return src;
    };

    BrowserRoot.prototype.getFileType = function(item) {
        var filename = item.id;
        var filetype = null;
        if (item == item.root) {
            filetype = 'directory';
        } else if (filename.charAt(filename.length - 1) == '/') {
            filetype = 'directory';
        } else {
            if (item.properties) {
                if (!item.properties['DAV:'] ||
                        !item.properties['DAV:']['getcontenttype']) {
                    filetype = 'application/unknown';
                } else {
                    filetype = item.properties['DAV:']['getcontenttype'].
                                    documentElement.childNodes[0].toXML();
                };
            } else {
                filetype = 'application/unknown';
            };
        };
        return filetype;
    };

    BrowserRoot.prototype._preloadIcons = function() {
        var loaded = [];
        var body = this.doc.getElementsByTagName('body')[0];
        for (var filetype in this.icons) {
            var path = this.icons[filetype];
            var img = new Image();
            img.src = path;
        };
    };

    function BrowserNode(id, parent) {
        if (id || parent) {
            this._initialize(id, parent);
        };
    };

    BrowserNode.prototype = new tree.Node;

    BrowserNode.prototype._initialize = function(id, parent) {
        tree.Node.prototype._initialize(id, parent);
        this.id = id;
        this.parent = parent;
        this.items = {};
        this.isdavroot = false;
    };

    BrowserNode.prototype.isCollection = function() {
        var iscol = (this.id.substr(this.id.length - 1) == '/');
        return iscol;
    };

    BrowserNode.prototype.reload = function() {
        this.itemids = null;
        this.properties = null;
        if (!this.isCollection() || this.opened) {
            this.close();
            this.open(true);
        };
    };

    BrowserNode.prototype.reloadSingle = function() {
        /* reload the properties of a non-collection */
        var item = this;
        var handler = function(error, content) {
            if (error) {
                alert(_('Error loading properties: ${error}',
                        {'error': error}));
                return;
            };
            item.properties = content;
        };
        this.root.fs.getProps(this.getPath(), handler);
    };
}();


