/*
 * Copyright (c) 2011, 2012 Red Hat, Inc.
 *
 * Gnome Documents 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.
 *
 * Gnome Documents 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 Gnome Documents; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Author: Cosimo Cecchi <cosimoc@redhat.com>
 *
 */

const EvView = imports.gi.EvinceView;
const GdkPixbuf = imports.gi.GdkPixbuf;
const Gio = imports.gi.Gio;
const Gd = imports.gi.Gd;
const GdPrivate = imports.gi.GdPrivate;
const Gdk = imports.gi.Gdk;
const GData = imports.gi.GData;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Zpj = imports.gi.Zpj;
const _ = imports.gettext.gettext;

const Lang = imports.lang;
const Signals = imports.signals;

const ChangeMonitor = imports.changeMonitor;
const Global = imports.global;
const Manager = imports.manager;
const Notifications = imports.notifications;
const Path = imports.path;
const Query = imports.query;
const Searchbar = imports.searchbar;
const TrackerUtils = imports.trackerUtils;
const Utils = imports.utils;

const SingleItemJob = new Lang.Class({
    Name: 'SingleItemJob',

    _init: function(urn) {
        this._urn = urn;
        this._cursor = null;
    },

    run: function(flags, callback) {
        this._callback = callback;

        let query = Global.queryBuilder.buildSingleQuery(flags, this._urn);
        Global.connectionQueue.add(query.sparql, null, Lang.bind(this,
            function(object, res) {
                try {
                    let cursor = object.query_finish(res);
                    cursor.next_async(null, Lang.bind(this, this._onCursorNext));
                } catch (e) {
                    log('Unable to query single item ' + e.toString());
                    this._emitCallback();
                }
            }));
    },

    _onCursorNext: function(cursor, res) {
        let valid = false;

        try {
            valid = cursor.next_finish(res);
        } catch (e) {
            log('Unable to query single item ' + e.toString());
        }

        if (!valid) {
            cursor.close();
            this._emitCallback();

            return;
        }

        this._cursor = cursor;
        this._emitCallback();
        cursor.close();
    },

    _emitCallback: function() {
        this._callback(this._cursor);
    }
});

const DeleteItemJob = new Lang.Class({
    Name: 'DeleteItemJob',
// deletes the given resource

    _init: function(urn) {
        this._urn = urn;
    },

    run: function(callback) {
        this._callback = callback;

        let query = Global.queryBuilder.buildDeleteResourceQuery(this._urn);
        Global.connectionQueue.update(query.sparql, null, Lang.bind(this,
            function(object, res) {
                try {
                    object.update_finish(res);
                } catch (e) {
                    log(e);
                }

                if (this._callback)
                    this._callback();
            }));
    }
});


const CollectionIconWatcher = new Lang.Class({
    Name: 'CollectionIconWatcher',

    _init: function(collection) {
        this._collection = collection;
        this._pixbuf = null;

        this._start();
    },

    _clear: function() {
        this._docConnections = {};
        this._urns = [];
        this._docs = [];
    },

    _start: function() {
        this._clear();

        let query = Global.queryBuilder.buildCollectionIconQuery(this._collection.id);
        Global.connectionQueue.add(query.sparql, null, Lang.bind(this,
            function(object, res) {
                let cursor = null;
                try {
                    cursor = object.query_finish(res);
                } catch (e) {
                    log('Unable to query collection items ' + e.toString());
                    return;
                }

                cursor.next_async(null, Lang.bind(this, this._onCursorNext));
            }));
    },

    _onCursorNext: function(cursor, res) {
        let valid = false;

        try {
            valid = cursor.next_finish(res);
        } catch (e) {
            log('Unable to query collection items ' + e.toString());
            cursor.close();
            return;
        }

        if (!valid) {
            cursor.close();
            this._onCollectionIconFinished();

            return;
        }

        let urn = cursor.get_string(0)[0];
        this._urns.push(urn);

        cursor.next_async(null, Lang.bind(this, this._onCursorNext));
    },

    _onCollectionIconFinished: function() {
        if (!this._urns.length)
            return;

        // now this._urns has all the URNs of items contained in the collection
        let toQuery = [];

        this._urns.forEach(Lang.bind(this,
            function(urn) {
                let doc = Global.documentManager.getItemById(urn);
                if (doc)
                    this._docs.push(doc);
                else
                    toQuery.push(urn);
            }));

        this._toQueryRemaining = toQuery.length;
        if (!this._toQueryRemaining) {
            this._allDocsReady();
            return;
        }

        toQuery.forEach(Lang.bind(this,
            function(urn) {
                let job = new SingleItemJob(urn);
                job.run(Query.QueryFlags.UNFILTERED, Lang.bind(this,
                    function(cursor) {
                        if (cursor) {
                            let doc = Global.documentManager.createDocumentFromCursor(cursor);
                            this._docs.push(doc);
                        }

                        this._toQueryCollector();
                    }));
            }));
    },

    _toQueryCollector: function() {
        this._toQueryRemaining--;

        if (!this._toQueryRemaining)
            this._allDocsReady();
    },

    _allDocsReady: function() {
        this._docs.forEach(Lang.bind(this,
            function(doc) {
                let updateId = doc.connect('info-updated',
                                           Lang.bind(this, this._createCollectionIcon));
                this._docConnections[updateId] = doc;
            }));

        this._createCollectionIcon();
    },

    _createCollectionIcon: function() {
        // now this._docs has an array of Document objects from which we will create the
        // collection icon
        let pixbufs = [];

        this._docs.forEach(
            function(doc) {
                pixbufs.push(doc.pristinePixbuf);
            });

        this._pixbuf = GdPrivate.create_collection_icon(Utils.getIconSize(), pixbufs);
        this._emitRefresh();
    },

    _emitRefresh: function() {
        this.emit('icon-updated', this._pixbuf);
    },

    destroy: function() {
        for (id in this._docConnections) {
            let doc = this._docConnections[id];
            doc.disconnect(id);
        }
    },

    refresh: function() {
        this.destroy();
        this._start();
    }
});
Signals.addSignalMethods(CollectionIconWatcher.prototype);

const DocCommon = new Lang.Class({
    Name: 'DocCommon',

    _init: function(cursor) {
        this.id = null;
        this.uri = null;
        this.name = null;
        this.author = null;
        this.mtime = null;
        this.resourceUrn = null;
        this.pixbuf = null;
        this.pristinePixbuf = null;
        this.defaultAppName = null;

        this.mimeType = null;
        this.rdfType = null;
        this.dateCreated = null;
        this.typeDescription = null;
        this.sourceName = null;

        this.shared = false;

        this.collection = false;
        this._collectionIconWatcher = null;

        this.thumbnailed = false;
        this._thumbPath = null;

        this.populateFromCursor(cursor);

        this._refreshIconId =
            Global.settings.connect('changed::view-as',
                                    Lang.bind(this, this.refreshIcon));
        this._filterId =
            Global.searchCategoryManager.connect('active-changed',
                                                 Lang.bind(this, this.refreshIcon));
    },

    refresh: function() {
        let job = new SingleItemJob(this.id);
        job.run(Query.QueryFlags.NONE, Lang.bind(this,
            function(cursor) {
                if (!cursor)
                    return;

                this.populateFromCursor(cursor);
            }));
    },

    _sanitizeTitle: function() {
        this.name = this.name.replace('Microsoft Word - ', '', 'g');
    },

    populateFromCursor: function(cursor) {
        this.uri = cursor.get_string(Query.QueryColumns.URI)[0];
        this.id = cursor.get_string(Query.QueryColumns.URN)[0];
        this.identifier = cursor.get_string(Query.QueryColumns.IDENTIFIER)[0];
        this.author = cursor.get_string(Query.QueryColumns.AUTHOR)[0];
        this.resourceUrn = cursor.get_string(Query.QueryColumns.RESOURCE_URN)[0];

        let mtime = cursor.get_string(Query.QueryColumns.MTIME)[0];
        if (mtime) {
            let timeVal = GLib.time_val_from_iso8601(mtime)[1];
            this.mtime = timeVal.tv_sec;
        } else {
            this.mtime = Math.floor(GLib.get_real_time() / 1000000);
        }

        this.mimeType = cursor.get_string(Query.QueryColumns.MIMETYPE)[0];
        this.rdfType = cursor.get_string(Query.QueryColumns.RDFTYPE)[0];
        this._updateInfoFromType();

        let dateCreated = cursor.get_string(Query.QueryColumns.DATE_CREATED)[0];
        if (dateCreated) {
            let timeVal = GLib.time_val_from_iso8601(dateCreated)[1];
            this.dateCreated = timeVal.tv_sec;
        } else {
            this.dateCreated = -1;
        }

        // sanitize
        if (!this.uri)
            this.uri = '';

        let title = cursor.get_string(Query.QueryColumns.TITLE)[0];
        let filename = cursor.get_string(Query.QueryColumns.FILENAME)[0];

        if (title && title != '')
            this.name = title;
        else if (filename)
            this.name = GdPrivate.filename_strip_extension(filename);
        else
            this.name = '';

        this._sanitizeTitle();

        this.refreshIcon();
    },

    updateIconFromType: function() {
        let icon = null;

        if (this.mimeType)
            icon = Gio.content_type_get_icon(this.mimeType);

        if (!icon)
            icon = Utils.iconFromRdfType(this.rdfType);

        let iconInfo =
            Gtk.IconTheme.get_default().lookup_by_gicon(icon, Utils.getIconSize(),
                                                        Gtk.IconLookupFlags.FORCE_SIZE |
                                                        Gtk.IconLookupFlags.GENERIC_FALLBACK);

        if (iconInfo != null) {
            try {
                this.pixbuf = iconInfo.load_icon();
            } catch (e) {
                log('Unable to load pixbuf: ' + e.toString());
            }
        }

        this.checkEffectsAndUpdateInfo();
    },

    _refreshCollectionIcon: function() {
        if (!this._collectionIconWatcher) {
            this._collectionIconWatcher = new CollectionIconWatcher(this);

            this._collectionIconWatcher.connect('icon-updated', Lang.bind(this,
                function(watcher, pixbuf) {
                    if (!pixbuf)
                        return;

                    this.pixbuf = pixbuf;
                    this.checkEffectsAndUpdateInfo();
                }));
        } else {
            this._collectionIconWatcher.refresh();
        }
    },

    refreshIcon: function() {
        if (this._thumbPath) {
            this._refreshThumbPath();
            return;
        }

        this.updateIconFromType();

        if (this.collection) {
            this._refreshCollectionIcon();
            return;
        }

        if (this._failedThumbnailing)
            return;

        if (!this._triedThumbnailing)
            this._triedThumbnailing = true;

        this._file = Gio.file_new_for_uri(this.uri);
        this._file.query_info_async(Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH,
                                    0, 0, null,
                                    Lang.bind(this, this._onFileQueryInfo));
    },

    _onFileQueryInfo: function(object, res) {
        let info = null;
        let haveNewIcon = false;

        try {
            info = object.query_info_finish(res);
        } catch (e) {
            log('Unable to query info for file at ' + this.uri + ': ' + e.toString());
            this._failedThumbnailing = true;
            return;
        }

        this._thumbPath = info.get_attribute_byte_string(Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH);
        if (this._thumbPath) {
            this._refreshThumbPath();
        } else {
            this.thumbnailed = false;

            // try to create the thumbnail
            GdPrivate.queue_thumbnail_job_for_file_async(this._file,
                                                         Lang.bind(this, this._onQueueThumbnailJob));
        }
    },

    _onQueueThumbnailJob: function(object, res) {
        let thumbnailed = GdPrivate.queue_thumbnail_job_for_file_finish(res);

        if (!thumbnailed) {
            this._failedThumbnailing = true;
            return;
        }

        // get the new thumbnail path
        this._file.query_info_async(Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH,
                                    0, 0, null,
                                    Lang.bind(this, this._onThumbnailPathInfo));
    },

    _onThumbnailPathInfo: function(object, res) {
        let info = null;

        try {
            info = object.query_info_finish(res);
        } catch (e) {
            log('Unable to query info for file at ' + this.uri + ': ' + e.toString());
            this._failedThumbnailing = true;
            return;
        }

        this._thumbPath = info.get_attribute_byte_string(Gio.FILE_ATTRIBUTE_THUMBNAIL_PATH);
        if (this._thumbPath)
            this._refreshThumbPath();
        else
            this._failedThumbnailing = true;
    },

    _refreshThumbPath: function() {
        let thumbFile = Gio.file_new_for_path(this._thumbPath);

        thumbFile.read_async(GLib.PRIORITY_DEFAULT, null, Lang.bind(this,
            function(object, res) {
                try {
                    let stream = object.read_finish(res);
                    GdkPixbuf.Pixbuf.new_from_stream_at_scale_async(stream,
                        Utils.getIconSize(), Utils.getIconSize(),
                        true, null, Lang.bind(this,
                            function(object, res) {
                                try {
                                    this.pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(res);

                                    this.thumbnailed = true;
                                    this.checkEffectsAndUpdateInfo();
                                } catch (e) {
                                    this._failedThumbnailing = true;
                                }

                                // close the underlying stream immediately
                                stream.close_async(0, null, null);
                            }));
                } catch (e) {
                    this._failedThumbnailing = true;
                }
            }));
    },

    _updateInfoFromType: function() {
        if (this.rdfType.indexOf('nfo#DataContainer') != -1)
            this.collection = true;

        this.updateTypeDescription();
    },

    _createSymbolicEmblem: function(name) {
        let pix = Gd.create_symbolic_icon(name, Utils.getIconSize());

        if (!pix)
            pix = new Gio.ThemedIcon({ name: name });

        return pix;
    },

    checkEffectsAndUpdateInfo: function() {
        let emblemIcons = [];
        let pixbuf = this.pixbuf;
        let activeItem;

        // save the pixbuf before modifications, to use in collection icons
        this.pristinePixbuf = pixbuf;

        activeItem = Global.searchCategoryManager.getActiveItem();

        if (this.shared &&
            (!activeItem ||
             (activeItem.id != Searchbar.SearchCategoryStock.SHARED)))
            emblemIcons.push(this._createSymbolicEmblem('emblem-shared'));

        if (emblemIcons.length > 0) {
            let emblemedIcon = new Gio.EmblemedIcon({ gicon: this.pixbuf });

            emblemIcons.forEach(
                function(emblemIcon) {
                    let emblem = new Gio.Emblem({ icon: emblemIcon });
                    emblemedIcon.add_emblem(emblem);
                });

            let theme = Gtk.IconTheme.get_default();

            try {
                let iconInfo = theme.lookup_by_gicon(emblemedIcon,
                                                     Math.max(this.pixbuf.get_width(),
                                                              this.pixbuf.get_height()),
                                                     Gtk.IconLookupFlags.FORCE_SIZE);

                pixbuf = iconInfo.load_icon();
            } catch (e) {
                log('Unable to render the emblem: ' + e.toString());
            }
        }

        if (this.thumbnailed) {
            let [ slice, border ] = Utils.getThumbnailFrameBorder();
            this.pixbuf = GdPrivate.embed_image_in_frame(pixbuf,
                Path.ICONS_DIR + 'thumbnail-frame.png',
                slice, border);
        } else {
            this.pixbuf = pixbuf;
        }

        this.emit('info-updated');
    },

    destroy: function() {
        if (this._collectionIconWatcher) {
            this._collectionIconWatcher.destroy();
            this._collectionIconWatcher = null;
        }

        Global.settings.disconnect(this._refreshIconId);
        Global.searchCategoryManager.disconnect(this._filterId);
    },

    open: function(screen, timestamp) {
        try {
            Gtk.show_uri(screen, this.uri, timestamp);
        } catch (e) {
            log('Unable to show URI ' + this.uri + ': ' + e.toString());
        }
    },

    print: function(toplevel) {
        this.load(null, Lang.bind(this,
            function(doc, docModel, error) {
                if (error) {
                    log('Unable to print document ' + this.uri + ': ' + error);
                    return;
                }

                let printOp = EvView.PrintOperation.new(docModel.get_document());
                let printNotification = new Notifications.PrintNotification(printOp, doc);

                printOp.run(toplevel);
            }));
    },

    getWhere: function() {
        let retval = '';

        if (this.collection)
            retval = '{ ?urn nie:isPartOf <' + this.id + '> }';

        return retval;
    }
});
Signals.addSignalMethods(DocCommon.prototype);

const LocalDocument = new Lang.Class({
    Name: 'LocalDocument',
    Extends: DocCommon,

    _init: function(cursor) {
        this._failedThumbnailing = false;
        this._triedThumbnailing = false;

        this.parent(cursor);

        this.sourceName = _("Local");

        let defaultApp = null;
        if (this.mimeType)
            defaultApp = Gio.app_info_get_default_for_type(this.mimeType, true);

        if (defaultApp)
            this.defaultAppName = defaultApp.get_name();
    },

    updateTypeDescription: function() {
        if (this.mimeType)
            this.typeDescription = Gio.content_type_get_description(this.mimeType);
    },

    load: function(cancellable, callback) {
        GdPrivate.pdf_loader_load_uri_async(this.uri, cancellable, Lang.bind(this,
            function(source, res) {
                try {
                    let docModel = GdPrivate.pdf_loader_load_uri_finish(res);
                    callback(this, docModel, null);
                } catch (e) {
                    callback(this, null, e);
                }
            }));
    },

    canTrash: function() {
        return this.collection;
    },

    trash: function() {
        if (this.collection) {
            let job = new DeleteItemJob(this.id);
            job.run(null);
        }
    }
});

const _GOOGLE_DOCS_SCHEME_LABELS = "http://schemas.google.com/g/2005/labels";
const _GOOGLE_DOCS_TERM_STARRED = "http://schemas.google.com/g/2005/labels#starred";

const GoogleDocument = new Lang.Class({
    Name: 'GoogleDocument',
    Extends: DocCommon,

    _init: function(cursor) {
        this._triedThumbnailing = true;
        this._failedThumbnailing = true;

        this.parent(cursor);

        // overridden
        this.defaultAppName = _("Google Docs");
        this.sourceName = _("Google");
    },

    _createGDataEntry: function(cancellable, callback) {
        let source = Global.sourceManager.getItemById(this.resourceUrn);

        let authorizer = new GData.GoaAuthorizer({ goa_object: source.object });
        let service = new GData.DocumentsService({ authorizer: authorizer });

        service.query_single_entry_async
            (service.get_primary_authorization_domain(),
             this.identifier, null,
             GData.DocumentsText,
             cancellable, Lang.bind(this,
                 function(object, res) {
                     let entry = null;
                     let exception = null;

                     try {
                         entry = object.query_single_entry_finish(res);
                     } catch (e) {
                         exception = e;
                     }

                     callback(entry, service, exception);
                 }));
    },

    load: function(cancellable, callback) {
        this._createGDataEntry(cancellable, Lang.bind(this,
            function(entry, service, exception) {
                if (exception) {
                    // try loading from the most recent cache, if any
                    GdPrivate.pdf_loader_load_uri_async(this.identifier, cancellable, Lang.bind(this,
                        function(source, res) {
                            try {
                                let docModel = GdPrivate.pdf_loader_load_uri_finish(res);
                                callback(this, docModel, null);
                            } catch (e) {
                                // report the outmost error only
                                callback(this, null, exception);
                            }
                        }));

                    return;
                }

                GdPrivate.pdf_loader_load_gdata_entry_async
                    (entry, service, cancellable, Lang.bind(this,
                        function(source, res) {
                            try {
                                let docModel = GdPrivate.pdf_loader_load_uri_finish(res);
                                callback(this, docModel, null);
                            } catch (e) {
                                callback(this, null, e);
                            }
                        }));
            }));
    },

    updateTypeDescription: function() {
        let description;

        if (this.rdfType.indexOf('nfo#Spreadsheet') != -1)
            description = _("Spreadsheet");
        else if (this.rdfType.indexOf('nfo#Presentation') != -1)
            description = _("Presentation");
        else if (this.rdfType.indexOf('nfo#DataContainer') != -1)
            description = _("Collection");
        else
            description = _("Document");

        this.typeDescription = description;
    },

    populateFromCursor: function(cursor) {
        this.shared = cursor.get_boolean(Query.QueryColumns.SHARED);

        this.parent(cursor);
    },

    canTrash: function() {
        return false;
    }
});

const SkydriveDocument = new Lang.Class({
    Name: 'SkydriveDocument',
    Extends: DocCommon,

    _init: function(cursor) {
        this._triedThumbnailing = true;
        this._failedThumbnailing = true;

        this.parent(cursor);

        // overridden
        this.defaultAppName = _("Skydrive");
        this.sourceName = _("Skydrive");
    },

    _createZpjEntry: function(cancellable, callback) {
        let source = Global.sourceManager.getItemById(this.resourceUrn);

        let authorizer = new Zpj.GoaAuthorizer({ goa_object: source.object });
        let service = new Zpj.Skydrive({ authorizer: authorizer });

        const zpj_prefix = "windows-live:skydrive:";
        let zpj_id = this.identifier.substring(zpj_prefix.length);

        service.query_info_from_id_async
            (zpj_id, cancellable,
             Lang.bind(this,
                 function(object, res) {
                     let entry = null;
                     let exception = null;

                     try {
                         entry = object.query_info_from_id_finish(res);
                     } catch (e) {
                         exception = e;
                     }

                     callback(entry, service, exception);
                 }));
    },

    load: function(cancellable, callback) {
        this._createZpjEntry(cancellable, Lang.bind(this,
            function(entry, service, exception) {
                if (exception) {
                    // try loading from the most recent cache, if any
                    GdPrivate.pdf_loader_load_uri_async(this.identifier, cancellable, Lang.bind(this,
                        function(source, res) {
                            try {
                                let docModel = GdPrivate.pdf_loader_load_uri_finish(res);
                                callback(this, docModel, null);
                            } catch (e) {
                                // report the outmost error only
                                callback(this, null, exception);
                            }
                        }));

                    return;
                }

                GdPrivate.pdf_loader_load_zpj_entry_async
                    (entry, service, cancellable, Lang.bind(this,
                        function(source, res) {
                            try {
                                let docModel = GdPrivate.pdf_loader_load_zpj_entry_finish(res);
                                callback(this, docModel, null);
                            } catch (e) {
                                callback(this, null, e);
                            }
                        }));
            }));
    },

    updateTypeDescription: function() {
        let description;

        if (this.rdfType.indexOf('nfo#Spreadsheet') != -1)
            description = _("Spreadsheet");
        else if (this.rdfType.indexOf('nfo#Presentation') != -1)
            description = _("Presentation");
        else if (this.rdfType.indexOf('nfo#DataContainer') != -1)
            description = _("Collection");
        else
            description = _("Document");

        this.typeDescription = description;
    },

    canTrash: function() {
        return false;
    }
});

const DocumentManager = new Lang.Class({
    Name: 'DocumentManager',
    Extends: Manager.BaseManager,

    _init: function() {
        this.parent();

        this._activeDocModel = null;
        this._activeDocModelIds = [];
        this._loaderCancellable = null;

        // a stack containing the collections which were used to
        // navigate to the active document or collection
        this._collectionPath = [];

        Global.changeMonitor.connect('changes-pending',
                                     Lang.bind(this, this._onChangesPending));
    },

    _onChangesPending: function(monitor, changes) {
        for (idx in changes) {
            let changeEvent = changes[idx];

            if (changeEvent.type == ChangeMonitor.ChangeEventType.CHANGED) {
                let doc = this.getItemById(changeEvent.urn);

                if (doc)
                    doc.refresh();
            } else if (changeEvent.type == ChangeMonitor.ChangeEventType.CREATED) {
                this._onDocumentCreated(changeEvent.urn);
            } else if (changeEvent.type == ChangeMonitor.ChangeEventType.DELETED) {
                let doc = this.getItemById(changeEvent.urn);

                if (doc) {
                    doc.destroy();
                    this.removeItemById(changeEvent.urn);

                    if (doc.collection)
                        Global.collectionManager.removeItemById(changeEvent.urn);
                }
            }
        }
    },

    _onDocumentCreated: function(urn) {
        let job = new SingleItemJob(urn);
        job.run(Query.QueryFlags.NONE, Lang.bind(this,
            function(cursor) {
                if (!cursor)
                    return;

                this.addDocumentFromCursor(cursor);
            }));
    },

    _identifierIsGoogle: function(identifier) {
        return (identifier &&
                (identifier.indexOf('https://docs.google.com') != -1));
    },

    _identifierIsSkydrive: function(identifier) {
        return (identifier &&
                (identifier.indexOf('windows-live:skydrive:') != -1));
    },

    createDocumentFromCursor: function(cursor) {
        let identifier = cursor.get_string(Query.QueryColumns.IDENTIFIER)[0];
        let doc;

        if (this._identifierIsGoogle(identifier))
            doc = new GoogleDocument(cursor);
        else if (this._identifierIsSkydrive(identifier))
            doc = new SkydriveDocument(cursor);
        else
            doc = new LocalDocument(cursor);

        return doc;
    },

    addDocumentFromCursor: function(cursor) {
        let doc = this.createDocumentFromCursor(cursor);
        this.addItem(doc);

        if (doc.collection)
            Global.collectionManager.addItem(doc);

        return doc;
    },

    clear: function() {
        let items = this.getItems();
        for (idx in items) {
            items[idx].destroy();
        };

        this.parent();
    },

    _onDocumentLoadError: function(doc, error) {
        if (error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
            return;

        // Translators: %s is the title of a document
        let message = _("Unable to load \"%s\" for preview").format(doc.name);
        this.emit('load-error', doc, message, error);
    },

    _onDocumentLoaded: function(doc, docModel, error) {
        if (error) {
            this._onDocumentLoadError(doc, error);
            return;
        }

        // save loaded model and signal
        this._activeDocModel = docModel;
        this.emit('load-finished', doc, docModel);

        // load metadata
        this._connectMetadata(docModel);
    },

    setActiveItem: function(doc) {
        if (!this.parent(doc))
            return;

        // cleanup any state we have for previously loaded model
        this._clearActiveDocModel();

        if (!doc)
            return;

        if (doc.collection) {
            this._collectionPath.push(Global.collectionManager.getActiveItem());
            Global.collectionManager.setActiveItem(doc);
            return;
        }

        let recentManager = Gtk.RecentManager.get_default();
        recentManager.add_item(doc.uri);

        this._loaderCancellable = new Gio.Cancellable();
        doc.load(this._loaderCancellable, Lang.bind(this, this._onDocumentLoaded));
        this.emit('load-started', doc);
    },

    activatePreviousCollection: function() {
        this._clearActiveDocModel();
        Global.collectionManager.setActiveItem(this._collectionPath.pop());
    },

    _clearActiveDocModel: function() {
        // cancel any pending load operation
        if (this._loaderCancellable) {
            this._loaderCancellable.cancel();
            this._loaderCancellable = null;
        }

        // clear any previously loaded document model
        if (this._activeDocModel) {
            this._activeDocModelIds.forEach(Lang.bind(this,
                function(id) {
                    this._activeDocModel.disconnect(id);
                }));

            this._activeDocModel = null;
            this._activeDocModelIds = [];
        }
    },

    _connectMetadata: function(docModel) {
        let evDoc = docModel.get_document();
        let file = Gio.File.new_for_uri(evDoc.get_uri());

        if (!GdPrivate.is_metadata_supported_for_file(file))
            return;

        let metadata = new GdPrivate.Metadata({ file: file });

        // save current page in metadata
        let [res, val] = metadata.get_int('page');
        if (res)
            docModel.set_page(val);
        this._activeDocModelIds.push(
            docModel.connect('page-changed', Lang.bind(this,
                function(source, oldPage, newPage) {
                    metadata.set_int('page', newPage);
                }))
        );
    }
});
