// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2009
// 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";

const FLOCK_IO_CHUNKSIZE = 65536 * 4;
const FLOCK_THUMB_SIZE = 75;
const FLOCK_PREVIEW_SIZE = 300;

const PREF_PHOTO_RESIZE = "flock.photo.resize";
const PREF_PHOTO_RESIZE_DIMENSIONS = "flock.photo.resize.dimensions";

const IMG_EXT = ".jpg .jpeg .gif .png .bmp";

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


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

// Constructor.
function flockPhotoUploadService() {
  this._obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);

  this._isRunning = false;
  this._isImporting = false;

  this._uploadObservers = [];
  this._importQueue = [];
  this._asyncCopier = CC["@mozilla.org/network/async-stream-copier;1"]
                      .createInstance(CI.nsIAsyncStreamCopier);
  this._init();
}


/**************************************************************************
 * 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
  ]
);

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

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

flockPhotoUploadService.prototype.hasOnePhotoAccount =
function PhotoUS_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 CI.flockIMediaUploadWebService) {
        return true;
      }
    }
  }

  return false;
};

flockPhotoUploadService.prototype.getDefaultService =
function PhotoUS_getDefaultService() {
  var prefService = CC['@mozilla.org/preferences-service;1']
                    .getService(CI.nsIPrefService);
  // XXX Should we do this with if() instead of try/catch?
  try {
    var pref = prefService.getBranch("flock.photo.")
                          .getCharPref("defaultUploadService");
    if (pref && pref.length) {
      return pref;
    }
    return null;
  } catch(ex) {
    return null;
  }
};

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

flockPhotoUploadService.prototype.promptForAccount =
function PhotoUS_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.launchUploader =
function PhotoUS_launchUploader() {
  // Tell the user if he has to configure an account
  if (this.hasOnePhotoAccount()) {
    this._showUploaderWindow();
  } else {
    this.promptForAccount();
  }
};

flockPhotoUploadService.prototype.importSingleMediaItem =
function PhotoUS_importSingleMediaItem(aUploadObserver, aFileName) {
  this._isImporting = true;
  var inst = this;
  var importListener = {
    onCreateStart: function importListener_onCreateStart(aTitle) {
      aUploadObserver.onMediaImportStart(aTitle, 1);
    },

    onCreateEnd: function importListener_onCreateEnd(aPhotoUpload) {
      inst._isImporting = false;
      aUploadObserver.onMediaImportFinish(aPhotoUpload, 1);
    },

    onCreateError: function importListener_onCreateError(aFilename) {
      inst._isImporting = false;
      aUploadObserver.onMediaImportError(aFilename);
    },

    onError: function importListener_onError(aError) {
      inst._isImporting = false;
      aUploadObserver.onError(aError);
    }
  };
  this._createPhotoUpload(importListener, aFileName);
};

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

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

  var processor = {
    count: 0,
    start: function processor_start() {
      for each (var obs in inst._uploadObservers) {
        obs.onMediaBatchImportStart(inst._importQueue.length);
      }
      this.next();
    },

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

    onCreateStart: function processor_onCreateStart(aTitle) {
      for each (var obs in inst._uploadObservers) {
        obs.onMediaImportStart(aTitle, this.count);
      }
    },

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

    onCreateError: function processor_onCreateError(aFilename) {
      for each (var obs in inst._uploadObservers) {
        obs.onMediaImportError(aFilename);
      }
    },

    onError: function processor_onError(aError) {
      for each (var obs in inst._uploadObservers) {
        obs.onError(aError);
      }
    },

    finish: function processor_finish() {
      inst._isImporting = false;
      for each (var obs in inst._uploadObservers) {
        obs.onMediaBatchImportFinish();
      }
    }
  };

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

flockPhotoUploadService.prototype.cancelImport =
function PhotoUS_cancelImport() {
  //this._asyncCopier.cancel(null);
  this._isImporting = false;
  this._importQueue = []; // flush the array
  // signal we're done
  for each (var obs in this._uploadObservers) {
    obs.onMediaBatchImportFinish();
  }
};

flockPhotoUploadService.prototype.upload =
function PhotoUS_upload(aAPI) {
  if (this.isUploading) {
    return;
  }
  var inst = this;
  this._uploadListener = {
    _init: function ul__init(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._isRunning = true;
      this._doUpload();
    },
    _stop: function ul__stop() {
      this.svc._isRunning = false;
      if (this.coopUpload && this.coopUpload.state == "uploading") {
        // The user cancelled the upload
        this.coopUpload.state = "cancelled";
      }
      for each (var obs in this.svc._uploadObservers) {
        obs.onUploadComplete();
      }
    },
    _doUpload: function ul__doUpload() {
      if (!this.svc._isRunning) {
        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);
      this.currentUpload = upload;
      this.coopUpload.state = "uploading";
      for each (var obs in this.svc._uploadObservers) {
        obs.onUploadStart(this.currentUpload, this.count, this.max);
      }
      this.api.upload(this, upload, url);
    },
    onProgress: function ul_onProgress(aCurrentProgress) {
      for each (var obs in this.svc._uploadObservers) {
        obs.onUploadProgress(aCurrentProgress);
      }
    },
    onUploadComplete: function ul_onUploadComplete(aUpload) {
      for each (var obs in this.svc._uploadObservers) {
        obs.onUpload(aUpload, this.count, this.max);
      }
      this.svc.removePhotoUpload(aUpload.id, false);
    },
    onUploadFinalized: function ul_onUploadFinalized(aUpload, aMediaItem) {
      for each (var obs in this.svc._uploadObservers) {
        obs.onUploadFinalized(aUpload, aMediaItem);
      }
      this._doUpload();
    },
    onError: function ul_onError(aError) {
      this.coopUpload.state = "error";
      for each (var obs in this.svc._uploadObservers) {
        // Don't send back error on cancelUpload
        if (this.svc._isRunning) {
          obs.onUploadError(this.currentUpload, this.count, this.max, aError);
        }
      }
      inst._logger.error("error uploading");
      this._stop();
    }
  };

  // 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(aAPI, childrenEnum, this, count);
};

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

flockPhotoUploadService.prototype.cancelUpload =
function PhotoUS_cancelUpload() {
  this._uploadListener._stop();
};

flockPhotoUploadService.prototype.addPhotoUpload =
function PhotoUS_addPhotoUpload(aReference, aUpload) {
  // TODO: change the logic to avoid having to call the observers twice;
  // eliminate the first call if possible, add a new method otherwise
  // (don't use the same method for 2 separate events)
  this._logger.debug("addPhotoUpload " + aUpload.state);
  var obs;
  // This call informs observers that we are going to add a photo.
  for each (obs in this._uploadObservers) {
    obs.onUploadAdd(null);
  }

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

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

  // This call informs observers that we just added a photo.
  for each (obs in this._uploadObservers) {
    obs.onUploadAdd(aUpload);
  }
};

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

  var coopUpload = this._coop.get(aUpload.id);
  if (!coopUpload) {
    coopUpload = new this._coop.MediaUploadItem(aUpload.id);
  }
  // We have to create a copy of the photoUpload before saving it to RDF.
  // Otherwise the observers of the slideybox template will be triggered when 
  // we are writing its first property and will cause a reload before the 
  // other properties have been saved.
  var photoCopy = CC["@flock.com/photo-upload;1"]
                  .createInstance(CI.flockIPhotoUpload);
  for each (var attr in FlockUploadUtils.ATTRIBUTES) {
    photoCopy[attr] = aUpload[attr];
  }

  for each (var attr in FlockUploadUtils.ATTRIBUTES) {
    coopUpload[attr] = photoCopy[attr];
  }
  return coopUpload;
};

flockPhotoUploadService.prototype.removePhotoUpload =
function PhotoUS_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 obs in this._uploadObservers) {
    obs.onUploadRemove();
  }
};

flockPhotoUploadService.prototype.getPhotoUpload =
function PhotoUS_getPhotoUpload(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.addObserver =
function PhotoUS_addObserver(aUploadObserver) {
  this._uploadObservers.push(aUploadObserver);
  this._logger.debug("adding photo upload listener "
                     + this._uploadObservers.length);
};


flockPhotoUploadService.prototype.removeObserver =
function PhotoUS_removeObserver(aUploadObserver) {
  for (var i = 0; i < this._uploadObservers.length; i++) {
    if (aUploadObserver == this._uploadObservers[i]) {
      this._uploadObservers.splice(i,1);
      break;
    }
  }
};

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

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

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

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

flockPhotoUploadService.prototype.__defineGetter__('isUploading',
function PhotoUS_isUploading() {
  return this._isRunning;
});

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



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

flockPhotoUploadService.prototype._callCount = 0;

flockPhotoUploadService.prototype._init =
function PhotoUS__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 PhotoUS__getGM() {
  if (this._gm) {
    return this._gm;
  }
  this._gm = CC["@flock.com/imagescaler;1"]
             .getService(CI.flockIImageScaler);
  return this._gm;
};

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

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

flockPhotoUploadService.prototype._resize =
function PhotoUS__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 PhotoUS__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 PhotoUS__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 PhotoUS__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 PhotoUS__cropExt(aSrc,
                                          aDest,
                                          aWidth,
                                          aHeight,
                                          aHorzOffset,
                                          aVertOffset,
                                          aRot,
                                          aSize)
{
  var dims = aWidth + "x" + aHeight;
  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 PhotoUS__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 PhotoUS__transferExif(aSourceUrl, aTargetUrl) {
  var sourceFile = this._getFileFromURL(aSourceUrl);
  var targetFile = this._getFileFromURL(aTargetUrl);
  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 PhotoUS__readFile(aFile) {
  var inStream;
  var sInStream;
  try {
     inStream = CC["@mozilla.org/network/file-input-stream;1"]
                .createInstance(CI.nsIFileInputStream);
     inStream.init(aFile, 0x01, 0444, null);

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

  var str;
  try {
    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 PhotoUS__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 PhotoUS__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 PhotoUS__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 PhotoUS__getFileFromPath(aPath) {
  var file = CC["@mozilla.org/file/local;1"]
             .createInstance(CI.nsILocalFile);
  file.initWithPath(aPath);
  return file;
};

flockPhotoUploadService.prototype._removeFileByPath =
function PhotoUS__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 PhotoUS__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, FLOCK_IO_CHUNKSIZE);

  return sourceBufferStream;
};

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

  var sinkBufferStream = CC["@mozilla.org/network/buffered-output-stream;1"]
                         .createInstance(CI.nsIBufferedOutputStream);
  sinkBufferStream.init(sinkFileStream, FLOCK_IO_CHUNKSIZE);

  return sinkBufferStream;
};

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

flockPhotoUploadService.prototype._regenerateThumbs =
function PhotoUS__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);
  if (aX1 != null) {
    aPhotoUpload.crop = aX1 + "," + aY1 + "," + aX2 + "," + aY2;
    var newWorkingFile = this._doCrop(aPhotoUpload, 0, FLOCK_PREVIEW_SIZE);
    var newWorkingSpec = this._getURLFromFile(newWorkingFile);
    this._removeFileByPath(aPhotoUpload.workingFilePath);
    aPhotoUpload.workingFilePath = newWorkingFile.path;
    aPhotoUpload.workingFileSpec = newWorkingSpec;
  }

  var previewPath = aPhotoUpload.previewFilePath;
  var thumbPath = aPhotoUpload.thumbFilePath;

  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(FLOCK_THUMB_SIZE, newPreviewFile.path, newThumbFile.path);
  var newThumbSpec = this._getURLFromFile(newThumbFile);

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

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

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

flockPhotoUploadService.prototype._showUploaderWindow =
function PhotoUS__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._getUniqueFname =
function PhotoUS__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;
};

flockPhotoUploadService.prototype._removeExtension =
function PhotoUS__removeExtension(aFile) {
  var exts = IMG_EXT.split(" ");
  var result = aFile;
  for(var i = 0; i < exts.length; i++) {
    var matchstr = "^(.*)\\" + exts[i] + "$";
    var myRegExp = new RegExp(matchstr,"i");
    if (aFile.match(myRegExp)) {
      result = RegExp.$1;
      break;
    }
  }
  return result;
};

flockPhotoUploadService.prototype._doCrop=
function PhotoUS__doCrop(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._doTransform =
function PhotoUS__doTransform(aUpload) {
  var url = aUpload.originalFileSpec;
  var xNew, yNew;
  function rotateLeftNinetyDegrees() {
    var tmp = xNew;
    xNew = 1 - yNew;
    yNew = tmp;
  }

  // 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(PREF_PHOTO_RESIZE) ||
      aUpload.crop!="0,0,1,1" ||
      parseInt(aUpload.rotation) != 0)
  {
    var maxDim = null;
    if (FlockPrefsUtils.getBoolPref(PREF_PHOTO_RESIZE)) {
      maxDim = FlockPrefsUtils.getIntPref(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 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);

        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._createPhotoUpload =
function PhotoUS__createPhotoUpload(aListener, aUrl) {
  this._logger.debug("_createPhotoUpload " + aUrl);
  var url = aUrl;

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

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

  this._asyncCopier.init(sourceBufferStream,
                         sinkBufferStream,
                         null,
                         true,
                         true,
                         FLOCK_IO_CHUNKSIZE);
  aListener.onCreateStart(sourceTitle);

  var inst = this;

  var createListener = {
    fname: sourceTitle,
    asyncCopier: this._asyncCopier,
    url: url,
    sinkFile: sinkFile,
    sinkFileName: sinkFileName,
    preview: false,
    onStartRequest: function createListener_onStartRequest(aRequest, aContext) {
    },
    onStopRequest: function createListener_onStopRequest(aRequest, aContext) {
      var sourcePath = this.sinkFile.path;
      var previewPath = getOtherFName(sourcePath, "preview");
      inst._resizeAsync(FLOCK_PREVIEW_SIZE, sourcePath, previewPath, this);
    },
    onFinished: function createListener_onFinished(aResult) {
      inst._logger.debug("onFinished: " + aResult);
      if (!inst._isImporting) {
        return;
      }
      var sourcePath = this.sinkFile.path;
      var thumbPath = getOtherFName(sourcePath, "thumb");
      var previewPath = getOtherFName(sourcePath, "preview");
      var workingFilePath = getOtherFName(sourcePath, "working");
      var previewFile;

      // 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) {
          previewFile = inst._getFileFromPath(previewPath);
          previewFile.copyTo(null,
                             getOtherFName(this.sinkFile.leafName,
                             "working"));
          this.preview = true;
          inst._thumbAsync(FLOCK_THUMB_SIZE, previewPath, thumbPath, this);
        } else {
          var thumbFile = inst._getFileFromPath(thumbPath);
          var thumbSpec = inst._getURLFromFile(thumbFile);

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

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

  this._asyncCopier.asyncCopy(createListener, createListener.sinkFile);
};


/**************************************************************************
 * 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
 **************************************************************************/
