/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * 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.
 *
 * The Original Code is bookmark transaction code.
 *
 * The Initial Developer of the Original Code is
 *   Joey Minta <jminta@gmail.com>
 *
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

function bookmarkTransactionManager() {
    this.wrappedJSObject = this;
    this.mTransactionManager = Components.classes["@mozilla.org/transactionmanager;1"]
                                         .createInstance(Components.interfaces.nsITransactionManager);

    this.mBatchCount = 0;

    this.classInfo = {
        getInterfaces: function (count) {
            var ifaces = [
                Components.interfaces.nsISupports,
                Components.interfaces.nsIClassInfo
            ];
            count.value = ifaces.length;
            return ifaces;
        },

        getHelperForLanguage: function (language) {
            return null;
        },

        contractID: "@mozilla.org/bookmarks/transactionManager;1",
        classDescription: "Booksmarks Transaction Manager",
        classID: Components.ID("{62d2f7fb-acd2-4876-aa2d-b607de9329ff}"),
        implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,
        flags: 0
    };

    // Define our transactions
    function bkmkTxn() {
        this.item    = null;
        this.parent  = null;
        this.index   = null;
        this.removedProp = null;
    };

    bkmkTxn.prototype = {
        BMDS: null,

        QueryInterface: function bkmkTxnQI(iid) {
            if (!iid.equals(Components.interfaces.nsITransaction) &&
                !iid.equals(Components.interfaces.nsISupports))
            throw Components.results.NS_ERROR_NO_INTERFACE;

            return this;
        },

        merge               : function (aTxn)   {return false},
        getHelperForLanguage: function (aCount) {return null},
        getInterfaces       : function (aCount) {return null},
        canCreateWrapper    : function (aIID)   {return "AllAccess"},

        RDFCU: Components.classes["@mozilla.org/rdf/container-utils;1"]
                         .getService(Components.interfaces.nsIRDFContainerUtils),

        getParents: function getParents(aResource) {
            var parents = [];
            var arcs = this.BMDS.ArcLabelsIn(aResource);
            while (arcs && arcs.hasMoreElements()) {
                var testresource = arcs.getNext();
                if (testresource && testresource.QueryInterface) {
                    testresource.QueryInterface(Components.interfaces.nsIRDFResource);
                    if (this.RDFCU.IsOrdinalProperty(testresource)) {
                        var source = this.BMDS.GetSource(testresource, aResource, true)
                                         .QueryInterface(Components.interfaces.nsIRDFResource);
                        if (parents.indexOf(source) == -1) {
                          //dump("CDC: found parent: "+source.Value+"\n");
                          parents.push(source);
                        }
                    }
                }
            }
            //dump("CDC: found "+parents.length+" parents\n");
            return parents;
        },

        mAssertProperties: function bkmkTxnAssertProps(aProps) {
            if (!aProps) {
                return;
            }

            //for each (var prop in aProps) { // Mozilla bug. Bruno
                for (var i = 0; i < this.Properties.length; i++) {
                    var oldValue = this.BMDS.GetTarget(this.item, this.Properties[i], true);
                    // must check, if paste call after copy the oldvalue didn't remove.
                    if (!oldValue) {
                        var newValue = aProps[i];
                        if (newValue) {
                            this.BMDS.Assert(this.item, 
                                             this.Properties[i], 
                                             newValue, true);
                        }
                    } else {
                        this.removedProp[i] = oldValue;
                    }
                }
            //}
        },

        mUnassertProperties: function bkmkTxnUnassertProps(aProps) {
            if (!aProps) {
                return;
            }
            //for each (var prop in aProps) {// Mozilla bug. Bruno
                for (var i = 0; i < this.Properties.length; i++) {
                    var oldValue = aProps[i];
                    if (oldValue) {
                        this.BMDS.Unassert(this.item, this.Properties[i], oldValue);
                    }
                }
            //}
        }
    };

    bkmkTxn.prototype.RDFC = 
            Components.classes["@mozilla.org/rdf/container;1"]
                      .createInstance(Components.interfaces.nsIRDFContainer);
    var rdfService = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                               .getService(Components.interfaces.nsIRDFService);
    bkmkTxn.prototype.BMDS = rdfService.GetDataSource("rdf:flock-favorites");

    bkmkTxn.prototype.Properties = 
            [rdfService.GetResource("http://home.netscape.com/NC-rdf#Name"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#URL"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#ShortcutURL"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#Description"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#WebPanel"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#FeedURL"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#MicsumGenURI"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#MicsumExpiration"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#GeneratedTitle"),
             rdfService.GetResource("http://home.netscape.com/NC-rdf#BookmarkAddDate"),
             rdfService.GetResource("http://home.netscape.com/WEB-rdf#LastModifiedDate"),
             rdfService.GetResource("http://home.netscape.com/WEB-rdf#LastVisitDate"),
             rdfService.GetResource("http://home.netscape.com/WEB-rdf#LastCharset"),
             rdfService.GetResource("http://flock.com/rdf#flockType"),
             rdfService.GetResource("http://flock.com/rdf#isTransient"),
             rdfService.GetResource("http://flock.com/rdf#isIndexable"),
             rdfService.GetResource("http://flock.com/rdf#unseenItems"),
             rdfService.GetResource("http://flock.com/rdf#count"),
             rdfService.GetResource("http://flock.com/rdf#maxItems"),
             rdfService.GetResource("http://flock.com/rdf#capItems"),
             rdfService.GetResource("http://flock.com/rdf#hasUnseenItems"),
             rdfService.GetResource("http://flock.com/rdf#isPollable"),
             rdfService.GetResource("http://flock.com/rdf#refreshing"),
             rdfService.GetResource("http://flock.com/rdf#shared"),
             rdfService.GetResource("http://flock.com/rdf#favicon"),
             rdfService.GetResource("http://flock.com/rdf#tags"),
             rdfService.GetResource("http://flock.com/rdf#lastItemDate"),
             rdfService.GetResource("http://flock.com/rdf#lastRefresh"),
             rdfService.GetResource("http://flock.com/rdf#lastQuery"),
             rdfService.GetResource("http://flock.com/rdf#refreshInterval"),
             rdfService.GetResource("http://flock.com/rdf#datevalue"),
             rdfService.GetResource("http://flock.com/rdf#CoopType"),
             rdfService.GetResource("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")];

    function bkmkInsertTxn(aAction) {
        this.type = "insert";
        // move container declaration to here so it can be recognized if
        // undoTransaction is call after the BM manager is close and reopen.
        this.container = Components.classes["@mozilla.org/rdf/container;1"]
                                   .createInstance(Components.interfaces.nsIRDFContainer);
    }

    bkmkInsertTxn.prototype = {
         __proto__: bkmkTxn.prototype,

         isTransient: false,

         doTransaction: function bkmkInsertDoTxn() {
             this.RDFC.Init(this.BMDS, this.parent);
             // if the index is -1, we use appendElement, and then update the
             // index so that undoTransaction can still function
             if (this.index == -1) {
                 this.RDFC.AppendElement(this.item);
                 this.index = this.RDFC.GetCount();
             } else {
/*XXX- broken insert code, see bug 264571
                 try {
                     this.RDFC.InsertElementAt(this.item, this.index, true);
                 } catch (e if e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) {
                     // if this failed, then we assume that we really want to append,
                     // because things are out of whack until we renumber.
                     this.RDFC.AppendElement(this.item);
                     // and then fix up the index so undo works
                     this.index = this.RDFC.GetCount();
                 }
*/
                 this.RDFC.InsertElementAt(this.item, this.index, true);
             }

             // insert back all the properties
             this.mAssertProperties(this.removedProp);
         },

         undoTransaction: function bkmkInsertUndoTxn() {
             // XXXvarga Can't use |RDFC| here because it's being "reused" elsewhere.
             this.container.Init(this.BMDS, this.parent);

             // remove all properties befor we remove the element so
             // nsLocalSearchService doesn't return deleted element in Search
             this.mUnassertProperties(this.removedProp);

             this.container.RemoveElementAt(this.index, true);
         },

         redoTransaction: function bkmkInsertRedoTxn() {
             this.doTransaction();
         }
    };

    function bkmkRemoveTxn() {
        this.type    = "remove";
    }

    bkmkRemoveTxn.prototype = {
        __proto__: bkmkTxn.prototype,

        isTransient: false,

        doTransaction: function bkmkRemoveDoTxn() {
            this.RDFC.Init(this.BMDS, this.parent);

            // remove all properties befor we remove the element so
            // nsLocalSearchService doesn't return deleted element in Search
            this.mUnassertProperties(this.removedProp);

            this.RDFC.RemoveElementAt(this.index, false);
        },

        undoTransaction: function bkmkRemoveUndoTxn() {
            this.RDFC.Init(this.BMDS, this.parent);
            this.RDFC.InsertElementAt(this.item, this.index, false);

            // insert back all the properties
            this.mAssertProperties(this.removedProp);
        },

        redoTransaction: function () {
            this.doTransaction();
        }
    }

    function bkmkRemoveAllTxn() {
        this.type    = "removeAll";
    }

    bkmkRemoveAllTxn.prototype = {
        __proto__: bkmkTxn.prototype,

        isTransient: false,

        childContainer:
            Components.classes["@mozilla.org/rdf/container;1"]
                      .createInstance(Components.interfaces.nsIRDFContainer),

        doTransaction: function bkmkRemoveDoTxn() {
            this.item.QueryInterface(Components.interfaces.nsIRDFResource);
            //dump("CDC: bkmkRemoveAllDoTxn('"+this.item.Value+"')\n"); 

            checkTopSitesRequirement = function checkTopSitesRequirement(aParents) {
              for(var i=0;i<aParents.length;i++) {
                var parent = aParents[i];

                if(parent.Value == "urn:flock:topsites") {
                  return true;
                }
              }
              
              return false;
            };

            // When 'deleting' a bookmark item, we only want to REALLY delete
            // it if it has EXACTLY one parent
            if ( (this.getParents(this.item).length == 1) 
                 || (this.getParents(this.item).length == 2 && checkTopSitesRequirement(this.getParents(this.item)))
               ) {
                // First, recursively delete any children of this one...
                if (this.RDFCU.IsContainer(this.BMDS, this.item)) {
                    this.childContainer.Init(this.BMDS, this.item);
                    var children = this.childContainer.GetElements();
                    this.mSubTransactions = [];
                    while (children && children.hasMoreElements()) {
                        var child = children.getNext();
                        var txn = new bkmkRemoveAllTxn();
                        txn.item = child;
                        txn.parent = this.item;
                        txn.index = this.childContainer.IndexOf(child);
                        txn.action = this.action;
                        txn.removedProp = [];
                        for (var i = 0; i < this.Properties.length; i++) {
                            txn.removedProp[i] = this.BMDS.GetTarget(child, this.Properties[i], true);
                        }
                        txn.wrappedJSObject = txn;
                        this.mSubTransactions.push(txn);
                    }
                    for (var i = 0; i < this.mSubTransactions.length; i++) {
                        this.mSubTransactions[i].doTransaction();
                    }
                }

                // Remove all properties before we remove the element so
                // nsLocalSearchService doesn't return deleted element in Search
                this.mUnassertProperties(this.removedProp);
            }

            // Remove the item from its parent (this always gets done,
            // regardless of how many other parents there might be)
             //dump("CDC: bkmkRemoveAllDoTxn "+this.index+"\n"); 
            try {
              this.RDFC.Init(this.BMDS, this.parent);
              //If index == -1 then we  don't have an index to remove 
              // this can happen when bookmark is created and then removed, it might not have an index
              if(this.index > -1)
                this.RDFC.RemoveElementAt(this.index, false);
            }catch(e)
            {
            }
            
        },

        undoTransaction: function bkmkRemoveUndoTxn() {
            // Reinstate the arc to the main parent
            this.RDFC.Init(this.BMDS, this.parent);
            this.RDFC.InsertElementAt(this.item, this.index, false);

            if (this.getParents(this.item).length == 1) {
                // insert back all the properties
                this.mAssertProperties(this.removedProp);

                // Undo all the sub-transactions (contained bookmarks)
                if (this.mSubTransactions) {
                    for (var i = this.mSubTransactions.length - 1; i >= 0; i--) {
                        this.mSubTransactions[i].undoTransaction();
                        this.mSubTransactions[i] = undefined;
                    }
                    this.mSubTransactions = undefined;
                }
            }
        },

        redoTransaction: function () {
            this.doTransaction();
        }
    }

    function bkmkImportTxn(aAction) {
        this.type    = "import";
        this.action  = aAction;
    }

    bkmkImportTxn.prototype = {
        __proto__: bkmkTxn.prototype,

        isTransient: false,

        doTransaction: function bkmkImportDoTxn() {},

        undoTransaction: function mkmkImportUndoTxn() {
            this.RDFC.Init(this.BMDS, this.parent);
            this.RDFC.RemoveElementAt(this.index, true);
        },

        redoTransaction: function bkmkImportredoTxn() {
            this.RDFC.Init(this.BMDS, this.parent);
            this.RDFC.InsertElementAt(this.item, this.index, true);
        }
    };

    this.BookmarkRemoveTransaction = bkmkRemoveTxn;
    this.BookmarkRemoveAllTransaction = bkmkRemoveAllTxn;
    this.BookmarkInsertTransaction = bkmkInsertTxn;
    this.BookmarkImportTransaction = bkmkImportTxn;
}

bookmarkTransactionManager.prototype.QueryInterface = function bkTxnMgrQI(aIID) {
    if (aIID.equals(Components.interfaces.nsISupports) ||
        aIID.equals(Components.interfaces.nsIBookmarkTransactionManager)) {
        return this;
    } 
    if (aIID.equals(Components.interfaces.nsIClassInfo)) {
        return this.classInfo;
    }

    throw NS_ERROR_NO_INTERFACE;
}

bookmarkTransactionManager.prototype.createAndCommitTxn = 
function bkmkTxnMgrCandC(aType, aAction, aItem, aIndex, aParent, aPropCount, aRemovedProps) {
    //dump("CDC: createAndCommitTxn(aType='"+aType+"', aAction='"+aAction+"', aIndex="+aIndex+")\n");
    var txn;
    var nsIBookmarkTransactionManager = Components.interfaces.nsIBookmarkTransactionManager;
    switch (aType) {
        case nsIBookmarkTransactionManager.IMPORT: 
            txn = new this.BookmarkImportTransaction(aAction);
            break;
        case nsIBookmarkTransactionManager.INSERT: 
            txn = new this.BookmarkInsertTransaction(aAction);
            break;
        case nsIBookmarkTransactionManager.REMOVE: 
            if (aAction == "move") {
              txn = new this.BookmarkRemoveTransaction(aAction);
            } else {
              txn = new this.BookmarkRemoveAllTransaction(aAction);
            }
            break;
        default:
            Components.utils.reportError("Unknown bookmark transaction type:"+aType);
            throw NS_ERROR_FAILURE;
    }
    txn.item = aItem;
    txn.parent = aParent;
    txn.index = aIndex;
    txn.removedProp = aRemovedProps;
    txn.action = aAction;
    txn.wrappedJSObject = txn;
    this.mTransactionManager.doTransaction(txn);
    //dump("CDC: end createAndCommitTxn(aType='"+aType+"', aAction='"+aAction+"', aIndex="+aIndex+")\n");
}

bookmarkTransactionManager.prototype.startBatch = function bkmkTxnMgrUndo() {
    if (this.mBatchCount == 0) {
        this.mTransactionManager.beginBatch();
    }
    this.mBatchCount++;
}

bookmarkTransactionManager.prototype.endBatch = function bkmkTxnMgrUndo() {
    this.mBatchCount--;
    if (this.mBatchCount == 0) {
        this.mTransactionManager.endBatch();
    }
}

bookmarkTransactionManager.prototype.undo = function bkmkTxnMgrUndo() {
    this.mTransactionManager.undoTransaction();
}

bookmarkTransactionManager.prototype.redo = function bkmkTxnMgrRedo() {
    this.mTransactionManager.redoTransaction();
}

bookmarkTransactionManager.prototype.canUndo = function bkmkTxnMgrCanUndo() {
    return this.mTransactionManager.numberOfUndoItems > 0;
}

bookmarkTransactionManager.prototype.canRedo = function bkmkTxnMgrCanRedo() {
    return this.mTransactionManager.numberOfRedoItems > 0;
}

bookmarkTransactionManager.prototype.__defineGetter__("transactionManager", 
function bkmkTxnMgrGetter() {  return this.mTransactionManager; });

/****
 **** module registration
 ****/

const kFactory = {
    createInstance: function (outer, iid) {
        if (outer != null)
            throw Components.results.NS_ERROR_NO_AGGREGATION;
        return (new bookmarkTransactionManager()).QueryInterface(iid);
    }
};

var bkmkTxnMgrModule = {
    mCID: Components.ID("{8be133d0-681d-4f0b-972b-6a68e41afb62}"),
    mContractID: "@mozilla.org/bookmarks/transactionmanager;1",
    
    registerSelf: function (compMgr, fileSpec, location, type) {
        compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
        compMgr.registerFactoryLocation(this.mCID,
                                        "Bookmark Transaction Manager",
                                        this.mContractID,
                                        fileSpec,
                                        location,
                                        type);
    },

    getClassObject: function (compMgr, cid, iid) {
        if (!cid.equals(this.mCID))
            throw Components.results.NS_ERROR_NO_INTERFACE;

        if (!iid.equals(Components.interfaces.nsIFactory))
            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

        return kFactory;
    },

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

function NSGetModule(compMgr, fileSpec) {
    return bkmkTxnMgrModule;
}
