// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms 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

const ENABLE_DEBUG = true;
function DEBUG(x) { if (ENABLE_DEBUG) debug("flockPhotobucketService: "+x+"\n"); }

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;

Cu.import("resource:///modules/FlockCryptoHash.jsm");

const PHOTOBUCKET_TITLE             = "PhotoBucket Web Service";
const PHOTOBUCKET_FAVICON           = "chrome://flock/content/services/photobucket/favicon.png";
const PHOTOBUCKET_CID               = Components.ID('{d3b147b0-a321-11da-a746-0800200c9a66}');
const PHOTOBUCKET_CONTRACTID        = "@flock.com/?photo-api-photobucket;1";
const SERVICE_ENABLED_PREF          = "flock.service.photobucket.enabled";
const CATEGORY_COMPONENT_NAME       = "Photobucket JS Component"
const CATEGORY_ENTRY_NAME           = "photobucket"

const flockIError                   = Components.interfaces.flockIError;
const flockIPhotoAlbum              = Components.interfaces.flockIPhotoAlbum;
const flockIPhotoPerson             = Components.interfaces.flockIPhotoPerson;

const FLOCK_PHOTO_ALBUM_CONTRACTID  = "@flock.com/photo-album;1";
const FLOCK_PHOTOPERSON_CONTRACTID  = "@flock.com/photo-person;1";
const FLOCK_ERROR_CONTRACTID        = "@flock.com/error;1";

const PHOTOBUCKET_API_VERSION       = "1.0";
const PHOTOBUCKET_TOKEN_PREF        = "flock.photo.photobucket.token";
const PHOTOBUCKET_SESSION_REFRESH_INTERVAL = 21600000; // every 6 hours refresh the session token

const PHOTOBUCKET_PK                = "e57335b0847da8b1105b6c8bc27d217a";
const PHOTOBUCKET_SCID              = "149825788";
const PHOTOBUCKET_PROPERTIES        = "chrome://flock/locale/services/photobucket.properties";

// services
const RDFS = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
const IOS = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
const RDFCU = Cc["@mozilla.org/rdf/container-utils;1"].getService(Ci.nsIRDFContainerUtils);

var gCompTK;
function getCompTK() {
  if (!gCompTK) {
    gCompTK = Cc["@flock.com/singleton;1"]
                        .getService(Ci.flockISingleton)
                        .getSingleton("chrome://browser/content/flock/services/common/load-compTK.js")
                        .wrappedJSObject;
  }
  return gCompTK;
}

// for Cardinal to Danphe migration
const FLOCK_NS = "http://flock.com/rdf#";

// String defaults... may be updated later through Web Detective
var gStrings = {
  "domains": "photobucket.com",
  "userloginURL": "http://photobucket.com/login.php",
  "userprofileURL": "http://photobucket.com/images/search/%accountid%",
  "serviceloginURL": "http://photobucket.com/svc/servicelogin.php",
  "apiURL": "http://photobucket.com/svc/api.php"
};

var gPhotobucketAPI;

// ====================================================
// ========== BEGIN General Helper Functions ==========
// ====================================================

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;
}

function loadLibraryFromSpec(aSpec) {
  var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                         .getService(Components.interfaces.mozIJSSubScriptLoader);
  loader.loadSubScript(aSpec);
}

loadLibraryFromSpec("chrome://flock/content/common/flocksafe.js");
loadLibraryFromSpec("chrome://browser/content/flock/photo/photoAPI.js");


function photobucketPhoto() {
}

photobucketPhoto.prototype= {
  id: "",
  thumbnail: "",
  webPageUrl: "",
  midSizePhoto: "",
  largeSizePhoto: "",
  title: "",
  username: "",
  userid: "",
  is_video: "",
  svcShortName: 'photobucket',

  buildTooltip: function( ) {
    // do we have to use document from the window to ceate elements? -- ja
    var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                       .getService(Components.interfaces.nsIWindowMediator);
    var win = wm.getMostRecentWindow('navigator:browser');
    if (!win) return null;

    var hbox = win.document.createElement('hbox');
    var box = win.document.createElement('vbox');
    box.setAttribute('style', 'max-width: 250px');
    var title = win.document.createElement('label');
    title.setAttribute('value', this.title );
    title.setAttribute('crop', 'end');
    box.appendChild(title);
    var lbl = win.document.createElement('label');
    lbl.setAttribute('value', this.username );
    lbl.setAttribute('class', 'user');
    box.appendChild(lbl);
    hbox.appendChild(box)

    var vbox = win.document.createElement('vbox');
    var cbox = win.document.createElement('hbox');
    var largeImg = win.document.createElement('image');
    largeImg.setAttribute('src', this.midSizePhoto);
    largeImg.setAttribute('style', 'margin-bottom: 2px;');
    var spacer = win.document.createElement('spacer'); 
    spacer.setAttribute('flex', '1');
    cbox.appendChild(largeImg);
    cbox.appendChild(spacer);
    vbox.appendChild(cbox);
    vbox.appendChild(hbox);

    return vbox;
  },
  buildHTML: function( ) {
    return '<a title="'+this.title+'" href="'+this.webPageUrl+'"><img src="'+this.midSizePhoto+'" border="0" /></a>';
  },
  buildLargeHTML: function( ) {
    return '<a title="'+this.title+'" href="'+this.webPageUrl+'"><img src="'+this.largeSizePhoto+'" border="0" /></a>';
  },
  buildBBCode: function ( ) {
    if(this.is_video)
    {
      return '[photob]' + this.largeSizePhoto + '[photob]'
      
    }else
    {
      return '[url=' + this.webPageUrl + '][img]'+ this.largeSizePhoto +'[/img][/url]';
    }
  },
  buildMiniPage: function ( ) {
    return '<html><head><title>' + this.title + ' (' + this.username + ')</title></head>' +
           '<body><object width="425" height="350">' +
           '<param name="movie" value="'+this.largeSizePhoto+'"/>' +
           '<center><embed src="'+this.largeSizePhoto+'&autoplay=1" type="application/x-shockwave-flash" width="425" height="350"/></object></center></body></html>';
  },
  QueryInterface: function(iid) {
    if (!iid.equals(Components.interfaces.nsISupports) &&
        !iid.equals(Components.interfaces.flockIPhoto)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
    }
    return this;
  }
};


// ====================================================
// ========== BEGIN photobucketService class ==========
// ====================================================

function photobucketService()
{
  this.obs = Components.classes["@mozilla.org/observer-service;1"]
                       .getService(Components.interfaces.nsIObserverService);
  this.acUtils = Components.classes["@flock.com/account-utils;1"]
                           .getService(Components.interfaces.flockIAccountUtils);
  this.url = "http://www.photobucket.com";
  this.initialized = false;
  this.logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  this.logger.init("photobucket");
  this._ctk = {
    interfaces: [
      "nsISupports",
      "nsIClassInfo",
      "nsISupportsCString",
      "nsIObserver",
      "flockIPollingService",
      "flockIWebService",
      "flockIAuthenticateNewAccount",
      "flockIManageableWebService",
      "flockIMediaWebService"
    ],
    shortName: "photobucket",
    fullName: "Photobucket",
    description: PHOTOBUCKET_TITLE,
    favicon: PHOTOBUCKET_FAVICON,
    contractID: PHOTOBUCKET_CONTRACTID,
    accountClass: photobucketAccount,
    needPassword: false
  };

  var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
            .getService(Components.interfaces.nsIStringBundleService);
  var bundle = sbs.createBundle(PHOTOBUCKET_PROPERTIES);

  this._channels = {
    "special:recent": {
      title: bundle.GetStringFromName("flock.photobucket.title.recent"),
      supportsSearch: false
    }
  };

  this.init();
}


// BEGIN nsIObserver interface
photobucketService.prototype.observe =
function photobucketService_observe(aSubject, aTopic, aData)
{
}
// END nsIObserver interface


photobucketService.prototype.shortName = "photobucket";
photobucketService.prototype.serviceName = "Photobucket";

photobucketService.prototype.getPhotoFromRDFNode =
function (aRDFId)
{
  var newPhoto = new photobucketPhoto();
  var coopPhoto = this.faves_coop.get(aRDFId);
  newPhoto.webPageUrl = coopPhoto.URL;
  newPhoto.thumbnail = coopPhoto.thumbnail;
  newPhoto.midSizePhoto = coopPhoto.midSizePhoto;
  newPhoto.largeSizePhoto = coopPhoto.largeSizePhoto;
  newPhoto.username = coopPhoto.username;
  newPhoto.userid = coopPhoto.userid;
  newPhoto.title = coopPhoto.name;
  newPhoto.id = coopPhoto.photoid;
  newPhoto.icon = coopPhoto.favicon;
  newPhoto.uploadDate = coopPhoto.datevalue;
  newPhoto.is_public = coopPhoto.is_public;
  newPhoto.is_video = coopPhoto.is_video;
  return newPhoto;
}

photobucketService.prototype.handlePhotosResult =
function photobucketService_handlePhotosResult(aXML) {
  var inst = this;
  function createItem(media, is_video) {
    var uploadDate = media.getAttribute("uploaddate");
    var title = media.getAttribute("name");
    var username = media.getAttribute("username");
    var page_url = "";
    var thumb_url = media.getElementsByTagName('thumb')[0].firstChild.nodeValue;
    var small_url = media.getElementsByTagName('url')[0].firstChild.nodeValue;
    // this is a photobucket api issue - need a fix -
    try {
      page_url = media.getElementsByTagName('browseurl')[0].firstChild.nodeValue;
      page_url = page_url.replace('&amp;', '&');
    }
    catch(e) {
      page_url = small_url;
    }
    var newPhoto = new photobucketPhoto();
    newPhoto.webPageUrl = page_url;
    newPhoto.thumbnail = thumb_url;
    newPhoto.midSizePhoto = thumb_url;
    newPhoto.largeSizePhoto = small_url;
    newPhoto.title = title;
    newPhoto.username = username;
    newPhoto.userid = username;
    if (uploadDate) {
      newPhoto.id = uploadDate;
      newPhoto.uploadDate = parseInt(uploadDate)*1000;
    } else {
      newPhoto.id = username + ":" + title;
    }
    newPhoto.is_video = is_video;
    newPhoto.has_miniView = is_video;
    return newPhoto;
  }

  try {
    var rval = [];

    var videoList = aXML.getElementsByTagName("video");
    for (var i = 0; i < videoList.length; i++) {
      rval.push(createItem(videoList[i], true));
    }

    var photoList = aXML.getElementsByTagName("photo");
    for (var i = 0; i < photoList.length; i++) {
      rval.push(createItem(photoList[i], false));
    }
  } catch(e) {
    inst.logger.error(e);
  }

  /*
   * do not sort anymore since PB no longer passes back the uploadDate.
   * instead we just present the photos in the order PB gives them to
   * us (upload order)
   * see https://bugzilla.flock.com/show_bug.cgi?id=9084

  // sort the photos and videos together by date
  // the follow sorts based on ID which we are using the uploadDate to be the id
  // it is ugly -- and probably not as efficient as it could be --- but it
  // is only sorting 20 items at most so it is good enough for now - ja
  rval.sort(function(a,b) {
    return (b.id-a.id);
  });
  */

  return rval;
}

photobucketService.prototype.getContacts =
function photobucketService_getContacts(aListener)
{
  return -1;  // throw unsupported exception
}

photobucketService.prototype.findByUsername =
function photobucketService_findByUsername(aListener, aUsername)
{
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      //var user = aXML.getElementsByTagName("photo")[0];
      // We should be getting all the user's details from the response xml, but
      // the pbucket response has all pertinent info with the photo tag.  If
      // there are no photos, then we have no user info
      var newUserObj = Components.classes[FLOCK_PHOTOPERSON_CONTRACTID]
                                 .createInstance(flockIPhotoPerson);
      newUserObj.id = aUsername;
      newUserObj.username = aUsername;
      newUserObj.fullname = aUsername;
      newUserObj.service = inst;
      aListener.onFindByUsernameResult(newUserObj);
    },
    onError: function (aXML) {
      aListener.onError(aXML);
    }
  }
  var params = {};
  params.username = aUsername;
  var dict = params2Dictionary(params);
  this.api.authenticatedCall(myListener, "getrecentusermedia", dict);
}

function MultiGetter() {
}

MultiGetter.prototype = {
  mTimer: Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer),
  init: function(aSvc, aListener, aEnumerator) {
    this.mHasNew = false;
    this.mNeedSave = false;
    this.mSvc = aSvc;
    this.mListener = aListener;
    this.mArray = new Array();
    while (aEnumerator.hasMoreElements()) {
      var p = aEnumerator.getNext();
      p.QueryInterface(Components.interfaces.flockIPhotoPerson);

      this.mArray.push(p);
    }
    this.next();
  },
  finish: function() {
    //dump("FINITI\n");
    if (this.mHasNew) {
      this.mListener.onGetMostRecentPhotoForList(Components.interfaces.flockIPhotoAPIListener.LIST_HAS_NEW);
    }
    else if (this.mNeedSave) {
      this.mListener.onGetMostRecentPhotoForList(Components.interfaces.flockIPhotoAPIListener.LIST_NEED_SAVE);
    }
    else {
      this.mListener.onGetMostRecentPhotoForList(Components.interfaces.flockIPhotoAPIListener.LIST_NO_CHANGE);
    }
    return;
  },
  updatePerson: function(aPerson, aPhoto) {
    //dump(aPerson.username + "updating person\n");
    var seq = aPerson.seq;
    var newSeq = parseInt(aPhoto.id);
    if (newSeq > seq) {
      var oldHasNew = aPerson.hasNew;
      aPerson.seq = aPhoto.id;
      this.mNeedSave = true;
      if (seq != 0) {
        this.mHasNew = true;
        aPerson.hasNew = true
        if (oldHasNew == false) {
          aPerson.lastNewSeq = seq + 1;
        }
      }
    }
  },
  notify: function() {
    this.doNext();
  },
  next: function() {
    this.mTimer.initWithCallback(this, 1000, 0);
  },
  doNext: function() {
    //dump("NEXT PLEASE\n");
    var inst = this;

    if (!this.mArray.length) {
      this.finish();
      return;
    }
    var person = this.mArray.pop();

    var listener = {
      onSearchResult: function(aEnumerator) {
      //dump("b search result\n");
        while (aEnumerator.hasMoreElements()) {
          var photo = aEnumerator.getNext();
          inst.updatePerson(person, photo);
          break;
        }
        inst.next();
      },
      onError: function(aError) {
      //dump("b search eror\n");
        inst.finish();
      }
    }
    this.mSvc.search(listener, person.id, "", "", 1, 1);
  }
}

photobucketService.prototype.getMostRecentPhotoForList =
function photobucketService_getMostRecentPhotoForList(aListener, aEnumerator)
{
  var mg = new MultiGetter();
  mg.init(this, aListener, aEnumerator);
}

photobucketService.prototype.supportsSearch =
function photobucketService_supportsSearch( aQueryString ) {

  var aQuery = new queryHelper(aQueryString);

  if (aQuery.special) {
    var channel = this._channels["special:" + aQuery.special];
    if (channel) {
      return channel.supportsSearch;
    }
  }

  if (aQuery.user)
    return true;
  if (aQuery.search)
    return false;
  return null;
}


photobucketService.prototype.queryChannel =
function photobucketService_queryChannel(aListener, aQueryString, aCount, aPage)
{
  var aQuery = new queryHelper(aQueryString);
  var inst = this;

  var myListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML);
      var enum_ = {
        hasMoreElements: function() {
          return (rval.length > 0);
        },
        getNext: function() {
          return rval.shift();
        }
      }
      aListener.onSearchResult(enum_);
    },
    onError: function (aError) {
      // if we get an error here this probably means user inputted an illegal character
      // do nothign and let the "Sorry, no results found" message appear
    }
  }

  var params = {
    page: aPage,
    num: aCount
  }

  if (aQuery.getKey('special') == 'recent') {
    var dict = params2Dictionary(params);
    this.api.call(myListener, "getrecentphotos", dict);
  }

  if (aQuery.hasKey('search')) {
    // don't pass in encoded text for PB
    // if an illegal char is used, we will return no results
    var aText = aQuery.search;
    if (aText && aText.length) {
      params.query = aText.split(".")[0];
      var dict = params2Dictionary(params);
      this.api.call(myListener, "search", dict);
    }
  }
}

photobucketService.prototype.search =
function photobucketService_search(aListener, aQueryString, aCount, aPage)
{
  var aQuery = new queryHelper(aQueryString);
  if (!aQuery.user) {
       this.queryChannel(aListener, aQueryString, aCount, aPage);
       return;
  }
  var aUserid = aQuery.user;

  var inst = this;

  var myListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML);
      var enum_ = {
        hasMoreElements: function() {
          return (rval.length > 0);
        },
        getNext: function() {
          return rval.shift();
        }
      }
      aListener.onSearchResult(enum_);
    },
    onError: function (aError) {
      aListener.onError(aError);
    }
  }

  var params = {};


  if (aUserid) {
    params.username = aUserid;
    params.perpage = aCount;
  }

  if (aQuery.search) params.query = aQuery.getEncodedKey('search').split(".")[0];
  if (aPage) params.page = aPage;
  if (aQuery.album) params.album_name = aQuery.album;

  params.media = 'all';

  var dict = params2Dictionary(params);
  if (params.query != null) {
    this.api.call(myListener, "search", dict);
  } else if (params.username != null) {
    if (params.album_name) {
      this.api.authenticatedCall(myListener, "getuseralbumpaginated", dict);
    }else
      this.api.authenticatedCall(myListener, "getrecentusermedia", dict);
  }

}

photobucketService.prototype.createAlbum =
function photobucketService_createAlbum(aListener, aPath)
{
  var svc = this;
  this.logger.debug("createAlbum - aPath = "+aPath);
  var albumCreationListener = {
    onResult: function(aXML) {
      svc.logger.info(aPath + " album successfully created");
      var newAlbum = Components.classes[FLOCK_PHOTO_ALBUM_CONTRACTID]
                               .createInstance(flockIPhotoAlbum);
      var newAlbumPath;
      if (params.parent_album_name) {
        newAlbumPath = params.parent_album_name + params.new_album_name;
      } else {
        newAlbumPath = params.new_album_name;
      }
      newAlbum.title = newAlbumPath;
      newAlbum.id = newAlbumPath;
      aListener.onCreateAlbum(newAlbum);
    },
    onError: function(aError) {
      svc.logger.error("Error in album creation");
      aListener.onError(aError);
    }
  }

  var parentDir = "";
  var pathName = aPath.split('/');
  var params = new Object();
  params.new_album_name = pathName.pop();
  var dict;

  for (var i = 0; i < pathName.length; i++) {
    parentDir += pathName[i] + '/';
  }

  if (parentDir.length) {
    params.parent_album_name = parentDir;
  }

  dict = params2Dictionary(params);
  this.api.authenticatedCall(albumCreationListener, "createalbum", dict);
}

photobucketService.prototype.handleAlbumsResult =
function photobucketService_handleAlbumsResult(aXML)
{
  var rval = new Array();
  var albumList = aXML.getElementsByTagName("photoalbum");
  for (var i = 0; i < albumList.length; i++) {
    var album = albumList[i];
    var title = album.getAttribute("name");

    // the first album node has no name attr, so skip it
    if (!title) continue;

    var username = album.getAttribute('username');
    var id;
    // parse out the prefix /username/
    if (title.match(album.getAttribute('username')) && title != username) {
      id = title.substring(username.length + 1);
      var newAlbum = Components.classes[FLOCK_PHOTO_ALBUM_CONTRACTID]
                               .createInstance(flockIPhotoAlbum);
      newAlbum.id = id;
      newAlbum.title = id;
      rval.push(newAlbum);
    }
  }
  return rval;
}

photobucketService.prototype.getAccountStatus =
function photobucketService_getAccountStatus(aListener)
{
  var inst = this;
  var myListener = {
    onResult: function(aXML) {
      try {
        var result = Cc["@mozilla.org/hash-property-bag;1"]
                     .createInstance(Ci.nsIWritablePropertyBag2);
        var username = aXML.getElementsByTagName("username")[0]
                           .firstChild
                           .nodeValue;
        var maxSpace = aXML.getElementsByTagName("megabytes_allowed")[0]
                           .firstChild
                           .nodeValue;
        var usedSpace = aXML.getElementsByTagName("megabytes_used")[0]
                            .firstChild
                            .nodeValue;
        var isPremium = aXML.getElementsByTagName("premium")[0]
                            .firstChild
                            .nodeValue;
        result.setPropertyAsAString("username", username);
        result.setPropertyAsAString("maxSpace", maxSpace);
        result.setPropertyAsAString("usedSpace", usedSpace);
        result.setPropertyAsAString("maxFileSize", "");
        result.setPropertyAsAString("usageUnits", "megabytes");
        result.setPropertyAsBool("isPremium", (isPremium == "1"));
        aListener.onSuccess(result, "");
      } catch (ex) {
        inst.logger.error('getaccountstatus error :' + ex);
      }
    },
    onError: function (aError) {
      aListener.onError(aError);
    }
  }
  var params = new Object();
  var dict = params2Dictionary(params);
  this.api.authenticatedCall(myListener, "getuserinfo", dict);
}

photobucketService.prototype.getAlbums =
function photobucketService_getAlbums(aListener, aUsername)
{
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      try {
        var rval = inst.handleAlbumsResult(aXML);
        var enum_ = {
          hasMoreElements: function() {
            return (rval.length > 0);
          },
          getNext: function() {
            return rval.shift();
          }
        }
        aListener.onGetAlbumsResult(enum_);
      } catch(e) {
        inst.logger.error(e);
        aListener.onError(null);
      }
    },
    onError: function (aXML) {
      aListener.onError(aXML);
    }
  }
  var params = new Object();
  params.recurse = "true";
  params.view = "flat"
  params.media = "none";
  if (aUsername) {
    params.username = aUsername;
  }
  var dict = params2Dictionary(params);
  this.api.authenticatedCall(myListener, "getuseralbum", dict);
}

photobucketService.prototype.mUploader = null;
photobucketService.prototype.icon = PHOTOBUCKET_FAVICON;

/* features for service */
photobucketService.prototype.supportsFeature =
function photobucketService_supportsFeature(aFeature)
{
  var supports = {};
  supports.tags = false;
  supports.title = false;
  supports.contacts = false;
  supports.privacy = false;
  supports.fileName = true;
  supports.albumCreation = true;
  return (supports[aFeature] == true);
}

photobucketService.prototype.mSupportsTitle = false;
photobucketService.prototype.mSupportsTags = false;
photobucketService.prototype.mSupportsContacts = false;
photobucketService.prototype.mSupportsPrivacey = false;

photobucketService.prototype.init =
function photobucketService_init()
{
  if (this.initialized) return;
  this.initialized = true;

  DEBUG(".init()");

  this.prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
  if ( this.prefService.getPrefType(SERVICE_ENABLED_PREF) &&
       !this.prefService.getBoolPref(SERVICE_ENABLED_PREF) )
  {
    DEBUG("Pref "+SERVICE_ENABLED_PREF+" set to FALSE... not initializing.");
    var catMgr = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
    catMgr.deleteCategoryEntry("wsm-startup", CATEGORY_COMPONENT_NAME, true);
    catMgr.deleteCategoryEntry("flockWebService", CATEGORY_ENTRY_NAME, true);
    catMgr.deleteCategoryEntry("flockMediaProvider", CATEGORY_ENTRY_NAME, true);
    return;
  }

  this.mAuthUser = {};

  this.faves_coop = Components.classes["@flock.com/singleton;1"]
                              .getService(Components.interfaces.flockISingleton)
                              .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                              .wrappedJSObject;

  this.urn = 'urn:photobucket:service';
  this.pbService = new this.faves_coop.Service(
    this.urn,
    {
      name: "photobucket",
      desc: "The Photobucket Service",
      contactLabel: 'People'
    }
  );
  this.pbService.serviceId = PHOTOBUCKET_CONTRACTID;
  if (!gPhotobucketAPI) {
    gPhotobucketAPI = new PhotobucketAPI();
  }
  this.api = gPhotobucketAPI;

  // Load Web Detective file
  this.webDetective = this.acUtils.useWebDetective("photobucket.xml");
  for (var s in gStrings) {
    gStrings[s] = this.webDetective.getString("photobucket", s, gStrings[s]);
  }
  this.mApiUrl = gStrings["apiURL"];
  this.pbService.domains = gStrings["domains"];
  this.pbService.loginURL = gStrings["userloginURL"];

  // Update auth states
  try {
    if (this.webDetective.detectCookies("photobucket", "loggedout", null)) {
      this.acUtils.markAllAccountsAsLoggedOut(PHOTOBUCKET_CONTRACTID);
      this.mAuthUser = null;
      this.mUploader = null;
    } else {
      this.checkPersistedToken();
    }
  } catch (ex) {
    this.logger.error("ERROR updating auth states for Photobucket: "+ex);
  }
}

photobucketService.prototype.checkPersistedToken =
function photobucketService_checkPersistedToken()
{
  var acctURN = this.acUtils.getFirstAuthenticatedAccountForService(PHOTOBUCKET_CONTRACTID);
  if (!acctURN) { return; }
  var token = this.faves_coop.get(acctURN).authToken;
  if (token) {
    this.logger.debug('checking if the session associated with the token is still valid ... ' + token);
    // try to get the session from the persisted token
    var inst = this;
    var myListener = {
      onResult: function(aXML) {
        try {
          var resp = aXML.getElementsByTagName('response');
          if (resp[0].getAttribute('stat') == 'ok') {
            var username = aXML.getElementsByTagName('username')[0].firstChild.nodeValue;
            var sessionkey = aXML.getElementsByTagName('session_key')[0].firstChild.nodeValue;
            inst.api.mApiUrl = aXML.getElementsByTagName("url")[0].firstChild.nodeValue;
            inst.getAuthUser().id = username;
            inst.getAuthUser().username = username;
            inst.getAuthUser().fullname = username;
            inst.getAuthUser().sessionkey = sessionkey;
            if (!inst.api.mSessionRefreshTimer) inst.api.fireSessionRefreshTimer();
          } else {
            inst.mAuthUser = null;
            this.mUploader = null;
          }
        }
        catch(e) {
          inst.logger.error(e);
        }
      },
      onError: function(aErr) {
        return false;
      }
    }
    var params = {};
    params.service_token = token;
    inst.call(myListener, "getsession", params2Dictionary(params));
  }
  //this.mAuthUser.sessionkey = null;
}

photobucketService.prototype.getAuthState =
function photobucketService_getAuthState()
{
  return this.state;
}

photobucketService.prototype.__defineGetter__("supportsTitle", function () { return this.mSupportsTitle; })
photobucketService.prototype.__defineGetter__("supportsTags", function () { return this.mSupportsTags; })
photobucketService.prototype.__defineGetter__("supportsContacts", function () { return this.mSupportsContacts; })
photobucketService.prototype.__defineGetter__("supportsPrivacey", function () { return this.mSupportsPrivacey; })
photobucketService.prototype.__defineGetter__("authState", function () { return this.state; })

photobucketService.prototype.logout =
function photobucketService_logout()
{
  this.mAuthUser = null;
  this.mUploader = null;
  if (this.mSessionRefreshTimer) { this.mSessionRefreshTimer.cancel(); }
  this.acUtils.removeCookies(this.webDetective.getSessionCookies("photobucket"));
}


photobucketService.prototype.getAuthUser =
function photobucketService_getAuthUser()
{
  return this.api.mAuthUser;
}

photobucketService.prototype.getAuthPerson =
function photobucketService_getAuthPerson()
{
  var user = this.getAuthUser();
  if (!user) {
    return null;
  }
  var person = {};
  person.id = user.id;
  person.fullname = user.fullname;
  person.username = user.username;
  person.service = this;
  return person;
}


photobucketService.prototype.notify = function photobucketService_sessionTimerNotify(aTimer) {
  var inst = this;
  var sessionRefreshListener = {
    onAuth: function() {
      inst.logger.debug('yay! pbucket refreshed its session key with the timer');
    },

    onError: function(aError) {
      inst.logger.error('!!!boo! error refreshing session key on the timer');
    }
  }
  this.getSessionKey(sessionRefreshListener);
}


photobucketService.prototype.getSessionKey =
function photobucketService_getSessionKey(aListener)
{
  var acctURN = this.acUtils.getFirstAuthenticatedAccountForService(PHOTOBUCKET_CONTRACTID);
  if (!acctURN) aListener.onError(null);
  var token = this.faves_coop.get(acctURN).token;
  this.logger.debug(">>>getSessionKey called with the token "+token+"\n\n\n");

  var inst = this;
  var mySessionListener = {
    onResult: function(aXML) {
      var username = aXML.getElementsByTagName('username')[0].firstChild.nodeValue;
      var sessionkey = aXML.getElementsByTagName('session_key')[0].firstChild.nodeValue;
      inst.api.mApiUrl = aXML.getElementsByTagName('url')[0].firstChild.nodeValue;
      inst.getAuthUser().id = username;
      inst.getAuthUser().username = username;
      inst.getAuthUser().fullname = username;
      inst.getAuthUser().sessionkey = sessionkey;
      aListener.onAuth();
    },
    onError: function(aErr) {
      inst.logger.debug('>>>>>> ' + aErr.errorString + '\n\n')
      aListener.onError(aErr);
    }
  };
  var params = {};
  params.service_token = token;
  inst.call(mySessionListener, "getsession", params2Dictionary(params));
}

photobucketService.prototype.upload =
function photobucketService_upload(aListener, aUpload, aFile)
{
  var inst = this;
  var sigParams = {};
  var myParams = {};

  this.logger.debug('uploading to album ' + aUpload.album);
  myParams.version = PHOTOBUCKET_API_VERSION;
  myParams.scid = PHOTOBUCKET_SCID;
  myParams.method = 'upload';
  myParams.description = aUpload.description;
  myParams.title = aUpload.title;
  myParams.album_name = aUpload.album;
  myParams.session_key = this.getAuthUser().sessionkey;
  myParams.sig = this.api.buildSig("upload", sigParams, true);

  sigParams.session_key = this.getAuthUser().sessionkey;

  this.mUploader = new PhotoUploader();
  this.mUploader.setEndpoint(inst.api.mApiUrl);
  this.mUploader.photoParam = "uploadfile";

  var myListener = {
    onResult: function(aXML) {
      var photo = aXML.getElementsByTagName("photo")[0];
      if (photo) {
        inst.logger.debug('uploaded the pic named ' + photo.getAttribute('name'));
        var rval = inst.handlePhotosResult(aXML);
        aListener.onUploadComplete(aUpload);
        aListener.onUploadFinalized(aUpload, rval[0]);
      }
      else {
        aListener.onError(inst.api.getError("SERVICE_ERROR", aXML, null));
      }
    },
    onError: function(aErrorCode) {
      if (aErrorCode) {
        aListener.onError(inst.api.getError("HTTP_ERROR", null, aErrorCode));
      } else {
        aListener.onError(inst.api.getError("SERVICE_ERROR", null, null));
      }
    },
    onProgress: function(aCurrentProgress) {
        aListener.onProgress(aCurrentProgress);
    }
  }
  this.mUploader.upload(myListener, aFile, myParams, aUpload);
}

photobucketService.prototype.cancelUpload =
function photobucketService_cancelUpload()
{
  try {
    this.mUploader.req.abort();
  } catch(e) { }
}


// flockIPollingService implementation

photobucketService.prototype.refresh =
function photobucketService_refresh(aURN, aListener)
{
  var refreshItem = this.faves_coop.get(aURN);
  if (refreshItem.isInstanceOf(this.faves_coop.Account)) {
    aListener.onResult();
  }
}


photobucketService.prototype.migrateAccount =
function photobucketService_migrateAccount( aId, aUsername) {
  this.init();

  var token = '';
  try {
    token = flock_getCharPref(PHOTOBUCKET_TOKEN_PREF);
  } catch (e) { }

  this.addAccountById(aId, false, null, aUsername, token);
}

// BEGIN flockIWebService interface
photobucketService.prototype.addAccountById =
function photobucketService_addAccountById(aAccountID, aIsTransient, aListener, aUsername, aToken)
{
  this.logger.debug("{flockIWebService}.addAccountById('"+aAccountID+"', "+aIsTransient+", aListener)");

  if (!aUsername) {
    // Get the password associated with this account
    var pw = this.acUtils.getPassword(this.urn+':'+aAccountID);
    var name = pw.user;
    var token = '';
    var pollable = false;
    var auth = false;
  } else {
    var name = aUsername;
    var token = aToken;
    var pollable = true;
    var auth = true;
  }

  var accountURN = "urn:flock:photobucket:"+aAccountID;
  var acctCoopObj = this.faves_coop.get(accountURN);
  if (!acctCoopObj) {
    acctCoopObj = new this.faves_coop.Account(
      accountURN,
      {
        accountId: aAccountID,
        name: name, // This gets changed to the user's full name once logged in
        serviceId: PHOTOBUCKET_CONTRACTID,
        service: this.pbService,
        URL: "http://www.photobucket.com/",
        favicon: this.icon,
        isTransient: aIsTransient,
        isPollable: pollable,
        authToken: token,
        isAuthenticated: auth
      }
    );
    this.faves_coop.accounts_root.children.add(acctCoopObj);
  }

  // perhaps we should do a checktoken and if valid go ahead and set the account as logged in?

  var acct = this.getAccount(acctCoopObj.id());
  if (aListener) aListener.onSuccess(acct, "addAccount");
  return acct;
}
// END flockIWebService interface


// BEGIN flockIAuthenticateNewAccount interface
photobucketService.prototype.authenticateNewAccount =
function photobucketService_authenticateNewAccount(aListener)
{
  this.logger.debug("{flockIAuthenticateNewAccount}.authenticateNewAccount(aListener)");
  aListener.onStart(null, "newaccountstarted");
  var inst = this;
  var tokenListener = {
    onResult: function (aXML) {
      inst.logger.debug(".authenticateNewAccount(): tokenListener: onResult()");
      var params = {
        service_token: aXML.getElementsByTagName("service_token")[0].firstChild.nodeValue
      };
      var sessionListener = {
        onResult: function (aXML) {
          inst.logger.debug(".authenticateNewAccount(): tokenListener: sessionListener: onResult()");
          var username = aXML.getElementsByTagName("username")[0].firstChild.nodeValue;
          var sessionkey = aXML.getElementsByTagName("session_key")[0].firstChild.nodeValue;
          inst.api.mApiUrl = aXML.getElementsByTagName("url")[0].firstChild.nodeValue;
          inst.getAuthUser().id = username;
          inst.getAuthUser().username = username;
          inst.getAuthUser().fullname = username;
          inst.getAuthUser().sessionkey = sessionkey;
          var acctURN = inst.acUtils.getFirstAuthenticatedAccountForService(PHOTOBUCKET_CONTRACTID);
          var account = null;
          try {
            account = inst.getAccount(acctURN);
            if (!account) throw "ACCOUNT WAS NOT CREATED";
          } catch (ex) {
            dump(ex);
            inst.logger.error(".authenticateNewAccount(): tokenListener: onResult(): ERROR: account was not created");
            if (aListener) {
              aListener.onError(null, "newaccount", null);
            }
            return;
          }
          if (aListener) {
            aListener.onSuccess(account, "newaccount");
          }
        },
        onError: function (aError) {
          inst.logger.debug(".authenticateNewAccount(): tokenListener: sessionListener: onError()");
          inst.logger.debug(">>>>>> " + aError.errorString);
          if (aListener) {
            aListener.onError(aError, "newaccount", aError);
          }
        }
      };
      var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                         .getService(Components.interfaces.nsIWindowMediator);
      var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                         .getService(Components.interfaces.nsIWindowWatcher);
      var topWin = wm.getMostRecentWindow(null);
      var loginUrl = inst.api.buildUrl(gStrings["serviceloginURL"], "login", params);
      var url = "chrome://browser/content/flock/photo/photoLoginWindow.xul?"
              + "url="+escape(loginUrl)
              + "&finalString="+escape("You have granted");
      var chrome = "chrome,close,titlebar,resizable=yes,toolbar,dialog=no,"
                 + "scrollbars=yes,modal,centerscreen";
      topWin.open(url, "_blank", chrome);
      // Wait till the window closes
      inst.api.call(sessionListener, "getsession", params2Dictionary(params));
    },
    onError: function (aError) {
      inst.logger.debug(".authenticateNewAccount(): tokenListener: onError()");
      inst.logger.debug(">>>>>> " + aError.errorString);
      if (aListener) {
        aListener.onError(aError, "newaccount");
      }
    }
  };
  if (!this.api.mAuthUser) {
    this.api.mAuthUser = {};
  }
  this.api.getToken(tokenListener);
}
// END flockIAuthenticateNewAccount interface


// BEGIN flockIManageableWebService interface
photobucketService.prototype.getAccountIDFromDocument =
function photobucketService_getAccountIDFromDocument(aDocument)
{
  this.logger.debug("{flockIManageableWebService}.getAccountIDFromDocument(aDocument)");
  aDocument.QueryInterface(Components.interfaces.nsIDOMHTMLDocument);
  var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  if (this.webDetective.detect("photobucket", "accountinfo", aDocument, results)) {
    return results.getPropertyAsAString("userid");
  } else {
    // We could not detect any account info on the login landing page.  This
    // may mean that it's the API authentication login landing page, which
    // doesn't have any account info.  If this is the case, then we should
    // have a temp password entry associated with the session cookie.
    var sessionValue = this.acUtils.getCookie("http://photobucket.com", "PHPSESSID");
    var pw = this.acUtils.getTempPassword("photobucket:session:"+sessionValue);
    if (pw) {
      this.logger.debug("Sneaky! Got acctID from a temp password associated with session cookie!");
      return pw.user;
    }
  }
  return null;
}

photobucketService.prototype.getCredentialsFromForm =
function photobucketService_getCredentialsFromForm(aForm)
{
  var inst = this;
  var detectForm = function (aType, aResults) {
    return inst.webDetective.detectForm("photobucket", aType, aForm, aResults);
  };

  var formType = "login";
  var results = getCompTK().newResults();
  if (!detectForm(formType, results)) {
    formType = "signup";
    results = getCompTK().newResults();
    if (!detectForm(formType, results)) {
      formType = "changepassword";
      results = getCompTK().newResults();
      if (!detectForm(formType, results)) {
        results = null;
      }
    }
  }

  if (results) {
    var pw = {
      QueryInterface: function(aIID) {
        if (!aIID.equals(Components.interfaces.nsISupports) &&
            !aIID.equals(Components.interfaces.nsIPassword) &&
            !aIID.equals(Components.interfaces.flockIPassword)) { 
          throw Components.interfaces.NS_ERROR_NO_INTERFACE;
        }
        return this;
      },
      user: results.getPropertyAsAString("username"),
      password: results.getPropertyAsAString("password"),
      host: null,
      formType: formType
    };

    // Doing a bit of a hack here...
    // Since the Photobucket login landing page doesn't always reveal which
    // account is logged in, we will need to look at the session cookie and
    // see if its the same as what it was when the user last logged in.  So
    // at this point we're just storing the account username as a temp
    // password entry associated with the session cookie token.
    var sessionValue = this.acUtils.getCookie("http://photobucket.com", "PHPSESSID");
    this.acUtils.clearTempPassword("photobucket:session:"+sessionValue);
    this.acUtils.setTempPassword("photobucket:session:"+sessionValue, pw.user, "", formType);
    return pw;
  }
  return null;
}

photobucketService.prototype.updateAccountStatusFromDocument =
function photobucketService_updateAccountStatusFromDocument(aDocument)
{
  var inst = this;
  this.logger.debug("{flockIManageableWebService}.updateAccountStatusFromDocument('"+aDocument.URL+"')");
  if (this.webDetective.detect("photobucket", "loggedout", aDocument, null)) {
    this.acUtils.markAllAccountsAsLoggedOut(PHOTOBUCKET_CONTRACTID);
    this.logout();
  } else if (this.webDetective.detect("photobucket", "loggedin", aDocument, null)) {
    var acctID;
    var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                            .createInstance(Components.interfaces.nsIWritablePropertyBag2);
    if (this.webDetective.detect("photobucket", "accountinfo", aDocument, results)) {
      acctID = results.getPropertyAsAString("userid");
    } else {
      // We could not detect any account info on the login landing page.  This
      // may mean that it's the API authentication login landing page, which
      // doesn't have any account info.  If this is the case, then we should
      // have a temp password entry associated with the session cookie.
      var sessionValue = this.acUtils.getCookie("http://photobucket.com", "PHPSESSID");
      var pw = this.acUtils.getTempPassword("photobucket:session:"+sessionValue);
      if (pw) {
        this.logger.debug("Sneaky! Got acctID from a temp password associated with session cookie!");
        acctID = pw.user;
      }
    }
    if (acctID) {
      var acctURN = this.acUtils.getAccountURNById(this.urn, acctID);
      if (acctURN) {
        var acctCoopObj = this.faves_coop.get(acctURN);
        if (!acctCoopObj.isAuthenticated) {
          // Re-authenticate the API
          var reauthListener = {
            onSuccess: function reauthListener_onSuccess(aSubject, aTopic) {
              inst.logger.debug("reauthListener: onSuccess()");
            },
            onError: function reauthListener_onError(aSubject, aTopic, aError) {
              inst.logger.debug("reauthListener: onError()");
              acctCoopObj.isAuthenticated = false;
            }
          };
          this.getAccount(acctURN).login(reauthListener);
          // Prematurely set isAuthenticated to TRUE in order to avoid other
          // asyncronous auth attempts before this one completes.
          acctCoopObj.isAuthenticated = true;
        }
      }
    }
  }
}
// END flockIManageableWebService interface


// BEGIN flockIMediaWebService interface
photobucketService.prototype.decorateForMedia =
function photobucketService_decorateForMedia(aDocument)
{
  this.logger.debug("{flockIMediaWebService}.decorateForMedia(aDocument)");
  var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  if (this.webDetective.detect("photobucket", "media", aDocument, results)) {
    var userid = results.getPropertyAsAString("userid");
    var mediaArr = new Array();

    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = new Object();
    }
    var media = {
      name: userid,
      query: 'user:' + userid + "|username:" + userid,
      label: userid,
      favicon: this.icon,
      service: this.shortName
    }
    mediaArr.push(media);

    aDocument._flock_decorations.mediaArr = mediaArr;
    this.obs.notifyObservers(aDocument, 'media', 'media:update');
  }
}

photobucketService.prototype.handlesMediaStream =
function photobucketService_handlesMediaStream() 
{
  return true;
}

photobucketService.prototype.checkIsStreamUrl =
function photobucketService_checkIsStreamUrl(aUrl)
{
  // Videos:
  // /player.swf?file=http://vid166.photobucket.com/albums/u113/h671226/Smart_Dog.flv
  // Images:
  // http://i166.photobucket.com/albums/u105/such-a-craka/thfhg.jpg
  if (this.webDetective.detectNoDOM("photobucket", "isStreamUrl", "", aUrl, null)) {
    this.logger.debug("Checking if url is photobucket stream: YES: " + aUrl);
    return true;
  }
  this.logger.debug("Checking if url is photobucket stream: NO: " + aUrl);
  return false;
}

photobucketService.prototype.getMediaQueryFromURL =
function photobucketService_getMediaQueryFromURL(aUrl, aListener)
{
  var userName = null;
  var detectResults = Cc["@mozilla.org/hash-property-bag;1"]
                      .createInstance(Ci.nsIWritablePropertyBag2);
  if (this.webDetective.detectNoDOM("photobucket", "person", "", aUrl, detectResults)) {
    userName = detectResults.getPropertyAsAString("userid");
  }

  if (userName) {
    var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                            .createInstance(Components.interfaces.nsIWritablePropertyBag2);
    results.setPropertyAsAString("query", "user:" + userName + "|username:" + userName);
    results.setPropertyAsAString("title", this.title + " user: " + userName);
    aListener.onSuccess(results, "query");
  } else {
    aListener.onError(null, "Unable to get user.", null);
  }
}
// END flockIMediaWebService interface


photobucketService.prototype.doMigration =
function photobucketService_doMigration(aDatasource, aAccountURN) {
}


// ================================================
// ========== BEGIN XPCOM Module support ==========
// ================================================

function createModule(aParams) {
  return {
    registerSelf: function (aCompMgr, aFileSpec, aLocation, aType) {
      aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
      aCompMgr.registerFactoryLocation( aParams.CID, aParams.componentName,
                                        aParams.contractID, aFileSpec,
                                        aLocation, aType );
      var catMgr = Cc["@mozilla.org/categorymanager;1"]
        .getService(Ci.nsICategoryManager);
      if (!aParams.categories) { aParams.categories = []; }
      for (var i = 0; i < aParams.categories.length; i++) {
        var cat = aParams.categories[i];
        catMgr.addCategoryEntry( cat.category, cat.entry,
                                 cat.value, true, true );
      }
    },
    getClassObject: function (aCompMgr, aCID, aIID) {
      if (!aCID.equals(aParams.CID)) { throw Cr.NS_ERROR_NO_INTERFACE; }
      if (!aIID.equals(Ci.nsIFactory)) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }
      return { // Factory
        createInstance: function (aOuter, aIID) {
          if (aOuter != null) { throw Cr.NS_ERROR_NO_AGGREGATION; }
          var comp = new aParams.componentClass();
          if (aParams.implementationFunc) { aParams.implementationFunc(comp); }
          return comp.QueryInterface(aIID);
        }
      };
    },
    canUnload: function (aCompMgr) { return true; }
  };
}

// NS Module entrypoint
function NSGetModule(aCompMgr, aFileSpec) {
  return createModule({
    componentClass: photobucketService,
    CID: PHOTOBUCKET_CID,
    contractID: PHOTOBUCKET_CONTRACTID,
    componentName: CATEGORY_COMPONENT_NAME,
    implementationFunc: function (aComp) { getCompTK().addAllInterfaces(aComp); },
    categories: [
      { category: "wsm-startup", entry: CATEGORY_COMPONENT_NAME, value: PHOTOBUCKET_CONTRACTID },
      { category: "flockWebService", entry: CATEGORY_ENTRY_NAME, value: PHOTOBUCKET_CONTRACTID },
      { category: "flockMediaProvider", entry: CATEGORY_ENTRY_NAME, value: PHOTOBUCKET_CONTRACTID }
    ]
  });
}

// ========== END XPCOM Module support ==========


// ====================================================
// ============= BEGIN photobucket API ================
// ====================================================

function PhotobucketAPI() {
  this._logger = Cc["@flock.com/logger;1"]
                 .createInstance(Ci.flockILogger);
  this._logger.init("photobucketAPI");
  this._logger.info("Created Photobucket API Object");
  this.acUtils = Cc["@flock.com/account-utils;1"]
                 .getService(Ci.flockIAccountUtils);
  this.webDetective = this.acUtils.useWebDetective("photobucket.xml");
  this.faves_coop = Cc["@flock.com/singleton;1"]
                    .getService(Ci.flockISingleton)
                    .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                    .wrappedJSObject;

  this.mApiUrl = gStrings["apiURL"];
  this.mAuthUser = null;
  this.mSessionRefreshTimer = null;
  var inst = this;
};


PhotobucketAPI.prototype.isLoggedIn =
function photobucketAPI_isLoggedIn() {
  return (this.mAuthUser && this.mAuthUser.sessionkey != null);
};


PhotobucketAPI.prototype.getToken =
function photobucketAPI_getToken(aListener) {
  var inst = this;
  var tokenListener = {
    onResult: function (aXML) {
      try {
        var resp = aXML.getElementsByTagName("response");
        if (resp[0].getAttribute("stat") == "ok") {
          //rval = resp[0].firstChild.nodeValue;
          aListener.onResult(aXML);
        } else {
          aListener.onError(null);  //TODO - figure out errors
          //rval = -1;
        }
      }
      catch(e) {
        inst._logger.error(e);
      }
    },
    onError: function (aError) {
      inst._logger.error(aError.errorString);
      aListener.onError(aError);
    }
  }
  var params = {};
  this.call(tokenListener, "getservicetoken", params2Dictionary(params));
};


PhotobucketAPI.prototype.call =
function photobucketAPI_call(aListener,
                             aMethod,
                             aDictionary,
                             aAuth) {
  var inst = this;
  var params = this.dictionary2Params(aDictionary);
  var url = this.buildUrl(inst.mApiUrl, aMethod, params, aAuth);
  this._logger.debug(url + " < call");
  this.req = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
             .createInstance(Ci.nsIXMLHttpRequest)
             .QueryInterface(Ci.nsIJSXMLHttpRequest);

  this.req.open("GET", url, true);
  var req = this.req;
  this.req.onreadystatechange = function (aEvt) {
    if (req.readyState == 4) {
      try {
        inst._logger.debug("doCall[onreadystatechange] req.status = "
                           + req.status);
        if (req.status/100 == 2) {
          inst._logger.debug(req.responseText);
          var rsp = req.responseXML.getElementsByTagName("response")[0];
          var stat = rsp.getAttribute("stat");
          if (stat != "ok") {
            aListener.onError(inst.getError("SERVICE_ERROR",
                                            req.responseXML,
                                            null));
          } else {
            aListener.onResult(req.responseXML);
          }
        } else {
          aListener.onError(inst.getError("HTTP_ERROR", null, req.status));
        }
      } catch(e) {
        inst._logger.error(e);
        // We need to convert to a string because that's what getError expects
        var errorCode = String(Ci.flockIError.XML_HTTP_REQUEST_ERROR)
        aListener.onError(inst.getError("HTTP_ERROR", null, errorCode));
      }
    }
  }
  this.req.send(null);
};


PhotobucketAPI.prototype.dictionary2Params =
function photobucketAPI_dictionary2Params(aDictionary) {
  var params = {};
  var obj = {};
  var count = {};
  var keys = aDictionary.getKeys(count, obj);
  for (var i = 0; i < keys.length; ++i) {
    var supports = aDictionary.getValue(keys[i]);
    var supportsString = supports.QueryInterface(Ci.nsISupportsString);
    var val = supportsString.toString();
    params[keys[i]] = val;
  }
  return params;
};

PhotobucketAPI.prototype.authenticatedCall =
function photobucketAPI_authenticatedCall(aListener,
                                          aMethod,
                                          aDictionary) {
  this._logger.debug("authenticatedCall " + aMethod);
  if (!this.isLoggedIn()) {
    this._logger.debug("not logged in, so trying unauth call for " + aMethod);
    return this.call(aListener, aMethod, aDictionary);
  }
  this._logger.debug("+++++" + this.mAuthUser.sessionkey);
  return this.call(aListener, aMethod, aDictionary, true);
};

PhotobucketAPI.prototype.doLogin =
function photobucketAPI_doLogin(aAccountURN, aListener) {
  this._logger.debug(".doLogin('" + aAccountURN + "')");
  var inst = this;
  var tokenListener = {
    onResult: function(aXML) {
      inst._logger.debug(".doLogin('"
                         + aAccountURN +
                         "'): tokenListener: onResult()");
      // Time to authenticate...
      var params = {
        service_token: aXML.getElementsByTagName("service_token")[0]
                           .firstChild
                           .nodeValue
      };
      // persist the token
      var acctCoopObj = inst.faves_coop.get(aAccountURN);
      acctCoopObj.authToken = params.service_token;

      // listener for the upcoming "getsession" api call
      var sessionListener = {
        onResult: function(aXML) {
          inst._logger.debug(".doLogin('"
                             + aAccountURN
                             + "'): tokenListener:"
                             + "sessionListener: onResult()");
          var username = aXML.getElementsByTagName('username')[0]
                             .firstChild
                             .nodeValue;
          var sessionkey = aXML.getElementsByTagName('session_key')[0]
                               .firstChild
                               .nodeValue;
          inst.mApiUrl = aXML.getElementsByTagName('url')[0]
                             .firstChild
                             .nodeValue;
          inst.mAuthUser.id = username;
          inst.mAuthUser.username = username;
          inst.mAuthUser.fullname = username;
          inst.mAuthUser.sessionkey = sessionkey;
          inst.acUtils.ensureOnlyAuthenticatedAccount(aAccountURN);
          if (!inst.mSessionRefreshTimer) inst.fireSessionRefreshTimer();
          aListener.onSuccess(null, "");
        },
        onError: function(aErr) {
          inst._logger.error(".doLogin('"
                             + aAccountURN
                             + "'): tokenListener: sessionListener: onError()");
          inst._logger.error(">>>>>> " + aErr.errorString);
          aListener.onError(aErr);
        }
      };

      // If we have the user's un/pw, then we can do this silently...
      var pw = inst.acUtils.getPassword("urn:photobucket:service:"
                                        + acctCoopObj.accountId);
      if (pw) {
        inst._logger.debug(".doLogin('"
                           + aAccountURN
                           + "'): tokenListener: onResult(): found password");

        var postBody = "action=login"
                     + "&service_token="+params.service_token
                     + "&sig="+inst.buildSig("login", params)
                     + "&scid="+PHOTOBUCKET_SCID
                     + "&method=login"
                     + "&callback_verify="
                     + "&username="+pw.user
                     + "&password="+pw.password
                     + "&login=Login";
        var hr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
                 .createInstance(Ci.nsIXMLHttpRequest)
                 .QueryInterface(Ci.nsIJSXMLHttpRequest);

        var onReadyStateFunc = function(eEvt) {
          if (hr.readyState == 4) {
            inst._logger.debug(".doLogin('"
                               + aAccountURN
                               + "'): tokenListener:"
                               + "onResult(): hr.onreadystatechange()");
            var results = Cc["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Ci.nsIWritablePropertyBag2);
            if (inst.webDetective.detectNoDOM("photobucket",
                                              "apiauth",
                                              "",
                                              hr.responseText,
                                              results)) {
              var hr2 = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
                        .createInstance(Ci.nsIXMLHttpRequest)
                        .QueryInterface(Ci.nsIJSXMLHttpRequest);

              hr2.onreadystatechange = function(eEvt2) {
                if (hr2.readyState == 4) {
                  inst.call(sessionListener,
                            "getsession",
                            params2Dictionary(params));
                }
              }

              var postBody2 = "action=complete"
                            + "&authorized=yes"
                            + "&service_token=" + params.service_token
                            + "&sig=" + inst.buildSig("login", params)
                            + "&scid=" + PHOTOBUCKET_SCID
                            + "&method=login"
                            + "&callback_verify="
                            + "&user_id="
                            + results.getPropertyAsAString("user_id");

              hr2.backgroundRequest = true;
              hr2.open("POST", gStrings["serviceloginURL"],true);
              hr2.setRequestHeader("Content-Type",
                                   "application/x-www-form-urlencoded");
              hr2.send(postBody2);
            } else {
              inst.call(sessionListener,
                        "getsession",
                        params2Dictionary(params));
            }
          }
        };

        hr.onreadystatechange = onReadyStateFunc;
        hr.backgroundRequest = true;
        hr.open("POST", gStrings["serviceloginURL"],true);
        hr.setRequestHeader("Content-Type",
                            "application/x-www-form-urlencoded");
        hr.send(postBody);

      } else {
        inst._logger.debug(".doLogin('"
                           + aAccountURN +
                           "'): tokenListener: onResult(): NO password!");

        // No password stored, so we have to prompt the user
        var windowManager = Cc["@mozilla.org/appshell/window-mediator;1"]
                            .getService(Ci.nsIWindowMediator);
        var topWindow = windowManager.getMostRecentWindow(null);

        var loginUrl = inst.buildUrl(gStrings["serviceloginURL"],
                                     "login",
                                     params);
        var url = "chrome://browser/content/flock/photo/photoLoginWindow.xul?"
                + "url=" + escape(loginUrl)
                + "&finalString=" + escape("You have granted");
        var options = "chrome,"
                    + "close,"
                    + "titlebar,"
                    + "resizable=yes,"
                    + "toolbar,"
                    + "dialog=no,"
                    + "scrollbars=yes,"
                    + "modal,"
                    + "centerscreen";
        topWindow.open(url, "_blank", options);
        // we are waiting till window closes

        inst.call(sessionListener, "getsession", params2Dictionary(params));
      }
    },
    onError: function(aErr) {
      inst._logger.error(".doLogin('"
                         + aAccountURN
                         + "'): tokenListener: onError()");
      aListener.onError(aErr);
    }
  }

  if (!this.mAuthUser) {
    this.mAuthUser = {};
  }
  this.getToken(tokenListener);
};

PhotobucketAPI.prototype.fireSessionRefreshTimer =
function photobucketAPI_fireSessionRefreshTimer() {
  this.mSessionRefreshTimer = Cc["@mozilla.org/timer;1"]
                              .createInstance(Ci.nsITimer);
  // refresh the session key once an hour, probably can adjust this
  this.mSessionRefreshTimer.initWithCallback(this,
                                             PHOTOBUCKET_SESSION_REFRESH_INTERVAL,
                                             2);
};

PhotobucketAPI.prototype.getError =
function photobucketAPI_getError(aErrorType, aXML, aHTTPErrorCode) {
  var errorNode;
  var code;
  var serverErrorMessage;
  var error = Cc[FLOCK_ERROR_CONTRACTID]
              .createInstance(flockIError);

  this._logger.debug("getError aErrorType="
                     + aErrorType
                     + "; aHTTPErrorCode="
                     + aHTTPErrorCode);

  if (aErrorType == "HTTP_ERROR") {
    error.errorCode = aHTTPErrorCode;
    return error;
  } else if (aErrorType == "SERVICE_ERROR") {
    try {
      errorNode = aXML.getElementsByTagName("error")[0];
      code = errorNode.getAttribute("code");
      serverErrorMessage = errorNode.getAttribute("msg");
      error.serviceErrorCode = code;
      error.serviceErrorString = serverErrorMessage;
      this._logger.debug("Found error code = "
                         + code
                         + " and message = '"
                         + serverErrorMessage
                         + "'");
    } catch (ex) {
      code = "-1"
    }

    if (!code) code = "-1";

    switch (code) {
      case "-1":
        error.errorCode = error.PHOTOSERVICE_INVALID_RESPONSE;
        break;
      case "007":
      case "008":
        error.errorCode = error.PHOTOSERVICE_LOGIN_FAILED;
        break;
      case "012":
        error.errorCode = error.PHOTOSERVICE_SESSION_KEY_EXPIRED;
        break;
      case "101":
        error.errorCode = error.PHOTOSERVICE_EMPTY_ALBUMNAME;
        break;
      case "102":
        error.errorCode = error.PHOTOSERVICE_INVALID_USER;
        break;
      case "103":
        error.errorCode = error.PHOTOSERVICE_INVALID_SEARCH_QUERY;
        break;
      case "104":
        error.errorCode = error.PHOTOSERVICE_INVALID_SEARCH_QUERY;
        break;
      case "105":
        error.errorCode = error.PHOTOSERVICE_INVALID_UPLOAD_FILE;
        break;
      case "106":
        error.errorCode = error.PHOTOSERVICE_INVALID_SEARCH_QUERY;
        break;
      case "107":
        error.errorCode = error.PHOTOSERVICE_INVALID_ALBUM;
        break;
      case "108":
        error.errorCode = error.PHOTOSERVICE_PRIVATE_ALBUM;
        break;
      case "109":
        error.errorCode = error.PHOTOSERVICE_PHOTOS_IN_ALBUM_LIMIT_REACHED;
        break;
      case "110":
        error.errorCode = error.PHOTOSERVICE_NO_FILE_UPLOADED;
        break;
      case "111":
        error.errorCode = error.PHOTOSERVICE_ALBUM_IS_OVER_SIZE_LIMIT;
        break;
      case "112":
        error.errorCode = error.PHOTOSERVICE_INVALID_UPLOAD_FILE;
        break;
      case "113":
        error.errorCode = error.PHOTOSERVICE_DUPLICATE_FILENAME;
        break;
      case "115":
        error.errorCode = error.PHOTOSERVICE_INVALID_ALBUMNAME;
        break;
      case "116":
        error.errorCode = error.PHOTOSERVICE_DUPLICATE_ALBUMNAME;
        break;
      case "314":
        error.errorCode = error.PHOTOSERVICE_FILE_IS_OVER_SIZE_LIMIT;
        break;
      default:
        error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
        if (serverErrorMessage && serverErrorMessage.length) {
          error.serviceErrorString = serverErrorMessage;
        }
        break;
    }
    return error;
  }
  return null;
};

PhotobucketAPI.prototype.buildUrl =
function photobucketAPI_buildUrl(aUrl, aMethod, aParams, aAuth) {
  var qs = "";
  var strConcat = "";
  for (var p in aParams) {
    qs += "&" + escape(p) + "=" + escape(aParams[p]);
    strConcat += escape(aParams[p]);
  }
  var rval = aUrl
           + "?method=" + aMethod
           + "&version=" + PHOTOBUCKET_API_VERSION;
  if (aAuth) {
    rval += "&session_key=" + this.mAuthUser.sessionkey;
  }
  rval += "&scid=" + PHOTOBUCKET_SCID
       + qs
       + "&sig=" + this.buildSig(aMethod, aParams, aAuth);
  this._logger.debug("this is your buildUrl: " + rval + "\n");
  return rval;
};

PhotobucketAPI.prototype.buildSig =
function photobucketAPI_buildSig(aMethod, aParams, aAuth) {
  var qs = "";
  var strConcat = "";
  for (var p in aParams) {
    qs += "&" + escape(p) + "=" + escape(aParams[p]);
    if (p == 'service_token') {
      strConcat += escape(aParams[p]);
    }
  }

  this._logger.debug(strConcat + "<<<<<<<<strconcat\n");
  var preHash = aMethod
                + PHOTOBUCKET_PK
                + PHOTOBUCKET_SCID
                + strConcat
                + (aAuth ? this.mAuthUser.sessionkey : "");
  this._logger.debug("going to sig::: " + preHash);

  var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                  .createInstance(Ci.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";

  var inputStream = converter.convertToInputStream(preHash);
  return FlockCryptoHash.md5Stream(inputStream);
};

// ========== END PhotobucketAPI class ==========



// ====================================================
// ========== BEGIN photobucketAccount class ==========
// ====================================================

function photobucketAccount()
{
  this.acUtils = Components.classes["@flock.com/account-utils;1"]
                           .getService(Ci.flockIAccountUtils);
  this.service = Cc[PHOTOBUCKET_CONTRACTID]
                 .getService(Ci.flockIMediaWebService);
  this._coop = Components.classes["@flock.com/singleton;1"]
                         .getService(Ci.flockISingleton)
                         .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                         .wrappedJSObject;
  this.logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  this.logger.init("photobucket");
  if (!gPhotobucketAPI) {
    gPhotobucketAPI = new PhotobucketAPI();
  }
  this.api = gPhotobucketAPI;
  this._ctk = {
    interfaces: [
      "nsISupports",
      "flockIWebServiceAccount",
      "flockIMediaWebServiceAccount",
      "flockIMediaUploadAccount"
    ]
  };
  getCompTK().addAllInterfaces(this);
}

// BEGIN flockIWebServiceAccount interface
photobucketAccount.prototype.urn = null;

photobucketAccount.prototype.login =
function photobucketAccount_login(aListener)
{
  this.logger.debug("{flockIWebServiceAccount}.login() urn=" + this.urn);

  var timer = Components.classes["@mozilla.org/timer;1"]
                        .createInstance(Ci.nsITimer);
  var inst = this;
  timerFunc = {
    notify: function(aTimer) {
      inst.api.doLogin(inst.urn, aListener);
    }
  }
  timer.initWithCallback(timerFunc, 0, 0);  //re-check token
}

photobucketAccount.prototype.activate =
function photobucketAccount_activate(aListener)
{
  this.logger.debug("{flockIWebServiceAccount}.activate() urn=" + this.urn);
  var acctCoopObj = this._coop.get(this.urn);
  var inst = this;
  var listener = {
    onSuccess: function() {
      inst.logger.debug("activate() listener onSuccess()");
      acctCoopObj.isPollable = true;
      aListener.onSuccess(inst, "accountAuthorized");
      try {
        var authUser = inst.api.mAuthUser;
        acctCoopObj.name = authUser.fullname;
        var photostreamURN = "urn:flock:stream:photo:photobucket:"+acctCoopObj.accountId;
        var photostream = inst._coop.get(photostreamURN);
        photostream.name = authUser.fullname+" Photostream";
      } catch (ex) {
        inst.logger.error(ex + '::' + ex.lineNumber);
      }
    },
    onError: function(aError) {
      inst.logger.debug("activate() listener onError()");
      try {
        var authUser = inst.getAuthUser();
        var acctCoopObj = inst._coop.get(inst.urn);
        acctCoopObj.isAuthenticated = false;
      } catch (ex) {
        // Don't really need to do anything, we were just trying to be nice
        // anyway...
      }
    }
  }
  this.login(listener);
  // Pre-emptively setting isAuthenticated to TRUE in order to avoid further
  // auth attemps while this one is completing.
  acctCoopObj.isAuthenticated = true;
}

photobucketAccount.prototype.deactivate =
function photobucketAccount_deactivate()
{
  this.logger.debug("{flockIWebServiceAccount}.deactivate()");
  var acctCoopObj = this._coop.get(this.urn);
  acctCoopObj.isPollable = false;
  acctCoopObj.isAuthenticated = false;
}

photobucketAccount.prototype.keep =
function photobucketAccount_keep()
{
  var c_acct = this._coop.get(this.urn);
  c_acct.isTransient = false;
}
// END flockIWebServiceAccount interface

// ========== END photobucketAccount class ==========
