// 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 Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;

Cu.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;

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

const MODULE_NAME = "Flock Photo Upload";

// Photo Upload Service
const FLOCK_PHOTO_UPLOAD_SERVICE_NAME = "Flock Photo Upload Service";
const FLOCK_PHOTO_UPLOAD_CID = Components.ID("{9d499deb-fd01-43d5-9eda-91783a37f3bc}");
const FLOCK_PHOTO_UPLOAD_CONTRACTID = "@flock.com/media/upload-service;1";

const FLOCK_RDF_UPLOAD_ROOT = "urn:flock:photo:upload";

/**************************************************************************
 * Component: Flock Photo Upload Service
 **************************************************************************/

// Constructor.
function flockPhotoUploadService() {
  this.uploadListeners = [];

  this.obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
  this.obs.addObserver(this, 'flock-data-ready', false);

  this.running = false;
  this._isImporting = false;
  this.mImportQueue = Array();
  this.mAsyncCopier = Cc["@mozilla.org/network/async-stream-copier;1"].createInstance(Ci.nsIAsyncStreamCopier);

  this._FLOCK_PREF_PHOTO_RESIZE = "flock.photo.resize";
  this._FLOCK_PREF_PHOTO_RESIZE_DIMENSIONS = "flock.photo.resize.dimensions";

  this._FLOCK_IO_CHUNKSIZE = 65536 * 4;
  this._FLOCK_THUMB_SIZE = 75;
  this._FLOCK_PREVIEW_SIZE = 300;
}


/**************************************************************************
 * Flock Photo Upload Service: XPCOM Component Creation
 **************************************************************************/

flockPhotoUploadService.prototype = new FlockXPCOMUtils.genericComponent(
  FLOCK_PHOTO_UPLOAD_SERVICE_NAME,
  FLOCK_PHOTO_UPLOAD_CID,
  FLOCK_PHOTO_UPLOAD_CONTRACTID,
  flockPhotoUploadService,
  Ci.nsIClassInfo.SINGLETON,
  [
    Ci.flockIPhotoUploadService,
    Ci.nsIObserver
  ]
);

// FlockXPCOMUtils.genericModule() categories
flockPhotoUploadService.prototype._xpcom_categories = [
  { category: "flock-startup", service: true }
];

/**************************************************************************
 * Flock Photo Upload Service: Private Members
 **************************************************************************/

flockPhotoUploadService.prototype._init =
function PUS__init() {
  // Logger
  this._logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
  this._logger.init("photoUploadService");

  // Prepare the data 
  this._coop = Cc["@flock.com/singleton;1"]
               .getService(Ci.flockISingleton)
               .getSingleton("chrome://flock/content/common/load-faves-coop.js")
               .wrappedJSObject;

  // Upload root
  this._uploadRoot = this._coop.get(FLOCK_RDF_UPLOAD_ROOT);
  if (!this._uploadRoot) {
    this._uploadRoot = new this._coop.UncountedFolder(FLOCK_RDF_UPLOAD_ROOT);
  }
}

flockPhotoUploadService.prototype._getGM =
function flockPhotoUploadService__getGM() {
  if (this._gm) {
    return this._gm;
  }
  this._gm = Cc["@flock.com/imagescaler;1"]
             .getService(Ci.flockIImageScaler);
  return this._gm;
}

flockPhotoUploadService.prototype._getGMAsync =
function flockPhotoUploadService__getGMAsync() {
  if (this._gmAsync) {
    return this._gmAsync;
  }
  this._gmAsync = Cc["@flock.com/asyncImageScaler;1"]
                  .getService(Ci.flockIAsyncImageScaler);
  return this._gmAsync;
}

flockPhotoUploadService.prototype._createArgs =
function flockPhotoUploadService__createArgs() {
  var rval = [];
  for (var i = 0; i < arguments.length; ++i) {
    if (arguments[i] == null) {
      continue;
    }
    rval.push(arguments[i] + "");
  }
  var argString = rval.join(" ");
  return rval;
}

flockPhotoUploadService.prototype._resize =
function flockPhotoUploadService__resize(aSize, aSrc, aDest) {
  var dims = aSize + "x" + aSize + ">";
  var args = this._createArgs("convert",
                              "-size",
                              dims,
                              aSrc,
                              "-coalesce",
                              "-geometry",
                              dims,
                              aDest);
  this._getGM().doMagick(args, args.length);
}

flockPhotoUploadService.prototype._resizeAsync =
function flockPhotoUploadService__resizeAsync(aSize, aSrc, aDest, aListener) {
  var dims = aSize + "x" + aSize + ">";
  var args = this._createArgs("convert",
                              "-size",
                              dims,
                              aSrc,
                              "-quality",
                              "90",
                              "-coalesce",
                              "-geometry",
                              dims,
                              aDest);
  this._getGMAsync().doAsyncMagick(args, args.length, aListener);
}

flockPhotoUploadService.prototype._thumb =
function flockPhotoUploadService__thumb(aSize, aSrc, aDest) {
  this._logger.debug("thumb " + aSize);
  this._logger.debug("      " + aSrc);
  this._logger.debug("      " + aDest);

  var dims = aSize + "x" + aSize;
  var args = this._createArgs("convert", aSrc,
    "-resize", "x" + aSize*2, "-resize",  aSize*2 + "x<",
    "-resize", "50%", "-gravity", "center", "-crop", dims + "+0+0",
    "-page",
    "0x0+0+0",
    aDest);
  this._getGM().doMagick(args, args.length);
}

flockPhotoUploadService.prototype._thumbAsync =
function flockPhotoUploadService__thumbAsync(aSize, aSrc, aDest, aListener) {
  var dims = aSize + "x" + aSize;
  var args = this._createArgs("convert", aSrc,
    "-resize", "x" + aSize*2, "-resize",  aSize*2 + "x<",
    "-resize", "50%", "-quality", "90", "-gravity", "center", "-crop", dims + "+0+0",
    "-page",
    "0x0+0+0",
    aDest);
  this._getGMAsync().doAsyncMagick(args, args.length, aListener);
}

flockPhotoUploadService.prototype._cropExt =
function flockPhotoUploadService__cropExt(aSrc,
                                          aDest,
                                          aWidth,
                                          aHeight,
                                          aHorzOffset,
                                          aVertOffset,
                                          aRot,
                                          aSize)
{
  var dims = aWidth + "x" + aHeight;
  var rot = aRot;
  var args = this._createArgs("convert",
                              aSrc,
                              "-coalesce",
                              "-crop",
                              dims + "+" + aHorzOffset + "+" + aVertOffset,
                              "-page",
                              "0x0+0+0",
                              aRot ? "-rotate" : null,
                              aRot ? aRot : null,
                              aSize ? "-resize" : null,
                              aSize ? (aSize + "x" + aSize + ">") : null,
                              aDest);
  this._getGM().doMagick(args, args.length);
}

flockPhotoUploadService.prototype._getDimensions =
function flockPhotoUploadService__getDimensions(aSrc) {
  var aDims = {};
  aDims.x = 0;
  aDims.y = 0;

  var args = this._createArgs("identify", aSrc);
  this._getGM().doMagick(args, args.length);
  var profileDir = Cc["@mozilla.org/file/directory_service;1"]
                   .getService(Ci.nsIProperties)
                   .get("ProfD", Ci.nsIFile);
  if (profileDir) {
    var inFile = Cc["@mozilla.org/file/local;1"]
                 .createInstance(Ci.nsILocalFile);
    inFile.initWithPath(profileDir.path);
    inFile.append("flockimageinfo.txt");
    var rawText = this._readFile(inFile);

    // could this be better done wtih a REGEXP???
    rawText.match(/.+?(\d+)x(\d+)\+(\d+)\+(\d+).+/);

    aDims.x = RegExp.$1;
    aDims.y = RegExp.$2;
    return aDims;
  }
  return null;
}

flockPhotoUploadService.prototype._transferExif =
function flockPhotoUploadService__transferExif(sourceUrl, targetUrl) {
  var sourceFile = this._getFileFromURL(sourceUrl);
  var targetFile = this._getFileFromURL(targetUrl);
  var sourcePath = sourceFile.path;
  var targetPath = targetFile.path;

  var args = this._createArgs("convert", sourcePath, sourcePath + ".exif");
  this._getGM().doMagick(args, args.length);

  try {
    var exifFile = Cc["@mozilla.org/file/local;1"]
                   .createInstance(Ci.nsILocalFile);
    exifFile.initWithPath(sourcePath + ".exif");
    exifFile.moveTo(null, exifFile.leafName + ".app1");

    args = this._createArgs("convert",
                            "-profile",
                            sourcePath + ".exif.app1",
                            targetPath,
                            targetPath);
    this._getGM().doMagick(args, args.length);
    this._removeFileByPath(sourcePath + ".exif.app1");
  } catch(ex) {
    this._logger.error("Error getting the EXIF for " + sourcePath);
    this._logger.error(ex);
  }
}

flockPhotoUploadService.prototype._readFile =
function flockPhotoUploadService__readFile(aFile) {
  try {
     var inStream = Cc["@mozilla.org/network/file-input-stream;1"]
                    .createInstance(Ci.nsIFileInputStream);
     inStream.init(aFile, 0x01, 0444, null);

     var sInStream = Cc["@mozilla.org/scriptableinputstream;1"]
                     .createInstance(Ci.nsIScriptableInputStream);
     sInStream.init(inStream);
  } catch (ex) {
    this._logger.error("Error reading file");
    this._logger.error(ex);
  }

  try {
    var str = sInStream.read(aFile.fileSize - 1);
  } catch (ex) {
    this._logger.error("Error reading file");
    this._logger.error(ex);
  }
  sInStream.close();
  inStream.close();
  return str;
}

flockPhotoUploadService.prototype._getTmpFile =
function flockPhotoUploadService__getTmpFile(aFName) {
  var sinkFile = Cc["@mozilla.org/file/directory_service;1"]
      .getService(Ci.nsIProperties)
      .get("ProfD", Ci.nsIFile);
  sinkFile.append("flock_imagecache");
  if (!sinkFile.exists()) {
    sinkFile.createUnique(1, 0700);
  }
  if (aFName) {
    sinkFile.append(aFName);
  }
  return sinkFile;
}


flockPhotoUploadService.prototype._getURLFromFile =
function flockPhotoUploadService__getURLFromFile(aFile) {
  var ios = Cc["@mozilla.org/network/io-service;1"]
            .getService(Ci.nsIIOService);
  var fileHandler = ios.getProtocolHandler("file")
                       .QueryInterface(Ci.nsIFileProtocolHandler);
  return fileHandler.getURLSpecFromFile(aFile);
}

flockPhotoUploadService.prototype._getFileFromURL =
function flockPhotoUploadService__getFileFromURL(aURL) {
  var ios = Cc["@mozilla.org/network/io-service;1"]
            .getService(Ci.nsIIOService);
  var fileHandler = ios.getProtocolHandler("file")
                       .QueryInterface(Ci.nsIFileProtocolHandler);
  var sourceSpec = fileHandler.getFileFromURLSpec(aURL);
  var sourceFile = Cc["@mozilla.org/file/local;1"]
                   .createInstance(Ci.nsILocalFile);
  sourceFile.initWithFile(sourceSpec);
  return sourceFile;
}

flockPhotoUploadService.prototype._getFileFromPath =
function flockPhotoUploadService__getFileFromPath(aPath) {
  var file = Cc["@mozilla.org/file/local;1"]
             .createInstance(Ci.nsILocalFile);
  file.initWithPath(aPath);
  return file;
}

flockPhotoUploadService.prototype._removeFileByPath =
function flockPhotoUploadService__removeFileByPath(aPath, aRecursive) {
  try {
    var file = this._getFileFromPath(aPath);
    file.remove(aRecursive);
  }
  catch(ex) {
    this._logger.error("Error removing the file " + aPath);
    this._logger.error(ex);
  }
}

flockPhotoUploadService.prototype._getBufferedInputStream =
function flockPhotoUploadService__getBufferedInputStream(aFile) {
  var sourceFileStream = Cc["@mozilla.org/network/file-input-stream;1"]
                         .createInstance(Ci.nsIFileInputStream);
  sourceFileStream.init(aFile, 1, 0, false);

  var sourceBufferStream = Cc["@mozilla.org/network/buffered-input-stream;1"]
                           .createInstance(Ci.nsIBufferedInputStream);

  sourceBufferStream.init(sourceFileStream,
                          this._FLOCK_IO_CHUNKSIZE); // write, create, truncate
  return sourceBufferStream;
}

flockPhotoUploadService.prototype._getBufferedOutputStream =
function flockPhotoUploadService__getBufferedOutputStream(aFile) {
  var sinkFileStream = Cc["@mozilla.org/network/file-output-stream;1"]
                       .createInstance(Ci.nsIFileOutputStream);
  sinkFileStream.init(aFile, 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate

  var sinkBufferStream = Cc["@mozilla.org/network/buffered-output-stream;1"]
                         .createInstance(Ci.nsIBufferedOutputStream);
  sinkBufferStream.init(sinkFileStream,
                        this._FLOCK_IO_CHUNKSIZE); // write, create, truncate
  return sinkBufferStream;
}

flockPhotoUploadService.prototype._rotate =
function flockPhotoUploadService__rotate(aDegrees, aSrc, aDest) {
  var args = this._createArgs("convert", aSrc, "-rotate", aDegrees, aDest);
  this._getGM().doMagick(args, args.length);
}

flockPhotoUploadService.prototype._regenerateThumbs =
function flockPhotoUploadService__regenerateThumbs(aPhotoUpload,
                                                   aDegrees,
                                                   aX1,
                                                   aY1,
                                                   aX2,
                                                   aY2)
{
  this._logger.debug("_regenerateThumbs");
  this._logger.debug("     photo: " + aPhotoUpload.id);
  this._logger.debug("     degrees: " + aDegrees);
  this._logger.debug("     x1: " + aX1);
  this._logger.debug("     y1: " + aY1);
  this._logger.debug("     x2: " + aX2);
  this._logger.debug("     y2: " + aY2);

  var coopUpload = this._coop.get(aPhotoUpload.id);

  if (aX1 != null) {
    aPhotoUpload.crop = aX1 + "," + aY1 + "," + aX2 + "," + aY2;
    var newWorkingFile = this.doCrop(aPhotoUpload, 0, this._FLOCK_PREVIEW_SIZE);
    var newWorkingSpec = this._getURLFromFile(newWorkingFile);
    this._removeFileByPath(aPhotoUpload.workingFilePath);
    aPhotoUpload.workingFilePath = newWorkingFile.path;
    coopUpload.workingFilePath = newWorkingFile.path;
    aPhotoUpload.workingFileSpec = newWorkingSpec;
    coopUpload.workingFileSpec = newWorkingSpec;
  }

  var originalPath = aPhotoUpload.originalFilePath;
  var previewPath = aPhotoUpload.previewFilePath;
  var thumbPath = aPhotoUpload.thumbFilePath;
  var workingPath = aPhotoUpload.workingFilePath;

  var rotation = aDegrees;
  var newPreviewFileName = this._getUniqueFname(previewPath);
  var newPreviewFile = this._getTmpFile(newPreviewFileName);
  this._rotate(rotation, aPhotoUpload.workingFilePath, newPreviewFile.path);

  var newPreviewSpec = this._getURLFromFile(newPreviewFile);

  var newThumbFileName = this._getUniqueFname(thumbPath);
  var newThumbFile = this._getTmpFile(newThumbFileName);
  this._thumb(this._FLOCK_THUMB_SIZE,
              newPreviewFile.path,
              newThumbFile.path);
  var newThumbSpec = this._getURLFromFile(newThumbFile);

  aPhotoUpload.previewFilePath = newPreviewFile.path;
  coopUpload.previewFilePath = newPreviewFile.path;
  aPhotoUpload.previewFileSpec = newPreviewSpec;
  coopUpload.previewFileSpec = newPreviewSpec;

  aPhotoUpload.thumbFilePath = newThumbFile.path;
  coopUpload.thumbFilePath = newThumbFile.path;
  aPhotoUpload.thumbFileSpec = newThumbSpec;
  coopUpload.thumbFileSpec = newThumbSpec;
  aPhotoUpload.rotation = rotation + "";
  coopUpload.rotation = rotation + "";

  this._removeFileByPath(thumbPath);
  this._removeFileByPath(previewPath);
}


/**************************************************************************
 * Flock Photo Upload Service: flockIPhotoUploadService
 **************************************************************************/

flockPhotoUploadService.prototype.addListener = function(aUploadListener) {
  this.uploadListeners.push(aUploadListener);
  debug("adding photo upload listener " + this.uploadListeners.length + "\n");
}


flockPhotoUploadService.prototype.removeListener = function(aUploadListener) {
  for (var i = 0; i < this.uploadListeners.length; ++i) {
    if (aUploadListener == this.uploadListeners[i]) {
      this.uploadListeners.splice(i,1);
      break;
    }
  }
}


flockPhotoUploadService.prototype.hasOnePhotoAccount = function PUS_hasOnePhotoAccount () {
  // Iterate through all accounts, looking for photo upload accounts.
  var accountsEnum = this._coop.accounts_root.children.enumerate();
  while (accountsEnum.hasMoreElements()) {
    var acctCoopObj = accountsEnum.getNext();
    if (!acctCoopObj) continue; // getNext() can return NULL when hasMoreElements() is TRUE.
    var svcClass = Cc[acctCoopObj.serviceId];
    if (svcClass) {
      var svc = svcClass.getService(Ci.flockIWebService);
      if (svc instanceof Components.interfaces.flockIMediaUploadWebService) {
        return true;
      }
    }
  }

  return false;
}


flockPhotoUploadService.prototype.promptForAccount =
function PUS_promptForAccount() {
  var wm = Cc["@mozilla.org/appshell/window-mediator;1"]
           .getService(Ci.nsIWindowMediator);
  var win = wm.getMostRecentWindow('navigator:browser');
  var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"]
                      .getService(Ci.nsIPromptService);
  var title = flockGetString("photo/photo", "flock.photo.alert.title");
  var brand = brandGetString("brand", "brandShortName");
  var desc = flockGetString("photo/photo", "flock.photo.alert.text", [brand]);
  if (promptService.confirm(win, title, desc)) {
    var isAsSidebarOpen = win.isSidebarOpen("flock_AccountsSidebarBroadcaster");
    win.toggleSidebarWithMetric("flock_AccountsSidebarBroadcaster",
                                "AccountSidebar-Open",
                                null,
                                "PhotoUploaderNoAccounts",
                                true);
    var inst = this;
    // Ensure the Acounts and Services sidebar has in fact opened
    // before notifying the respective observers
    if (isAsSidebarOpen) {
      inst.obs.notifyObservers(null, "toggleAccountsArea", "media,people");
      inst.obs.notifyObservers(null,
                               "highlightSvcsByInterface",
                               "flockIMediaUploadWebService");
    } else {
      var sidebarOpenedObserver = {
        observe: function sideBarOpen_observe(aSubject, aTopic, aData) {
          if (aTopic == "accountsServicesSidebarReady") {
            inst.obs.removeObserver(this, "accountsServicesSidebarReady");
            inst.obs.notifyObservers(null, "toggleAccountsArea", "media,people");
            inst.obs.notifyObservers(null,
                                     "highlightSvcsByInterface",
                                     "flockIMediaUploadWebService");
          }
        }
      };
      this.obs.addObserver(sidebarOpenedObserver,
                           "accountsServicesSidebarReady",
                           false);
    }
  }
}


flockPhotoUploadService.prototype._showUploaderWindow = function PUS__showUploaderWindow () {
  // Set pref that specifies whether the uploader has been used
  var prefService = Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefService);
  prefService.getBranch("flock.photo.uploader.").setBoolPref("firstruncomplete", true);
  
  var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
  var uploaderWindow = wm.getMostRecentWindow("Flock:PhotoUploader");
  var win = wm.getMostRecentWindow('navigator:browser');
  if (uploaderWindow) {
    uploaderWindow.focus();
  } else {
    win.open("chrome://flock/content/photo/photoUploader.xul",
             "PhotoUploader",
             "chrome=1,resizable=1,width=680,height=660");
  }
}


flockPhotoUploadService.prototype.launchUploader = function PUS_launchUploader () {
  // Tell the user if he has to configure an account
  if (this.hasOnePhotoAccount())
    this._showUploaderWindow();
  else
    this.promptForAccount();
}


flockPhotoUploadService.prototype.getDefaultService = function() {
  var prefService = Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefService);
  try {
    var pref = prefService.getBranch("flock.photo.").getCharPref("defaultUploadService");
    if (pref && pref.length)
      return pref;
    return null;
  } catch(e) {
    return null;
  }
}

flockPhotoUploadService.prototype.setDefaultService = function(aServiceId) {
  var prefService = Cc['@mozilla.org/preferences-service;1'].getService(Ci.nsIPrefService);
  prefService.getBranch("flock.photo.").setCharPref("defaultUploadService", aServiceId);
}


flockPhotoUploadService.prototype.dictionary2Params = function (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;
}


flockPhotoUploadService.prototype.cancelUpload = function () {
  this.uploadListener.stop();
}

flockPhotoUploadService.prototype.__defineGetter__('isUploading', function () { return this.running; })
flockPhotoUploadService.prototype.__defineGetter__('isImporting', function () { return this._isImporting; })

flockPhotoUploadService.prototype.singleUpload =
function(aUploadListener, aAPI, aUpload) {
  var url = this.doTransform(aUpload);
  aAPI.upload(aUploadListener, aUpload, url);
  return;
}

flockPhotoUploadService.prototype.addMediaToImportQueue = function (aUrl) {
  this._logger.debug("addMediaToImportQueue " + aUrl);
  this.mImportQueue.push(aUrl.concat());
}

flockPhotoUploadService.prototype.importSingleMediaItem =
function(aUploadListener, aUrl) {
  this._isImporting = true;
  var inst = this;
  var listener = {
    onCreateStart: function(aTitle) {
      aUploadListener.onMediaImportStart(aTitle, 1);
    },
    
    onCreateEnd: function(aPhotoUpload) {
      inst._isImporting = false;
      aUploadListener.onMediaImportFinish(aPhotoUpload, 1);
    },
    
    onCreateError: function(aFilename) {
      inst._isImporting = false;
      aUploadListener.onMediaImportError(aFilename);
    }
  }
  this.createPhotoUpload(listener, aUrl);
}

flockPhotoUploadService.prototype.startImportQueue = function startImportQueue() {
  var inst = this;

  var processor = {
    count: 0,
    start: function() {
      for each (var ls in inst.uploadListeners) {
        ls.onMediaBatchImportStart(inst.mImportQueue.length);
      }
      this.next();
    },

    next: function() {
      if (inst.mImportQueue.length == 0) {
        this.finish();
        return;
      }
      this.count++;
      var url = inst.mImportQueue.shift();
      try {
        inst.createPhotoUpload(this, url);
      } catch (ex) {
        inst._logger.error(ex);
        this.onCreateError(url);
      }
    },

    onCreateStart: function(aTitle) {
      for each (var ls in inst.uploadListeners) {
        ls.onMediaImportStart(aTitle, this.count);
      }
    },

    onCreateEnd: function(aMediaUpload) {
      for each (var ls in inst.uploadListeners) {
        ls.onMediaImportFinish(aMediaUpload, this.count);
      }
     this.next();
    },

    onCreateError: function(aFilename) {
      for each (var ls in inst.uploadListeners) {
        ls.onMediaImportError(aFilename);
      }
    },

    onError: function(aError) {
      for each (var ls in inst.uploadListeners) {
        ls.onError(aError);
      }
    },

    finish: function() {
      inst._isImporting = false;
      for each (var ls in inst.uploadListeners) {
        ls.onMediaBatchImportFinish();
      }
    }
  }

  this._isImporting = true;
  processor.start();
}

flockPhotoUploadService.prototype.cancelImport = function () {
  //this.mAsyncCopier.cancel(null);
  this._isImporting = false;
  this.mImportQueue = Array(); // flush the array
  // signal we're done
  for each (var ls in this.uploadListeners) {
    ls.onMediaBatchImportFinish();
  }
}


flockPhotoUploadService.prototype._callCount = 0;
flockPhotoUploadService.prototype._getUniqueFname =
function flockPhotoUploadService__getUniqueFname(aFName) {
  aFName.match(/.+\.(.+)/);
  var ext = RegExp.$1;
  if(!ext) ext = "";
  var now = new Date();
  var name = aFName + now + this._callCount++;
  var sinkFileName = FlockCryptoHash.md5(name);
  sinkFileName = sinkFileName + "." + ext;
  return sinkFileName;
}

function getOtherFName(aFName, aType) {
  aFName.match(/.+\.(.+)/);
  var ext = RegExp.$1;
  debug(aFName + "." + aType + "." + ext + "\n");
  return aFName + "." + aType + "." + ext;
}

flockPhotoUploadService.prototype.cropPhotoUpload = function (aPhotoUpload, aX1, aY1, aX2, aY2) {
  var rotation = parseInt(aPhotoUpload.rotation);
  this._regenerateThumbs(aPhotoUpload, rotation, aX1, aY1, aX2, aY2);
}

flockPhotoUploadService.prototype.revertPhotoUpload = function (aPhotoUpload) {
  this._regenerateThumbs(aPhotoUpload,0,0,0,1,1);
}

flockPhotoUploadService.prototype.doCrop= function (aPhotoUpload, aDegrees, aSize) {
  var originalPath = aPhotoUpload.originalFilePath;
  var workingPath = aPhotoUpload.workingFilePath;
  var newWorkingFileName = this._getUniqueFname(workingPath);
  var newWorkingFile = this._getTmpFile(newWorkingFileName);
  var cropString = aPhotoUpload.crop;

  var rotation = parseInt(aPhotoUpload.rotation);
  var degrees = rotation;
  var size = null;

  if (aDegrees != null) degrees = aDegrees;
  if (aSize != null) size = aSize;
  // do the crop
  if ((cropString == "0,0,1,1") && (aDegrees == 0)) {
    this._resize(size, originalPath, newWorkingFile.path);
  }
  else {
    var coords = cropString.split(",");
    var x1, y1, x2, y2;
    x1 = parseFloat(coords[0]);
    y1 = parseFloat(coords[1]);
    x2 = parseFloat(coords[2]);
    y2 = parseFloat(coords[3]);


    var origDims = this._getDimensions(originalPath);

    var x = Math.floor(x1 * origDims.x);
    var y = Math.floor(y1 * origDims.y);
    var w = Math.ceil((x2 - x1) * origDims.x);
    var h = Math.ceil((y2 - y1) * origDims.y);
    this._cropExt(originalPath,
                  newWorkingFile.path,
                  w,
                  h,
                  x,
                  y,
                  degrees,
                  size,
                  origDims.px,
                  origDims.py);
  }

  // create the working file
  return newWorkingFile;
}


flockPhotoUploadService.prototype.rotatePhotoUpload =
function flockPhotoUploadService_rotatePhotoUpload(aPhotoUpload, aDegrees) {
  var rotation = (parseInt(aPhotoUpload.rotation) + aDegrees) % 360;
  this._regenerateThumbs(aPhotoUpload, rotation, null, null, null, null);
}

flockPhotoUploadService.prototype.createPhotoUpload = function (aListener, aUrl) {
  this._logger.debug("createPhotoUpload " + aUrl);
  var url = aUrl;

  var sourceFile = this._getFileFromURL(url);
  var sourceBufferStream = this._getBufferedInputStream(sourceFile);
  var sourceTitle = sourceFile.leafName;
  var sourceFilename = url;

  var sinkFileName = this._getUniqueFname(sourceFilename);
  var sinkFile = this._getTmpFile(sinkFileName);
  var sinkBufferStream = this._getBufferedOutputStream(sinkFile);

  this.mAsyncCopier.init(sourceBufferStream,
                         sinkBufferStream,
                         null,
                         true,
                         true,
                         this._FLOCK_IO_CHUNKSIZE);
  aListener.onCreateStart(sourceTitle); 

  var inst = this;

  var requestObserver = {
    fname: sourceTitle,
    asyncCopier: this.mAsyncCopier,
    url: url,
    sinkFile: sinkFile,
    sinkFileName: sinkFileName,
    preview: false,
    onStartRequest: function(aRequest, aContext) {
    },
    onStopRequest: function(aRequest, aContext) {
      var sourcePath = this.sinkFile.path;
      var thumbPath = getOtherFName(sourcePath, "thumb");
      var previewPath = getOtherFName(sourcePath, "preview");
      var workingPath = getOtherFName(sourcePath, "working");
      inst._resizeAsync(inst._FLOCK_PREVIEW_SIZE,
                        sourcePath,
                        previewPath,
                        this);
    },
    onFinished: function requestObserver_onFinished (aResult) {
      debug("onFinished: " + aResult + "\n");
      if (!inst._isImporting) return;
      var sourcePath = this.sinkFile.path;
      var thumbPath = getOtherFName(sourcePath, "thumb");
      var previewPath = getOtherFName(sourcePath, "preview");
      var workingFilePath = getOtherFName(sourcePath, "working");

      // if file size is zero
      if (!inst._getFileFromPath(sourcePath).fileSize) {
        var error = Cc["@flock.com/error;1"].createInstance(Ci.flockIError);
        error.errorCode = error.PHOTOSERVICE_FILE_EMPTY;
        aListener.onError(error);
        return;
      }
      
      try {
        if(!this.preview) {
          var previewFile = inst._getFileFromPath(previewPath);
          previewFile.copyTo(null, getOtherFName(this.sinkFile.leafName, "working"));
          this.preview = true;
          inst._thumbAsync(inst._FLOCK_THUMB_SIZE,
                           previewPath,
                           thumbPath,
                           this);
        } else {
          var thumbFile = inst._getFileFromPath(thumbPath);
          var thumbSpec = inst._getURLFromFile(thumbFile);

          var previewFile = inst._getFileFromPath(previewPath);
          var previewSpec = inst._getURLFromFile(previewFile);

          var workingFile = inst._getFileFromPath(workingFilePath);

          var date = new Date();
          
          var photo = Components.classes['@flock.com/photo-upload;1'].createInstance(Ci.flockIPhotoUpload);
          photo.originalFileSpec = inst._getURLFromFile(this.sinkFile);
          photo.originalFilePath = sourcePath;
          photo.workingFilePath = workingFilePath;
          photo.thumbFileSpec = thumbSpec;
          photo.thumbFilePath = thumbPath;
          photo.previewFileSpec = previewSpec;
          photo.previewFilePath = previewPath;
          photo.privacy_use_batch = "true";
          photo.is_public = "true";
          photo.tags = "";
          photo.notes = "";
          photo.description = "";
          photo.is_friend = "false";
          photo.is_family = "false";
          photo.title = this.fname;
          photo.fileName = this.fname;
          photo.state = "";
          photo.rotation = "0";
          photo.crop = "0,0,1,1";
          photo.id = "uri:" + FlockCryptoHash.md5(sourcePath);
          photo.album = ""; // null for now
          aListener.onCreateEnd(photo);
        }
      } catch (ex) {
        debug("photoupload: " + ex + " in " + ex.fileName+" line "+ex.lineNumber + "\n");
        debug("photoupload: " + ex.description + "\n");
        aListener.onCreateError(this.fname);
      }
    }
  };

  this.mAsyncCopier.asyncCopy(requestObserver, requestObserver.sinkFile);
}

flockPhotoUploadService.prototype.doTransform = function (aUpload) {
  var url = aUpload.originalFileSpec;
  var path = aUpload.originalFilePath;

  // if there are no changes to be made do not create new copy (as it changes jpg compression), just return path
  // see bug 3891

  if (FlockPrefsUtils.getBoolPref(this._FLOCK_PREF_PHOTO_RESIZE) ||
      aUpload.crop!="0,0,1,1" ||
      parseInt(aUpload.rotation) != 0)
  {
    var maxDim = null;
    if (FlockPrefsUtils.getBoolPref(this._FLOCK_PREF_PHOTO_RESIZE)) {
      var maxDim = FlockPrefsUtils.getIntPref(this._FLOCK_PREF_PHOTO_RESIZE_DIMENSIONS);
    }
    var newFile = this.doCrop(aUpload, null, maxDim);
    var newNotes = aUpload.notes;
    var coords = aUpload.crop.split(",");
    var x1, y1, x2, y2;
    x1 = parseFloat(coords[0]);
    y1 = parseFloat(coords[1]);
    x2 = parseFloat(coords[2]);
    y2 = parseFloat(coords[3]);
    var rotation = parseInt(aUpload.rotation);
    var xys = aUpload.notes.match(/"x":"\d+\.\d","y":"\d+\.\d"/g);
    if (xys) {
      for (var i = 0; i < xys.length; i++) {
        var xNew, yNew;
        var arr = xys[i].match(/"x":"(\d+\.\d)","y":"(\d+\.\d)"/);
        xNew = parseFloat(arr[1]) / 100.0;
        yNew = parseFloat(arr[2]) / 100.0;

        xNew = (xNew - x1) / (x2 - x1);
        yNew = (yNew - y1) / (y2 - y1);

        function rotateLeftNinetyDegrees() {
          var tmp = xNew;
          xNew = 1 - yNew;
          yNew = tmp;
        }

        var tmp = rotation;
        while (!isNaN(tmp) && tmp % 360 != 0) {
          rotateLeftNinetyDegrees();
          tmp -= 90;
        }

        xNew *= 100;
        yNew *= 100;
        var reNew = new RegExp(xys[i]);
        newNotes = newNotes.replace(reNew,
                      '"x":"' + xNew.toFixed(1)
                      + '","y":"' + yNew.toFixed(1) + '"');
      }
    }
    aUpload.notes = newNotes;

    url = this._getURLFromFile(newFile);

    this._transferExif(aUpload.originalFileSpec, url);
    return url;
  }

  return aUpload.originalFileSpec;
}

flockPhotoUploadService.prototype.upload = function (aAPI) {
  if(this.isUploading) { return; }
  var inst = this;
  this.api = aAPI;
  this.uploadListener = {
    init: function(aAPI, aKids, aSvc, aMax) {
      this.api = aAPI;
      this.svc = aSvc;
      this.kids = aKids;
      this.count = 0;
      this.max = aMax;
      this.imageUtils = Cc["@flock.com/imagescaler;1"]
                        .createInstance(Ci.flockIImageScaler);
      this.svc.running = true;
      this.doUpload();
    },
    stop: function() {
      this.svc.running = false;
      if (this.coopUpload && this.coopUpload.state == "uploading") {
        // The user cancelled the upload
        this.coopUpload.state = "cancelled";
      }
      for each (var ls in this.svc.uploadListeners) {
        ls.onUploadComplete();
      }
    },
    doUpload: function() {
      if (!this.svc.running) {
        return;
      }
      if (!this.kids.hasMoreElements()) {
        this.currentUpload = null;
        this.coopUpload = null;
        this.stop();
        return;
      }
      ++this.count;
      this.coopUpload = this.kids.getNext();
      var upload = this.svc.getPhotoUpload(this.coopUpload.id());
      var url = inst.doTransform(upload);
      var params = new Array();
      params.title = upload.title;
      params.description = upload.description;
      params.is_family = upload.is_family;
      params.is_friend = upload.is_friend;
      params.is_public = upload.is_public;
      params.async = "1";
      params.tags = upload.tags;
      params.notes = upload.notes;
      params.album = upload.album;
      this.currentUpload = upload;
      this.coopUpload.state = "uploading";
      for each (var ls in this.svc.uploadListeners) {
        ls.onUploadStart(this.currentUpload, this.count, this.max);
      }
      this.api.upload(this, upload, url);
    },
    onUploadComplete: function(aUpload) {
      this.doUpload();
      for each (var ls in this.svc.uploadListeners) {
        ls.onUpload(aUpload, this.count, this.max);
      }
      this.svc.removePhotoUpload(aUpload.id, false);
    },
    onUploadFinalized: function(aUpload, aPhoto) {
    },
    onError: function(aError) {
      this.coopUpload.state = "error";
      for each (var ls in this.svc.uploadListeners) {
        // Don't send back error on cancelUpload
        if (this.svc.running) { 
          ls.onUploadError(this.currentUpload, this.count, this.max, aError);
        }
      }
      this.stop();
      debug("error uploading\n");
    },
    onProgress: function(currentProgress) {
      for each (var ls in this.svc.uploadListeners) {
        ls.onUploadProgress(currentProgress);
      }
    }
  };

  // this._uploadRoot.children.count() is buggy
  var count = 0;
  var countEnum = this._uploadRoot.children.enumerate();
  while (countEnum.hasMoreElements()) {
    countEnum.getNext();
    count++;
  }

  // we have to reverse the order of the child nodes
  // so that the service gets the oldest photo first uploaded.
  var childrenEnum = this._uploadRoot.children.enumerateBackwards();

  this.uploadListener.init(this.api,
                           childrenEnum,
                           this,
                           count);
}


flockPhotoUploadService.prototype.savePhotoUpload = function (aUpload) {
  this._logger.debug("savePhotoUpload " + aUpload.id);
  this._logger.debug("savePhotoUpload is_public " + aUpload.is_public);

  var coopUpload = this._coop.get(aUpload.id);
  if (!coopUpload) {
    coopUpload = new this._coop.MediaUploadItem(aUpload.id);
  }
  for each (var attr in FlockUploadUtils.ATTRIBUTES) {
    coopUpload[attr] = aUpload[attr];
  }
}


flockPhotoUploadService.prototype.addPhotoUpload = function (aReference, aUpload) {
  debug("flockPhotoUploadService.prototype.addPhotoUpload " + aUpload.state + "\n");
  for each (var ls in this.uploadListeners) {
    ls.onUploadAdd(null);
  }

  var coopPhoto = this._coop.get(aUpload.id);
  if (!coopPhoto) {
    this.savePhotoUpload(aUpload);
    coopPhoto = this._coop.get(aUpload.id);
  }

  var index = 1; // RDF sequences start at 1, not 0
  if (aReference) {
    var coopReference = this._coop.get(aReference.id);
    if (coopReference) {
      var index = this._uploadRoot.children.indexOf(coopReference);
    }
  }
  this._uploadRoot.children.insertAt(coopPhoto, index);

  // Need a comment to explain why we do this twice. Bruno
  for each (var ls in this.uploadListeners) {
    ls.onUploadAdd(aUpload);
  }
}


flockPhotoUploadService.prototype.removePhotoUpload =
function flockPhotoUploadService__removePhotoUpload(aUploadId) {
  this._logger.debug("removePhotoUpload(" + aUploadId + ")");
  var coopPhoto = this._coop.get(aUploadId);
  if (coopPhoto) {
    coopPhoto.destroy();
  } else {
    this._logger.warn("Couldn't remove the photo upload id "
                      + aUploadId
                      + " because it doesn't exist");
  }

  for each (var ls in this.uploadListeners) {
    ls.onUploadRemove();
  }
}

flockPhotoUploadService.prototype.getPhotoUpload = function (aURI) {
  var coopUpload = this._coop.get(aURI);
  if (!coopUpload) {
    this._logger.error("getPhotoUpload: coudn't find photo id " + aURI);
    return null;
  }

  var result = Cc["@flock.com/photo-upload;1"]
               .createInstance(Ci.flockIPhotoUpload);
  result.id = coopUpload.id();
  for each (var attr in FlockUploadUtils.ATTRIBUTES) {
    result[attr] = coopUpload[attr];
  }

  return result;
}

flockPhotoUploadService.prototype.cleanPhotoUploadCache = function () {
  debug("Clean the cache.\n");
  try {
    var sinkFile = Cc["@mozilla.org/file/directory_service;1"]
      .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
    sinkFile.append("flock_imagecache");
    if (sinkFile.exists()) {
      debug("Image cache found\n");
      var files = sinkFile.directoryEntries;
      while (files.hasMoreElements()) {
        var file = files.getNext().QueryInterface(Ci.nsIFile);
        try {
          file.remove(false);
        } catch (e) {
          debug("could not delete file: " + file.path + " exception: " + e + "\n");
        }
      }
    } else {
      debug("Image cache not found\n");
    }
  } catch (e) {
    debug("exception cleaning the cache: " + e + "\n");
  }
}

/**************************************************************************
 * Flock Photo Upload Service: nsIObserver implementation
 **************************************************************************/

flockPhotoUploadService.prototype.observe =
function PUS_observe (subject, topic, state)
{
  switch (topic) {
    case 'flock-data-ready':
      this.obs.removeObserver(this, 'flock-data-ready');
      this._init();
      return;
  }
}


/**************************************************************************
 * XPCOM Support - Module Construction
 **************************************************************************/

// Create array of components.
var gComponentsArray = [flockPhotoUploadService];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      gComponentsArray);

/**************************************************************************
 * END XPCOM Support
 **************************************************************************/
