// 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/FlockCryptoHash.jsm");

const CACHE_DIR = "flock_imagecache";

var EXPORTED_SYMBOLS = ["FlockPhotoUploader"];

/******************************************************************************
 * Private functions
 ******************************************************************************/

function flock_getTmpFile(aFName) {
  var sinkFile = CC["@mozilla.org/file/directory_service;1"]
                 .getService(CI.nsIProperties)
                 .get("ProfD", CI.nsIFile);
  sinkFile.append(CACHE_DIR);
  if (!sinkFile.exists()) {
    sinkFile.createUnique(1, 0700);
  }
  if (aFName) {
    sinkFile.append(aFName);
  }
  return sinkFile;
}


/******************************************************************************
 * FlockPhotoUploader
 ******************************************************************************/

function FlockPhotoUploader() {}

FlockPhotoUploader.prototype = {
  endpoint: null,
  photoParam: "photo",
  contentType: "application/octet-stream"
}

FlockPhotoUploader.prototype.setEndpoint =
function FlockPhotoUploader_setEndpoint(aEndpoint) {
  this.endpoint = aEndpoint;
};

FlockPhotoUploader.prototype.setPhotoParam =
function FlockPhotoUploader_setPhotoParam(aPhotoParam) {
  this.photoParam = aPhotoParam;
};

FlockPhotoUploader.prototype.upload =
function FlockPhotoUploader_upload(aListener, aPhoto, aParams, aUpload) {
  try {
    var now = new Date();
    var tmpFileName = aPhoto + now;
    tmpFileName = "flock_" + FlockCryptoHash.md5(tmpFileName) + ".tmp";

    var ios = CC["@mozilla.org/network/io-service;1"]
              .getService(CI.nsIIOService);
    var fileHandler = ios.getProtocolHandler("file")
                         .QueryInterface(CI.nsIFileProtocolHandler);
    var fileFromSpec = fileHandler.getFileFromURLSpec(aPhoto);

    var boundaryString = "AaB03x";
    var contentType = "multipart/form-data; boundary=" + boundaryString;
    var boundary = "--" + boundaryString;


    var photoFile = CC["@mozilla.org/file/local;1"]
                    .createInstance(CI.nsILocalFile);
    photoFile.initWithFile(fileFromSpec);

    var photoStream = CC["@mozilla.org/network/file-input-stream;1"]
                      .createInstance(CI.nsIFileInputStream);
    photoStream.init(photoFile, 1, 0644, 0);

    var tmpFile = flock_getTmpFile(tmpFileName);

    var tmpFileStream = CC["@mozilla.org/network/file-output-stream;1"]
                        .createInstance(CI.nsIFileOutputStream);
    tmpFileStream.init(tmpFile, 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate

    var tmpBufferStream = CC["@mozilla.org/network/buffered-output-stream;1"]
                          .createInstance(CI.nsIBufferedOutputStream);

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

    tmpBufferStream.init(tmpFileStream, 65536 * 4);

    tmpBufferStream.write(boundary, boundary.length);
    tmpBufferStream.write("\r\n", 2);

    var fileName = fileFromSpec.leafName;
    if(aUpload) {
        fileName = aUpload.fileName;
    }
    var next = "";
    next = 'Content-Disposition: form-data; name="'
         + this.photoParam
         + '"; filename="'
         + fileName + '"';
    tmpBufferStream.write(next, next.length);
    tmpBufferStream.write("\r\n", 2);

    if(aUpload && aUpload.mimetype) {
      next = "Content-Type: " + aUpload.mimetype;
    } else {
      next = "Content-Type: " + this.contentType;
    }

    tmpBufferStream.write(next, next.length);
    tmpBufferStream.write("\r\n", 2);
    tmpBufferStream.write("\r\n", 2);

    tmpBufferStream.writeFrom(photoStream, photoFile.fileSize);
    tmpBufferStream.write("\r\n", 2);

    for(var p in aParams) {
      tmpBufferStream.write(boundary, boundary.length);
      tmpBufferStream.write("\r\n", 2);
      var next = 'Content-Disposition: form-data; name="' + p + '"';
      tmpBufferStream.write(next, next.length);
      tmpBufferStream.write("\r\n", 2);
      tmpBufferStream.write("\r\n", 2);
      var param = aParams[p] + "";
      var inputStream = converter.convertToInputStream(param);
      tmpBufferStream.writeFrom(inputStream, inputStream.available());
      //tmpBufferStream.write(param, param.length);
      tmpBufferStream.write("\r\n", 2);
    }

    tmpBufferStream.write(boundary, boundary.length);
    tmpBufferStream.write("--", 2);
    tmpBufferStream.write("\r\n", 2);
    tmpBufferStream.write("\r\n", 2);
    tmpBufferStream.close();
    tmpFileStream.close();

    url = this.endpoint;
    this.req = CC['@mozilla.org/xmlextras/xmlhttprequest;1']
               .createInstance(CI.nsIXMLHttpRequest);
    var req = this.req;
    var inst = this;
    var onReadyStateFunc = function upload_onReadyStateFunc(aEvt) {
      if (req.readyState == 4) {
        if (Math.floor(req.status/100) == 2) {
          // Return the responseText and parse at a service level
          // as some services require XML, and others JSON
          aListener.onResult(req.responseText);
        } else {
          // HTTP errors (0 for connection lost)
          aListener.onError(req.status);
        }
      }
    };

    var onProgressStateFunc = function upload_onProgressStateFunc(aEvt) {
      var currentProgress = 0;
      if (aEvt.totalSize > 0) {
        currentProgress = (aEvt.position / aEvt.totalSize) * 100;
      }
      aListener.onProgress(currentProgress);
    };

    // This will make it so we don't invoke an XML parser
    req.overrideMimeType("text/txt");
    // Detach the xmlhttprequest from this document
    req.mozBackgroundRequest = true;

    // We have to attach the progress functions before calling open
    // This lets the nsXMLHttpRequest component know that we want async and
    // we want a progress listener attached.
    req.onreadystatechange = onReadyStateFunc;
    req.onuploadprogress = onProgressStateFunc;

    req.open("POST",url,true);
    req.setRequestHeader("Content-Type", contentType);

    var tmpInputStream = CC["@mozilla.org/network/file-input-stream;1"]
                         .createInstance(CI.nsIFileInputStream);
    tmpInputStream.init(tmpFile, 1, 0644, 0);

    var tmpInputBufferStream = CC["@mozilla.org/network/buffered-input-stream;1"]
                               .createInstance(CI.nsIBufferedInputStream);

    tmpInputBufferStream.init(tmpInputStream, 65536 * 4);
    req.send(tmpInputBufferStream);
  }
  catch(e) {
    dump(e+"\n");
    aListener.onError(null);
  }
};

// with XML data
FlockPhotoUploader.prototype.uploadWithXml =
function FlockPhotoUploader_uploadWithXml(aListener, aPhoto, aParams, aUpload) {
  var now = new Date();
  var tmpFileName = aPhoto + now;
  tmpFileName = "flock_" + FlockCryptoHash.md5(tmpFileName) + ".tmp";

  var ios = CC["@mozilla.org/network/io-service;1"]
            .getService(CI.nsIIOService);
  var fileHandler = ios.getProtocolHandler("file")
      .QueryInterface(CI.nsIFileProtocolHandler);
  var fileFromSpec = fileHandler.getFileFromURLSpec(aPhoto);

  var photoFile = CC["@mozilla.org/file/local;1"]
                  .createInstance(CI.nsILocalFile);
  photoFile.initWithFile(fileFromSpec);

  var photoStream = CC["@mozilla.org/network/file-input-stream;1"]
                    .createInstance(CI.nsIFileInputStream);
  photoStream.init(photoFile, 1, 0644, 0);

  var tmpFile = flock_getTmpFile(tmpFileName);

  var tmpFileStream = CC["@mozilla.org/network/file-output-stream;1"]
                      .createInstance(CI.nsIFileOutputStream);
  tmpFileStream.init(tmpFile, 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate

  var tmpBufferStream = CC["@mozilla.org/network/buffered-output-stream;1"]
                        .createInstance(CI.nsIBufferedOutputStream);

  tmpBufferStream.init(tmpFileStream, 65536 * 4);

  var contentType = aParams.headers["Content-Type"];
  var auth = aParams.headers["Authorization"];
  var boundaryString = aParams.boundaryString;
  var boundary = "--" + boundaryString;

  var prePhotoTxt = "";
  prePhotoTxt = "Media multipart posting"
              + "\r\n"
              + boundary
              + "\r\n"
              + "Content-Type: application/atom+xml"
              + "\r\n"
              + "\r\n"
              + aParams.dataXml
              + "\r\n"
              + boundary
              + "\r\n"
              + "Content-Type: image/jpeg"
              + "\r\n"
              + "\r\n";
  tmpBufferStream.write(prePhotoTxt, prePhotoTxt.length);

  var fileName = fileFromSpec.leafName;
  if(aUpload) {
    fileName = aUpload.fileName;
  }

  // Write from photo data stream
  tmpBufferStream.writeFrom(photoStream, photoFile.fileSize);
  bodyLength += photoFile.fileSize;

  var postPhotoTxt = "\r\n"
                   + boundary + "--";
  tmpBufferStream.write(postPhotoTxt, postPhotoTxt.length);

  tmpBufferStream.close();
  tmpFileStream.close();

  var bodyLength = prePhotoTxt.length
                 + photoFile.fileSize
                 + postPhotoTxt.length;

  url = this.endpoint;
  this.req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
             .createInstance(CI.nsIXMLHttpRequest);
  var req = this.req;
  var inst = this;
  var onReadyStateFunc = function uploadWithXml_onReadyStateFunc(aEvt) {
    if (req.readyState == 4) {
      if (Math.floor(req.status/100) == 2) {
        aListener.onResult(req.responseXML);
      } else {
        // HTTP errors (0 for connection lost)
        var errorCode;
        // Picasa upload error codes follow the syntax: (nnn) - some error text
        if (req.responseText.match(/^\(([0-9]+)\)/)) {
          errorCode = RegExp.$1;
        } else {
          errorCode = req.status;
        }
        aListener.onError(errorCode);
      }
    }
  };

  var onProgressStateFunc = function uploadWithXml_onProgressStateFunc(aEvt) {
    var currentProgress = 0;
    if (aEvt.totalSize > 0) {
      currentProgress = (aEvt.position / aEvt.totalSize) * 100;
    }
    aListener.onProgress(currentProgress);
  };

  // Detach the xmlhttprequest from this document
  req.mozBackgroundRequest = true;

  // We have to attach the progress functions before calling open
  // This lets the nsXMLHttpRequest component know that we want async and
  // we want a progress listener attached.
  req.onreadystatechange = onReadyStateFunc;
  req.onuploadprogress = onProgressStateFunc;

  req.open("POST", url, true);
  req.setRequestHeader("Content-Type", contentType);
  req.setRequestHeader("Content-Length", bodyLength);
  req.setRequestHeader("MIME-version", "1.0");
  req.setRequestHeader("Authorization", auth);

  var tmpInputStream = CC["@mozilla.org/network/file-input-stream;1"]
                       .createInstance(CI.nsIFileInputStream);
  tmpInputStream.init(tmpFile, 1, 0644, 0);

  var tmpInputBufferStream = CC["@mozilla.org/network/buffered-input-stream;1"]
                             .createInstance(CI.nsIBufferedInputStream);

  tmpInputBufferStream.init(tmpInputStream, 65536 * 4);
  req.send(tmpInputBufferStream);
};

FlockPhotoUploader.prototype.uploadBase64 =
function FlockPhotoUploader_uploadBase64(aListener,
                                    aPhoto,
                                    aUpload,
                                    aMethod,
                                    aHeader)
{
  var tmpFileName = "flock_"
                  + FlockCryptoHash.md5(aPhoto +  new Date())
                  + ".tmp";
  var tmpFile = flock_getTmpFile(tmpFileName);

  var ios = CC["@mozilla.org/network/io-service;1"]
            .getService(CI.nsIIOService);
  var fileHandler = ios.getProtocolHandler("file")
                       .QueryInterface(CI.nsIFileProtocolHandler);

  // Converts the URL string into the corresponding nsIFile
  var fileFromSpec = fileHandler.getFileFromURLSpec(aPhoto);
  var fileName = fileFromSpec.leafName;
  if (aUpload && aUpload.fileName) {
    fileName = aUpload.fileName;
  }
  // Grab the extension from the fileName
  var fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1);

 // Create a local file object
  var photoFile = CC["@mozilla.org/file/local;1"]
                  .createInstance(CI.nsILocalFile);

  // Initialize with the nsIFile
  photoFile.initWithFile(fileFromSpec);

  // Create a file stream to read in the data for base64 conversion
  var photoStream = CC["@mozilla.org/network/file-input-stream;1"]
                    .createInstance(CI.nsIFileInputStream);
  photoStream.init(photoFile, 1, 0644, 0);

  // Create a binary stream from the local file
  var inStream = CC["@mozilla.org/binaryinputstream;1"]
                 .createInstance(CI.nsIBinaryInputStream);
  inStream.setInputStream(photoStream);

  // Read in and base64 encode the photo
  var base64Encoded = btoa(inStream.readBytes(inStream.available()));

  var outFileStream = CC["@mozilla.org/network/file-output-stream;1"]
                      .createInstance(CI.nsIFileOutputStream);
  // Write, create, truncate
  outFileStream.init(tmpFile, 0x02 | 0x08 | 0x20, 0664, 0);

  var outStream = CC["@mozilla.org/binaryoutputstream;1"]
                  .createInstance(CI.nsIBinaryOutputStream);
  outStream.setOutputStream(outFileStream);

  // Buffer the output stream
  var bufferStream = CC["@mozilla.org/network/buffered-output-stream;1"]
                     .createInstance(CI.nsIBufferedOutputStream);
  bufferStream.init(outStream, 65536 * 4);

  // Write our base64 encoded image data to the buffered stream
  bufferStream.write(base64Encoded, base64Encoded.length);

  // Close the stream
  bufferStream.close();
  outStream.close();

  // Create the XHR
  this.req = CC['@mozilla.org/xmlextras/xmlhttprequest;1']
             .createInstance(CI.nsIXMLHttpRequest);
  var req = this.req;
  var inst = this;
  var onReadyStateFunc = function uploadBase64_onReadyStateFunc(aEvt) {
    if (req.readyState == 4) {
      if (Math.floor(req.status/100) == 2) {
        var dom = CC["@mozilla.org/xmlextras/domparser;1"]
                  .createInstance(CI.nsIDOMParser)
                  .parseFromString(req.responseText, "text/xml");
        aListener.onResult(dom);
      } else {
        // HTTP errors (0 for connection lost)
        aListener.onError(req.status);
      }
    }
  };

  var onProgressStateFunc =
  function uploadBase64_onProgressStateFunc(aEvt) {
    var currentProgress = 0;
    if (aEvt.totalSize > 0) {
      currentProgress = (aEvt.position / aEvt.totalSize) * 100;
    }
    aListener.onProgress(currentProgress);
  };

  // This will make it so we don't invoke an XML parser
  req.overrideMimeType("text/txt");

  // Detach the xmlhttprequest from this document
  req.mozBackgroundRequest = true;

  // We have to attach the progress functions before calling open
  // This lets the nsXMLHttpRequest component know that we want async and
  // we want a progress listener attached.
  req.onreadystatechange = onReadyStateFunc;
  req.onuploadprogress = onProgressStateFunc;

  req.open(aMethod, this.endpoint, true);
  // Set any specified request headers, blank strings are Ok
  for (var key in aHeader) {
    if (key && aHeader[key] != null) {
      req.setRequestHeader(key, aHeader[key]);
    }
  }

  // Set the Content-Length of our base64 encoded data
  req.setRequestHeader("Content-Length", base64Encoded.length);

  // Create a buffered input stream to send for interaction
  var sendInputStream = CC["@mozilla.org/network/file-input-stream;1"]
                        .createInstance(CI.nsIFileInputStream);
  sendInputStream.init(tmpFile, 1, 0644, 0);
  var sendInputBufferStream = CC["@mozilla.org/network/buffered-input-stream;1"]
                              .createInstance(CI.nsIBufferedInputStream);
  sendInputBufferStream.init(sendInputStream, 65536 * 4);

  // Send the buffer stream
  req.send(sendInputBufferStream);
};
