// vim: ts=4 sw=4 expandtab cindent foldmethod=marker
//
// BEGIN FLOCK GPL
// 
// Copyright Flock Inc. 2005-2006
// http://flock.com
// 
// This file may be used under the terms of of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
// 
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
// 
// END FLOCK GPL
//

function DEBUG(X) { }
//function DEBUG(X) { debug ('flockFavoritesQueue: '+X+'\n'); }

// interfaces {{{
const flockIFavoritesQueue       = Components.interfaces.flockIFavoritesQueue;
const flockIFavoritesChange      = Components.interfaces.flockIFavoritesChange;
const flockIFavoritesChangeFavorite = Components.interfaces.flockIFavoritesChangeFavorite;
const flockIFavoritesChangeCollection = Components.interfaces.flockIFavoritesChangeCollection;
const nsISupports                = Components.interfaces.nsISupports;
const nsITimer                   = Components.interfaces.nsITimer;
const nsIRDFDataSource           = Components.interfaces.nsIRDFDataSource;
const nsIRDFObserver             = Components.interfaces.nsIRDFObserver;
const nsIRDFService              = Components.interfaces.nsIRDFService;
const nsIRDFContainerUtils       = Components.interfaces.nsIRDFContainerUtils;
const nsIRDFContainer            = Components.interfaces.nsIRDFContainer;
const nsIRDFNode                 = Components.interfaces.nsIRDFNode;
const nsIRDFResource             = Components.interfaces.nsIRDFResource;
const nsIRDFLiteral              = Components.interfaces.nsIRDFLiteral;
const nsIRDFInt                  = Components.interfaces.nsIRDFInt;
const nsIRDFDate                 = Components.interfaces.nsIRDFDate;
const nsIRDFBlob                 = Components.interfaces.nsIRDFBlob;
const nsIProperties              = Components.interfaces.nsIProperties;
const nsIPrefBranch              = Components.interfaces.nsIPrefBranch;
const nsILocalFile               = Components.interfaces.nsILocalFile;
const nsIRDFRemoteDataSource     = Components.interfaces.nsIRDFRemoteDataSource;
const nsIObserverService         = Components.interfaces.nsIObserverService;
// }}}

// contract ids {{{
const MEMORY_DS_CONTRACTID      = '@mozilla.org/rdf/datasource;1?name=in-memory-datasource';
const RDFSERVICE_CONTRACTID     = '@mozilla.org/rdf/rdf-service;1';
const RDFCONTUTILS_CONTRACTID   = '@mozilla.org/rdf/container-utils;1';
const CONTAINER_CONTRACTID      = '@mozilla.org/rdf/container;1';
const DIRECTORY_SVC_CONTRACTID  = '@mozilla.org/file/directory_service;1';
const LOCAL_FILE_CONTRACTID     = '@mozilla.org/file/local;1';
const PREFERENCES_CONTRACTID    = '@mozilla.org/preferences-service;1';
const WINDOWWATCHER_CONTRACTID  = '@mozilla.org/embedcomp/window-watcher;1';
const TIMER_CONTRACTID          = '@mozilla.org/timer;1';
const FFU_CONTRACTID            = '@flock.com/favorites/utils;1';
const OBSERVICE_CONTRACTID      = '@mozilla.org/observer-service;1';
/// }}}

// services/*{{{*/
const RDFS = Components.classes[RDFSERVICE_CONTRACTID].getService (nsIRDFService);
const RDFCU = Components.classes[RDFCONTUTILS_CONTRACTID].getService (nsIRDFContainerUtils);
const DIRS = Components.classes[DIRECTORY_SVC_CONTRACTID].getService (nsIProperties);
const PREFS = Components.classes[PREFERENCES_CONTRACTID].getService (nsIPrefBranch);
/*}}}*/

// rdf namespaces/*{{{*/
function Namespace (ns) { return function (arg) { return RDFS.GetResource (ns+arg); } }
const W3RDF                     = Namespace ('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
const NSRDF                     = Namespace ('http://home.netscape.com/NC-rdf#');
const WBRDF                     = Namespace ('http://home.netscape.com/WEB-rdf#');
const FLRDF                     = Namespace ('http://flock.com/rdf#');/*}}}*/
const COLRDF                    = Namespace ('urn:flock:collection:');

// rdf properties/*{{{*/
const RDF_URL                   = NSRDF ('URL');
const RDF_TAGS                  = FLRDF ('tags');
const RDF_TAG                   = FLRDF ('tag');
const RDF_WSID                  = FLRDF ('wsid');
const RDF_NAME                  = NSRDF ('Name');
const RDF_PERMISSIONS           = FLRDF ('permissions');
const RDF_TYPE                  = W3RDF ('type');
const RDF_FAVORITE              = FLRDF ('Favorite');
const RDF_COLLECTION            = FLRDF ('Collection');
const RDF_LIVEMARK              = NSRDF ('Livemark');
const RDF_FEEDURL               = NSRDF ('FeedURL');
const RDF_FEEDENABLED           = FLRDF ('feedEnabled');
const RDF_SHORTCUTURL           = NSRDF ('ShortcutURL');
const RDF_DESCRIPTION           = NSRDF ('Description');
const RDF_NSYNC                 = FLRDF ('nsync');
const RDF_LOCAL                 = FLRDF ('local');
const RDF_LASTMODIFIEDDATE      = WBRDF ('LastModifiedDate');
const RDF_LASTVISITDATE         = WBRDF ('LastVisitDate');
const RDF_ADDEDDATE             = FLRDF ('FavoriteAddDate');
const RDF_INSTANCEOF            = W3RDF ('instanceOf');
const RDF_SEQ                   = W3RDF ('Seq');
const RDF_FEED                  = FLRDF ('feed');
const RDF_TOOLBAR               = FLRDF ('toolbar');
const RDF_STORE                 = FLRDF ('store');
const RDF_VERSION               = FLRDF ('version');
const RDF_QUEUEROOT             = FLRDF ('QueueRoot');

const RDF_DELETE                = FLRDF ('delete');
/*}}}*/

function rdfLiteral (s) {
    return RDFS.GetLiteral (s);
}

function rdfBoolean (b) {
    return rdfLiteral (b?"true":"false");
}

const DIRECTION_TO_WEB_SERVICE = flockIFavoritesQueue.DIRECTION_TO_WEB_SERVICE;
const DIRECTION_FROM_WEB_SERVICE = flockIFavoritesQueue.DIRECTION_FROM_WEB_SERVICE;

const FLUSH_PERIOD = 5; // flush after five seconds

function loadSubScript(spec)
{
    var loader = Components.classes['@mozilla.org/moz/jssubscript-loader;1']
        .getService(Components.interfaces.mozIJSSubScriptLoader);
    var context = {};
    loader.loadSubScript(spec, context);
    return context;
}
var flockFavoritesUtils = new (loadSubScript('chrome://browser/content/flock/favorites/flockFavoritesUtils.js').flockFavoritesUtils)();


function flockFavoritesQueue () { // {{{
    this.mName = null;
    this.mFlushTimer = null;

    var obService = Components.classes[OBSERVICE_CONTRACTID].getService (nsIObserverService);
    obService.addObserver( 
            {service: this, 
             observe: function(aSubject, aTopic, aData) 
                { this.service.Flush(); }
            }, 'quit-application-requested', false);

    
} // }}}
flockFavoritesQueue.prototype.QueryInterface = function (aIID) { // {{{
    if (aIID.equals (flockIFavoritesQueue) || aIID.equals (nsISupports)) {
        return this;
    }
    throw Components.results.NS_ERROR_NO_INTERFACE;
} // }}}
flockFavoritesQueue.prototype.Flush = function () { // {{{
    if (this.mFlushTimer) {
        this.mFlushTimer.cancel ();
        this.mFlushTimer = null;
    }
    this.mIn.Flush ();
    this.mOut.Flush ();
} // }}}
flockFavoritesQueue.prototype._Flush = function () { // {{{
    // schedules a flush
    if (!this.mFlushTimer) {
        this.mFlushTimer = Components.classes[TIMER_CONTRACTID]
            .createInstance (nsITimer);
        this.mFlushTimer.initWithCallback (this, FLUSH_PERIOD*1000,
                nsITimer.TYPE_ONE_SHOT);
    }
} // }}}
// the nsITimerCallback implementation {{{
flockFavoritesQueue.prototype.notify = function (aTimer) {
    if (aTimer == this.mFlushTimer && this.mFlushTimer != null) {
        this.Flush ()
    }
} // }}}
flockFavoritesQueue.prototype.root = function (aDirection) { // private {{{
    return [this.mOutRoot,this.mInRoot][aDirection?1:0];
} // }}}
flockFavoritesQueue.prototype.ds = function (aDirection) { // private {{{
    return [this.mOut,this.mIn][aDirection?1:0];
} // }}}
flockFavoritesQueue.prototype.init = function (aName) { // {{{
    // create a datasource in the profile directory
    var file_in = Components.classes[LOCAL_FILE_CONTRACTID]
        .createInstance (nsILocalFile);
    var file_out = Components.classes[LOCAL_FILE_CONTRACTID]
        .createInstance (nsILocalFile);
    try {
        var profd = DIRS.get ('ProfD', nsILocalFile).path;
        //debug ('profile dir=\"'+profd+'\"\n');

        file_in.initWithPath (profd);
        file_out.initWithPath (profd);
    } catch (e) { 
        // no profile dir I guess - throw it in /tmp
        file_in.initWithPath ('/tmp/');
        file_out.initWithPath ('/tmp/');
    }
    file_in.append ('flock_fq_'+aName+'_in.rdf');
    file_out.append ('flock_fq_'+aName+'_out.rdf');

    var ios = Components.classes['@mozilla.org/network/io-service;1']
        .getService(Components.interfaces.nsIIOService);

    var fileHandler = ios.getProtocolHandler('file')
       .QueryInterface(Components.interfaces.nsIFileProtocolHandler);

    var spec_in = fileHandler.getURLSpecFromFile(file_in);
    var spec_out = fileHandler.getURLSpecFromFile(file_out);

    this.mIn=RDFS.GetDataSourceBlocking (spec_in)
        .QueryInterface (nsIRDFRemoteDataSource);
    this.mOut=RDFS.GetDataSourceBlocking (spec_out)
        .QueryInterface (nsIRDFRemoteDataSource);

    //debug ('persiting favorites queue at '+file_in.path+' and '+ file_out.path+'\n');

    this.mInRoot=RDFCU.MakeSeq (this.mIn, RDF_QUEUEROOT);
    this.mOutRoot=RDFCU.MakeSeq (this.mOut, RDF_QUEUEROOT);
} // }}}
flockFavoritesQueue.prototype.clear = function () { // {{{
    DEBUG ('clear ()');
    // I could do this nice and efficiently, but I think I'll just hack it quickly.
    // this will be called infrequently, methinks
    while (this.nextChange (DIRECTION_TO_WEB_SERVICE));
    while (this.nextChange (DIRECTION_FROM_WEB_SERVICE));
} // }}}
flockFavoritesQueue.prototype.changeFavorite = function (aURL, aProperties, aDirection) { // {{{
    DEBUG ('changeFavorite ('+aURL+', '+aProperties+', '+aDirection+')\n');
    // this next line could be slowish
    //DEBUG ('properties: '+flockFavoritesUtils.propertiesToString (aProperties)+'\n');
    var our_root = this.root (aDirection);
    var their_root = this.root (!aDirection);
    var r = RDFS.GetResource (aURL);

    // add this change
    if (our_root.IndexOf (r) == -1) {
        // if the change isn't in the root yet, add it
        our_root.AppendElement (r);
    } else {
        // we need to remove the old change
        flockFavoritesUtils.removeProperties (this.ds (aDirection), r);
        var ds = this.ds (aDirection);
        if (ds.HasAssertion (r, RDF_DELETE, rdfBoolean (true), true)) {
            ds.Unassert (r, RDF_DELETE, rdfBoolean (true));
        }
    }

    // remove any change on the other side
    if (their_root.IndexOf (r) != -1) {
        flockFavoritesUtils.removeProperties (this.ds (!aDirection), r);
        var ds = this.ds (!aDirection);
        if (ds.HasAssertion (r, RDF_DELETE, rdfBoolean (true), true)) {
            ds.Unassert (r, RDF_DELETE, rdfBoolean (true));
        }
        // remove the change from the other queue
        their_root.RemoveElement (r, true);
    }

    flockFavoritesUtils.setProperties (this.ds (aDirection), r, aProperties, 0);
    this._Flush ();
} // }}}
flockFavoritesQueue.prototype.deleteFavorite = function (aURL, aProperties, aDirection) { // {{{
    DEBUG ('deleteFavorite ('+aURL+', '+aProperties+', '+aDirection+')\n');
    var our_root = this.root (aDirection);
    var their_root = this.root (!aDirection);
    var r = RDFS.GetResource (aURL);

    // add this change
    if (our_root.IndexOf (r) == -1) {
        // if the change isn't in the root yet, add it
        our_root.AppendElement (r);
    } else {
        // we need to remove the old change
        flockFavoritesUtils.removeProperties (this.ds (aDirection), r);
        var ds = this.ds (aDirection);
        if (ds.HasAssertion (r, RDF_DELETE, rdfBoolean (true), true)) {
            ds.Unassert (r, RDF_DELETE, rdfBoolean (true));
        }
    }

    // remove any change on the other side
    if (their_root.IndexOf (r) != -1) {
        flockFavoritesUtils.removeProperties (this.ds (!aDirection), r);
        var ds = this.ds (!aDirection);
        if (ds.HasAssertion (r, RDF_DELETE, rdfBoolean (true), true)) {
            ds.Unassert (r, RDF_DELETE, rdfBoolean (true));
        }
        their_root.RemoveElement (r, true);
    }

    if (aProperties) {
        flockFavoritesUtils.setProperties (this.ds (aDirection), r, aProperties, 0);
    }
    this.ds (aDirection).Assert (r, RDF_DELETE, rdfBoolean (true), true);
    this._Flush ();
} // }}}
flockFavoritesQueue.prototype._collectionResource = function (aCollectionName) { // {{{
    //debug ('flockFavoritesQueue._collectionResource ("'+aCollectionName+'")\n');
    return COLRDF (escape (aCollectionName));
} // }}}
flockFavoritesQueue.prototype._removeCollectionRdf = function (aDataSource, aCollection) { // {{{
    // remove the name property
    try {
        var target = aDataSource.GetTarget (aCollection, RDF_NAME, true);
        if (target) {
            aDataSource.Unassert (aCollection, RDF_NAME, target);
        }
    } catch (e) { }
    // remove the delete property
    if (aDataSource.HasAssertion (aCollection, RDF_DELETE, rdfBoolean (true), true)) {
        aDataSource.Unassert (aCollection, RDF_DELETE, rdfBoolean (true));
    } else {
        // remove the children
        if (RDFCU.IsSeq (aDataSource, aCollection)) {
            var seq = Components.classes[CONTAINER_CONTRACTID]
                .createInstance (nsIRDFContainer);
            seq.Init (aDataSource, aCollection);
            var elements = seq.GetElements ();
            while (elements.hasMoreElements ()) {
                var element = elements.getNext ();
                seq.RemoveElement (element, false);
            }
            // FIXME: remove the sequencyness properties
        }
    }
 } // }}}
flockFavoritesQueue.prototype.changeCollection = function (aName, aContents, aDirection) { // {{{
    DEBUG ('flockFavoritesQueue.changeCollection ('+aName+', '+aContents+', '+aDirection+')\n');
    var our_root = this.root (aDirection);
    var their_root = this.root (!aDirection);
    var r = this._collectionResource (aName);

    // add this change
    if (our_root.IndexOf (r) == -1) {
        // if the change isn't in the root yet, add it
        our_root.AppendElement (r);
    } else {
        // we need to remove the old change
        var ds = this.ds (aDirection);

        this._removeCollectionRdf (ds, r);
    }

    // remove any change on the other side
    if (their_root.IndexOf (r) != -1) {
        var ds = this.ds (!aDirection);
        this._removeCollectionRdf (ds, r);
        their_root.RemoveElement (r, true);
    }

    // add the name property
    this.ds (aDirection).Assert (r, RDF_NAME, rdfLiteral (aName), true);

    // add the children
    var seq = RDFCU.MakeSeq (this.ds (aDirection), r);
    while (aContents.hasMoreElements ()) {
        seq.AppendElement (aContents.getNext ());
    }

    this._Flush ();
} // }}}
flockFavoritesQueue.prototype.deleteCollection = function (aName, aDirection) { // {{{
    DEBUG ('deleteCollection ('+aName+', '+aDirection+')\n');
    var our_root = this.root (aDirection);
    var their_root = this.root (!aDirection);
    var r = this._collectionResource (aName);

    // add this change
    if (our_root.IndexOf (r) == -1) {
        // if the change isn't in the root yet, add it
        our_root.AppendElement (r);
    } else {
        // we need to remove the old change
        var ds = this.ds (aDirection);

        this._removeCollectionRdf (ds, r);
    }

    // remove any change on the other side
    if (their_root.IndexOf (r) != -1) {
        var ds = this.ds (!aDirection);
        this._removeCollectionRdf (ds, r);
        their_root.RemoveElement (r, true);
    }

    // add the name property
    this.ds (aDirection).Assert (r, RDF_NAME, rdfLiteral (aName), true);

    // add the delete property
    this.ds (aDirection).Assert (r, RDF_DELETE, rdfBoolean (true), true);

    this._Flush ();
} // }}}
flockFavoritesQueue.prototype.nextChange = function (aDirection) { // {{{
    DEBUG ('nextChange ('+aDirection+')');
    var root = this.root (aDirection);
    var elements;
    try {
        elements = root.GetElements ();
    } catch (e) {
        return null;
    }
    
    if (!elements.hasMoreElements ()) {
        // no more changes
        return null;
    }

    var element = elements.getNext ();
    element.QueryInterface (nsIRDFResource);
    //debug ('element: '+element+'\n');
    //debug ('url: '+element.ValueUTF8+'\n');
    elements = null; // remove the enumerator from our namespace

    // FIXME: we need to use a property to determine collectionyness
    if (element.ValueUTF8.match(/^urn:flock:collection:.*/)) {
        // its a collection
        var change = {
            name: this.ds (aDirection).GetTarget (element, RDF_NAME, true)
                      .QueryInterface (nsIRDFLiteral).Value,
            isDelete: this.ds (aDirection).HasAssertion (element, RDF_DELETE, 
                    rdfBoolean (true), true),
            QueryInterface: function (aIID) {
                if (aIID.equals (flockIFavoritesChange) || 
                        aIID.equals (flockIFavoritesChangeCollection)) {
                    return this;
                }
                throw Components.results.NS_ERROR_NO_INTERFACE;
            }
        };

        if (!change.isDelete) {
            // get the contents
            var seq = RDFCU.MakeSeq (this.ds (aDirection), element);
            var items = seq.GetElements ();
            change.contents = new Array ();
            while (items.hasMoreElements ()) {
                change.contents.push (items.getNext ());
            }
            change.contents.hasMoreElements = function () { return this.length != 0; }
            change.contents.getNext = function () { return this.shift (); }
            //debug ('collection change: '+seq.GetCount()+' items\n');
        }

        this._removeCollectionRdf (this.ds (aDirection), element);
        root.RemoveElement (element, true);
        if (this.ds (aDirection).HasAssertion (element, RDF_DELETE, rdfBoolean (true), true)) {
            this.ds (aDirection).Unassert (element, RDF_DELETE, rdfBoolean (true));
        }
        this._Flush ();

        //debug ('returning collection change for "'+change.name+'"\n');

        return change;
    } else {
        // its a favorite
        var change = {
            url: element.ValueUTF8,
            isDelete: this.ds (aDirection).HasAssertion (element, RDF_DELETE, 
                    rdfBoolean (true), true),
            properties: flockFavoritesUtils.getProperties (this.ds (aDirection), 
                    element, 0),
            QueryInterface: function (aIID) {
                if (aIID.equals (flockIFavoritesChange) || 
                        aIID.equals (flockIFavoritesChangeFavorite)) {
                    return this;
                }
                throw Components.results.NS_ERROR_NO_INTERFACE;
            }
        };

        flockFavoritesUtils.removeProperties (this.ds (aDirection), element);
        if (this.ds (aDirection).HasAssertion (element, RDF_DELETE, rdfBoolean (true), true)) {
            this.ds (aDirection).Unassert (element, RDF_DELETE, rdfBoolean (true));
        }
        root.RemoveElement (element, true);
        this._Flush ();

        //debug ('returning favorites change for '+change.url+'\n');

        return change;
    }
} // }}}
flockFavoritesQueue.prototype.changesPending = function (aURL) { // {{{
    var resource = RDFS.GetResource (aURL);
    return (this.mInRoot.IndexOf (resource) != -1) || 
        (this.mOutRoot.IndexOf (resource) != -1)
} // }}}

// JavaScript XPCOM Boilerplate /*{{{*/
function xpcomModule (aCID, aContractId, aComponentName, aConstructor) {
    this.mCID = Components.ID (aCID);
    this.mContractId = aContractId;
    this.mComponentName = aComponentName;
    this.mConstructor = aConstructor;
    this.mGlobalJavaScript = null;

    // factory object
    this.mFactory = {
        constructor: this.mConstructor,
        createInstance: function (aOuter, aIID) {
            if (aOuter != null) {
                throw Components.results.NS_ERROR_NO_AGGREGATION;
            }

            return (new (this.constructor) ()).QueryInterface (aIID);
        }
    };

}
xpcomModule.prototype = {
    setGlobalJavaScript: function (aGlobalJavaScript) {
        this.mGlobalJavaScript = aGlobalJavaScript;
    },

    // the module should register itself
    registerSelf: function (aCompMgr, aLocation, aLoaderStr, aType) {
        aCompMgr = aCompMgr.QueryInterface (
                Components.interfaces.nsIComponentRegistrar);
        aCompMgr.registerFactoryLocation (this.mCID, this.mComponentName, 
                this.mContractId, aLocation, aLoaderStr, aType);
        var catmgr = Components.classes["@mozilla.org/categorymanager;1"]
            .getService (Components.interfaces.nsICategoryManager);
        if (this.mGlobalJavaScript) {
            catmgr.addCategoryEntry ("JavaScript global property",
                    this.mGlobalJavaScript, this.mContractId, true, true);
        }
    },

    // get the factory                  
    getClassObject: function (aCompMgr, aCID, aIID) {
        if (!aCID.equals (this.mCID)) {
            throw Components.results.NS_ERROR_NO_INTERFACE;
        }

        if (!aIID.equals (Components.interfaces.nsIFactory)) {
            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        }

        return this.mFactory;
    },

    canUnload: function(compMgr) {
        return true;
    }
};


var module = new xpcomModule ('{93d47edc-6992-40a9-ab69-0927853b09e9}',
        '@flock.com/favorites/queue;1', 'Flock Favorites Queue', 
        flockFavoritesQueue);
function NSGetModule(aCompMgr, aFileSpec) {
    return module;
};
/*}}}*/
