/*
 * Copyright (c) 2011 Red Hat, Inc.
 *
 * GNOME Books 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 Books 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 Gd = imports.gi.Gd;
const GdPrivate = imports.gi.GdPrivate;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Lang = imports.lang;
const Search = imports.search;

var QueryColumns = {
    URN: 0,
    URI: 1,
    FILENAME: 2,
    MIMETYPE: 3,
    TITLE: 4,
    AUTHOR: 5,
    MTIME: 6,
    IDENTIFIER: 7,
    RDFTYPE: 8,
    RESOURCE_URN: 9,
    SHARED: 10,
    DATE_CREATED: 11
};

var QueryFlags = {
    NONE: 0,
    UNFILTERED: 1 << 0,
    COLLECTIONS: 1 << 1,
    DOCUMENTS: 1 << 2,
    SEARCH: 1 << 3
};

var LOCAL_BOOKS_COLLECTIONS_IDENTIFIER = 'gb:collection:local:';

var QueryBuilder = new Lang.Class({
    Name: 'QueryBuilder',

    _init: function(context) {
        this._context = context;
    },

    _createQuery: function(sparql) {
        return { sparql: sparql,
                 activeSource: this._context.sourceManager.getActiveItem() };
    },

    _buildFilterString: function(currentType, flags, isFtsEnabled) {
        let filters = [];

        if (!isFtsEnabled)
            filters.push(this._context.searchMatchManager.getFilter(flags));
        filters.push(this._context.sourceManager.getFilter(flags));

        if (currentType) {
            filters.push(currentType.getFilter());
        }

        return 'FILTER (' + filters.join(' && ') + ')';
    },

    _buildOptional: function() {
        let sparql =
            'OPTIONAL { ?urn nie:isStoredAs ?file . } ';
            'OPTIONAL { ?urn nco:creator ?creator . } ' +
            'OPTIONAL { ?urn nco:publisher ?publisher . } ';

        return sparql;
    },

    _addWhereClauses: function(partsList, global, flags, searchTypes, ftsQuery) {
        // build an array of WHERE clauses; each clause maps to one
        // type of resource we're looking for.
        searchTypes.forEach(Lang.bind(this,
            function(currentType) {
                let part = '{ ' + currentType.getWhere() + ftsQuery;
                part += this._buildOptional();

                if ((flags & QueryFlags.UNFILTERED) == 0) {
                    part += this._buildFilterString(currentType, flags, ftsQuery.length > 0);
                }

                part += ' }';
                partsList.push(part);
            }));
    },

    _buildWhere: function(global, flags) {
        let whereSparql = 'WHERE { ';
        let whereParts = [];
        let searchTypes = [];

        if (flags & QueryFlags.COLLECTIONS)
            searchTypes = [this._context.searchTypeManager.getItemById(Search.SearchTypeStock.COLLECTIONS)];
        else if (flags & QueryFlags.DOCUMENTS)
            searchTypes = this._context.searchTypeManager.getDocumentTypes();
        else if (flags & QueryFlags.SEARCH)
            searchTypes = this._context.searchTypeManager.getCurrentTypes();
        else
            searchTypes = this._context.searchTypeManager.getAllTypes();

        let matchItem = this._context.searchMatchManager.getActiveItem();

        // Skip matchTypes when only doing fts
        if (matchItem.id != Search.SearchMatchStock.CONTENT) {
            this._addWhereClauses(whereParts, global, flags, searchTypes, '');
        }

        if (flags & QueryFlags.SEARCH) {
            let ftsWhere = this._context.searchMatchManager.getWhere();

            // Need to repeat the searchTypes part to also include fts
            // Note that the filter string needs to be slightly different for the
            // fts to work properly
            if (ftsWhere.length || matchItem.id == Search.SearchMatchStock.CONTENT) {
                this._addWhereClauses(whereParts, global, flags, searchTypes, ftsWhere);
            }
        }

        // put all the clauses in an UNION
        whereSparql += whereParts.join(' UNION ');

        whereSparql += ' }';

        return whereSparql;
    },

    _buildQueryInternal: function(global, flags, offsetController, sortBy) {
	let selectClauses =
            '    (nie:isStoredAs(?urn) AS ?uri) ' +
            '    (COALESCE (nfo:fileName(?file), tracker:string-from-filename(nie:isStoredAs(?urn))) AS ?filename) ' +
            '    (nie:mimeType(?urn) AS ?mimetype) ' +
            '    (nie:title(?urn) AS ?title) ' +
            '    (tracker:coalesce(nco:fullname(?creator), nco:fullname(?publisher), \'\') AS ?author) ' +
            '    (nie:contentLastModified(?urn) AS ?mtime) ' +
            '    (nao:identifier(?urn) AS ?identifier) ' +
            '    (rdf:type(?urn) AS ?type) ' +
            '    (nie:dataSource(?urn) AS ?datasource ) ' +
            '    (( EXISTS { ?urn nco:contributor ?contributor FILTER ( ?contributor != ?creator ) } ) AS ?shared) ' +
            '    (nie:contentCreated(?urn) AS ?created) ';
        let whereSparql = this._buildWhere(global, flags);
        let tailSparql = '';

        // order results depending on sortBy
        if (global) {
            let offset = 0;
            let step = Search.OFFSET_STEP;

            tailSparql += 'ORDER BY DESC(fts:rank(?urn)) ';

            if (offsetController) {
                offset = offsetController.getOffset();
                step = offsetController.getOffsetStep();
            }

            switch (sortBy) {
            case Gd.MainColumns.PRIMARY_TEXT:
                tailSparql += 'ASC(tracker:coalesce(?title, ?filename))';
                break;
            case Gd.MainColumns.SECONDARY_TEXT:
                tailSparql += 'ASC(?author)';
                break;
            case Gd.MainColumns.MTIME:
                tailSparql += 'DESC(?mtime)';
                break;
            default:
                tailSparql += 'DESC(?mtime)';
                break;
            }

            tailSparql += ('LIMIT %d OFFSET %d').format(step, offset);
        }

        let sparql =
            'SELECT ?urn ' +
            '  ?uri ' +
            '  ?filename ' +
            '  ?mimetype ' +
            '  COALESCE (?localTitle, ?title) ' +
            '  ?author ' +
            '  ?mtime ' +
            '  ?identifier ' +
            '  ?type ' +
            '  ?datasource ' +
            '  ?shared ' +
            '  ?created ' +
            'WHERE { ';

        // Collections queries are local
        if (flags & QueryFlags.COLLECTIONS) {
            sparql +=
                'SELECT DISTINCT ?urn ' +
                selectClauses +
                whereSparql +
                tailSparql;
        } else {
            sparql +=
                'SERVICE <dbus:' + this._context.trackerMinerService + '> {' +
                '  GRAPH tracker:Documents { ' +
                '    SELECT DISTINCT ?urn ' +
                selectClauses +
                whereSparql +
                tailSparql +
                '  }' +
                '}' +
                'OPTIONAL { ?urn nie:title ?localTitle } . ';

                if (global && (flags & QueryFlags.UNFILTERED) == 0)
                    sparql += this._context.documentManager.getWhere();
        }

        sparql += '}';

        return sparql;
    },

    buildSingleQuery: function(flags, resource) {
        let sparql = this._buildQueryInternal(false, flags, null);
        sparql = sparql.replace(/\?urn/g, '<' + resource + '>');

        return this._createQuery(sparql);
    },

    buildGlobalQuery: function(flags, offsetController, sortBy) {
        return this._createQuery(this._buildQueryInternal(true, flags, offsetController, sortBy));
    },

    buildCountQuery: function(flags) {
	let sparql;
	if (flags & QueryFlags.COLLECTIONS) {
	    sparql = 'SELECT DISTINCT COUNT(?urn) AS ?c ' +
		this._buildWhere(true, flags);
	} else {
	    sparql = 'SELECT ?c {' +
		'  SERVICE <dbus:' + this._context.trackerMinerService + '> { ' +
		'    SELECT DISTINCT COUNT(?urn) AS ?c ' +
		this._buildWhere(true, flags) +
		'  }' +
		'}';
	}

        return this._createQuery(sparql);
    },

    // queries for all the items which are part of the given collection
    buildCollectionIconQuery: function(resource) {
        let sparql =
            ('SELECT ' +
             '?urn ' +
             'nie:contentLastModified(?urn) AS ?mtime ' +
             'WHERE { ?urn nie:isLogicalPartOf ?collUrn } ' +
             'ORDER BY DESC (?mtime)' +
             'LIMIT 4').replace(/\?collUrn/, '<' + resource + '>');

        return this._createQuery(sparql);
    },

    // queries for all the collections the given item is part of
    buildFetchCollectionsQuery: function(resource) {
        let sparql =
            ('SELECT ' +
             '?urn ' +
             'WHERE { ?urn a nfo:DataContainer . ?docUrn nie:isLogicalPartOf ?urn }'
            ).replace(/\?docUrn/, '<' + resource + '>');

        return this._createQuery(sparql);
    },

    // adds or removes the given item to the given collection
    buildSetCollectionQuery: function(itemUrn, collectionUrn, setting) {
        let sparql;
        if (setting) {
            sparql = ('INSERT DATA { <%s> a nie:InformationElement; nie:isLogicalPartOf <%s> }'
                     ).format(itemUrn, collectionUrn);
        } else {
            sparql = ('DELETE DATA { <%s> nie:isLogicalPartOf <%s> }'
                     ).format(itemUrn, collectionUrn);
        }
        return this._createQuery(sparql);
    },

    // bumps the mtime to current time for the given resource
    buildUpdateMtimeQuery: function(resource) {
        let time = GdPrivate.iso8601_from_timestamp(GLib.get_real_time() / GLib.USEC_PER_SEC);
        let sparql = ('INSERT OR REPLACE { <%s> a nie:InformationElement ; nie:contentLastModified \"%s\" }'
                     ).format(resource, time);

        return this._createQuery(sparql);
    },

    buildCreateCollectionQuery: function(name) {
        let application = Gio.Application.get_default();
        let collectionsIdentifier;
        collectionsIdentifier = LOCAL_BOOKS_COLLECTIONS_IDENTIFIER;

        let time = GdPrivate.iso8601_from_timestamp(GLib.get_real_time() / GLib.USEC_PER_SEC);
        let sparql = ('INSERT { _:res a nfo:DataContainer ; a nie:DataObject ; ' +
                      'nie:contentLastModified \"' + time + '\" ; ' +
                      'nie:title \"' + name + '\" ; ' +
                      'nao:identifier \"' + collectionsIdentifier + name + '\" }');

        return this._createQuery(sparql);
    },

    buildDeleteResourceQuery: function(resource) {
        let sparql = ('DELETE { <%s> a rdfs:Resource }').format(resource);

        return this._createQuery(sparql);
    }
});
