/*
 * enlightnerService.js
 */

const CI = Components.interfaces;
const CL = Components.classes;

const CONSOLE = CL['@mozilla.org/consoleservice;1'].getService(CI.nsIConsoleService);

/**
 * Helpers
 */

var DaimFactory = 
{
  createInstance : function ( aCID, anInterface, aClsId ) 
  {
    var _Instance = Components.classes[aCID].createInstance(anInterface);
    if(aClsId)   _Instance.createInstanceOf(aClsId,1);
    else         _Instance.instanciate();
    return _Instance;
  },

  getService : function ( aCID, anInterface, aService ) 
  {
    var _Instance = Components.classes[aCID].createInstance(anInterface);
    _Instance.getResidentService(aService);
    return _Instance;
  }
};


var hexChars = "0123456789ABCDEF";

// Returns the decimal value of a hex character
function hex2dec(hex)
{
  return hexChars.indexOf(hex);
}

// return the hexadecimal value of a decimal digit from 1-16
function dec2hex(dec)
{
  return hexChars.charAt(dec);
}

function getHexString( r, g, b )
{
  var c = new Array();
  c[0] = dec2hex(Math.floor(r / 16));
  c[1] = dec2hex(r % 16);
  c[2] = dec2hex(Math.floor(g / 16));
  c[3] = dec2hex(g % 16);
  c[4] = dec2hex(Math.floor(b / 16));
  c[5] = dec2hex(b % 16);
  return c.join("");
} 

/* ==================================================== */

const STRETCH_BILINEAR  = 2;

const spotlight_kTmpBuffer    = 0; // Reserve Buffer 0 as temporary buffer
const spotlight_kMaskBuffer   = 1;
const spotlight_kFilterKernel = "Smooth/Gauss(8)-Isotrope";

 
function spotlightService()
{
  this.wrappedJSObject = this;
}

spotlightService.prototype = {

  _imagelist   :null, 
  _parameters  :null,
  _colorspace  :null,
  _filters     :null,
  _imagemath   :null,
  _transforms  :null,
  _maskbuffers :null,
  _histogram   :null,

  _canevas     :null,
  _geometry    :CI.nsIDaimGeometry.DAIM_Rectangle,
  
  _imageListURI:null,
  _originalURI :null,
  
  _drawBorder  : false,
  _borderSize   : 1,
  _borderColor  : [255,255,255],
  _borderOpacity: 100.0,
  _rectangle    : null ,
    
  _initialize : function()
  {
    // Create imagelist as a DAIM resident service
    // so that it can be accessible from the `daim://' protocole       
    if(this._imageListURI)             
       this._imagelist = DaimFactory.getService( "@daim.project/imagelist;1",
                                      Components.interfaces.nsIDaimImageList,
                                      this._imageListURI);
    else
       this._imagelist = DaimFactory.createInstance( "@daim.project/imagelist;1",
                                      Components.interfaces.nsIDaimImageList);
      
    // Create a Parameter objects which will hold the images list
    this._parameters =  DaimFactory.createInstance("@daim.project/parameters;1",
                                        Components.interfaces.nsIDaimParameters);
  
    this._parameters.imageList = this.imagelist;
    this._colorspace  = this._parameters.colorSpace;
    this._maskbuffers = this._parameters.maskBuffer;

    // Filter handler
    this._filters    = DaimFactory.createInstance("@daim.project/filters;1",
                                  Components.interfaces.nsIDaimFilters);

    // Image arithmetics
    this._imagemath  = DaimFactory.createInstance("@daim.project/imagemath;1",
                                  Components.interfaces.nsIDaimImageMath);

    // Geometric transformations
    this._transforms = DaimFactory.createInstance("@daim.project/transforms;1",
                                  Components.interfaces.nsIDaimTransforms);
         
    // Histogram manipulation
    this._histogram = DaimFactory.createInstance("@daim.project/histograms;1",
                                  Components.interfaces.nsIDaimHistograms);                             

    // Drawing helper
    this._canevas = DaimFactory.createInstance("@daim.project/draw2d;1",
                                  Components.interfaces.nsIDaimDraw2d);                             

    this._canevas.pattern  = 1; // Draw lines
    this._canevas.drawMode = Components.interfaces.nsIDaimDraw2d.NSDAIM_DRAW_OUTLINE;;
   },

   init : function( uri )
   {
     this._imageListURI = uri;
     this._initialize();
   },

   releaseAllBuffers: function()
   {
     this._imagelist.releaseAll();
   },

   copyBuffer: function( srcBuffer, dstBuffer )
   {
     this._imagelist.cloneBuffer(srcBuffer,dstBuffer);
   },

   releaseBuffer: function( buffer )
   {
     this._imagelist.releaseBuffer(buffer);
   },

   loadImage : function( uri, srcBuffer, observers ) 
   {
     var url = uri;
     if( url instanceof CI.nsIURI ) {
       if(url.schemeIs("file"))
       {
         // XXX workaround to uri normalized encoding
         var ioSrvc  = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
        

         var prot   = ioSrvc.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
         url = "file://" + prot.getFileFromURLSpec(url.spec).path;
       }
       else
        url = url.spec;
     }
     this._imagelist.loadBuffer(url,srcBuffer);  
   },

   cropImage : function( left, top, width, height, srcBuffer, dstBuffer )
   {     
     // Release working buffer
     // this._imagelist.releaseBuffer(dstBuffer); 

     if(!this._imagelist.isEmpty(srcBuffer)) 
     {
       var params = this._parameters;

       // Crop original image  to that bounding rectangle
       params.selectBuffer(srcBuffer);  
            
       params.setRectRoi(left,top,width,height);
       params.storeSelection(dstBuffer);
       params.killRoi();
     }   
   },   
      
   transformImage: function( stretch, srcBuffer, dstBuffer  )
   {
     // Release working buffer
     // this._imagelist.releaseBuffer(dstBuffer); 
 
     if(!this._imagelist.isEmpty(srcBuffer)) 
     {
       // Do not apply correction at 100%
       if(stretch < 100.0) 
       {
         var params = this._parameters;

         params.killRoi();

         // Apply filter according to stretch factor
         var nIter = stretch < 50 ? 2:1;
                    
         // Sub-pixel anti-aliasing transformation

         this.stretchImage( srcBuffer,spotlight_kTmpBuffer,200 );

         params.selectBuffer(spotlight_kTmpBuffer);

         // Daim caveat : the applyKernel will store a copy of the original image
         // in the default image buffer. If not used, this buffer must be 
         // invalidated. Otherwise this could have some side effects on 
         // subsequent operations like brightness/contrast operations.

         // dump("applyKernel '"+spotlight_kFilterKernel+"' iter ="+nIter+"\n");                  
         this._filters.applyKernel(params,spotlight_kFilterKernel,nIter,0);

         this.stretchImage( spotlight_kTmpBuffer, dstBuffer, stretch/2 );

         // Invalidate the buffer.
         params.releaseImageBuffer();

       } 
       else

       if(stretch > 100.0) 
       {
         var params = this._parameters;

         params.killRoi();
         this.stretchImage( srcBuffer,dstBuffer,stretch );
         return;         
       } 
       else
         // just copy the buffer
         this._imagelist.cloneBuffer(srcBuffer,dstBuffer);
     }
   },

   contrastImage: function( brightness , contrast , buffer  )
   {
     var params = this._parameters; 
     
     params.selectBuffer(buffer);
     this._histogram.contrast(params,brightness,contrast);
   },


   sharpenImage: function( sharpen , buffer )
   {
     // Sharpness correction
     // dump("sharpenImage sharpness="+sharpen+" buffer ="+buffer+"\n"); 
     this.applySharpenFilter(buffer,spotlight_kTmpBuffer,sharpen);
   },

   setInnerROI: function( left, top, right, bottom, type, wrkBuffer )
   {
     // Invalidate previous rectangle
     this._rectangle = null;
     this._geometry  = type;
     
     if(!this._imagelist.isEmpty(wrkBuffer)) 
     {
       var params = this._parameters;
      
       params.selectBuffer(wrkBuffer);
      
       // Store roi in mask buffer 
       params.createRoi(type,[left,top,right,bottom],4);
       params.roiToMask();        
       params.invertMask();
       params.storeMask(spotlight_kMaskBuffer,0);
       params.killRoi();

       // Save rectangle for later use
       this._rectangle = { x0:left,y0:top,x1:right,y1:bottom };
     }
   },

  
   hilightImage : function( blendFactor, blendValue, brightnessValue, wrkBuffer ) 
   {
     if(!this._imagelist.isEmpty(wrkBuffer)) 
     {
       var params = this._parameters;
       
       params.selectBuffer(wrkBuffer);
       
       params.callMask(spotlight_kMaskBuffer,0);
       params.maskToRoi();

       // Blend with the given rgb value
       if(blendFactor) 
       {
         params.fillRGB(blendValue,blendValue,blendValue,blendFactor);
       }

       // Apply brigthness correction
       if(brightnessValue >= 0) 
       { 
         this._histogram.contrast(params,brightnessValue,0.5);   
       }
       
       // Draw border
       if(this._drawBorder && this._rectangle ) 
       {
         var canevas   = this._canevas;
         var color     = this._borderColor;
         var rectangle = this._rectangle;
         
         params.killRoi();
         canevas.initialize(params,0);
         canevas.penSize = this._borderSize;
         canevas.opacity = this._borderOpacity/100.0;
         canevas.setColor(color[0],color[1],color[2]);
         
         switch(this._geometry)
         {
           case CI.nsIDaimGeometry.DAIM_Ellipse:
           canevas.drawEllipse(
              Math.round((rectangle.x0+rectangle.x1)/2),
              Math.round((rectangle.y0+rectangle.y1)/2),
              Math.round((rectangle.x1 - rectangle.x0)/2),
              Math.round((rectangle.y1 - rectangle.y0)/2),
              1,0);
           
           break;
           
           default: 
           canevas.drawRectangle(rectangle.x0,rectangle.y0,
                                 rectangle.x1,rectangle.y1);
           
         }
       }
     }
   },

   computeSnapshotImage : function( sharpness, fromBuffer, toBuffer, size ) 
   {
     if(!this._imagelist.isEmpty(fromBuffer)) 
     {
       this._imagelist.cloneBuffer(fromBuffer,spotlight_kTmpBuffer);
      
       var params = this._parameters;
        
       params.selectBuffer(spotlight_kTmpBuffer);
       
       this._filters.applyKernel(params,spotlight_kFilterKernel,2,0); 
       
       var width  = size;
       var height = Math.round(size * (params.imageHeight/params.imageWidth));
  
       var bufferFormat = this._imagelist.getBufferFormat(spotlight_kTmpBuffer);

       // Create Image of the appropriate size
       this._imagelist.createBuffer(toBuffer,bufferFormat,width,height);
       this._transforms.stretch(params,toBuffer,STRETCH_BILINEAR); 
       
       this.applySharpenFilter(toBuffer,spotlight_kTmpBuffer,sharpness);    
     }   
   },
   
   get tmpBuffer() { return spotlight_kTmpBuffer; },   
   get imagelist() { return this._imagelist; },

   set borderColor(val) { 
     if(typeof val == "string") 
     {
        var clr = parseInt(val.replace(/^\#/,'0x'));
        this._borderColor = [
           (clr & 0xff0000)>>16,
           (clr & 0x00ff00)>>8,
           (clr & 0x0000ff),
        ];
     } else {
       this._borderColor = [val[0],val[1],val[2]]; 
     }
     return val; 
   },  
   get borderColor() { 
      return '#' + getHexString(this._borderColor[0],
                                this._borderColor[1],
                                this._borderColor[2]); 
   },
   
   set borderSize(val) { this._borderSize = val; return val; },
   get borderSize()    { return this._borderSize; },

   set borderOpacity(val) { this._borderOpacity = val; return val; },
   get borderOpacity()    { return this._borderOpacity; },

   set enableBorder(val) { this._drawBorder = val?true:false; return val; },
   get enableBorder()    { return this._drawBorder; },
   
  /*!
   * nsISupports
   */
  QueryInterface : function(aIID)
  {
    if(aIID.equals(CI.nsISupports))
       return this;
      
    throw Components.results.NS_NOINTERFACE;
  }
};


spotlightService.prototype.applySharpenFilter = 
function spotlight_applySharpenFilter( fromBuffer, tmpBuffer, value )
{
  if(value <= 0)
     return;

  var params    = this._parameters;
  var imagemath = this._imagemath;
  var filters   = this._filters;

  var radius = 3;
  var sharp  = value/100.0;

  // dump("applySharpenFilter value="+sharp+"\n");

  const nsIDaimFilters      = Components.interfaces.nsIDaimFilters;
  const nsIDaimImageFormats = Components.interfaces.nsIDaimImageFormats;
  const nsIDaimMath         = Components.interfaces.nsIDaimMath;

  filters.setMaskSize(radius,radius);

  function _goApply( workBuffer )
  {
    params.storeBuffer(workBuffer);    

  //filters.applyKernel(params,spotlight_kFilterKernel,1,0);
    filters.mFilter(params,nsIDaimFilters.NSID_Filters_Mean);
    imagemath.addMul(params,0,sharp);
    imagemath.doImageMath(params,nsIDaimMath.DAIM_SUB,workBuffer);   
     
    imagemath.addMul(params,0,1.0/(1.0 - sharp));
  };

  params.selectBuffer(fromBuffer);

  if((params.imageFormat & nsIDaimImageFormats.PixelFormatScalar)!=0) 
  {        
    _goApply(tmpBuffer);
  }
  else
  {
    // get the colorspace
    // split image
    // Apply sharpen to each band
    // merge bands

    var colorspace = this._colorspace;

    colorspace.split(params);
    
    params.imageList = colorspace;
    params.killRoi();

    var nChannels = colorspace.numChannels;
    for(var chan=0;chan<nChannels;++chan) 
    {
      params.selectBuffer(chan);
      _goApply( nChannels );
    }    

    // Restore imagelist;
    colorspace.createMergeBuffer(this._imagelist,fromBuffer);
    
    params.imageList = this._imagelist;
    params.selectBuffer(fromBuffer);

    colorspace.killRect();
    colorspace.merge(params);
    colorspace.releaseAll();
  }
  
  // Invalidate the buffer.
  params.releaseImageBuffer();

  
};


spotlightService.prototype.stretchImage =
function spotlight_stretchImage( fromBuffer,toBuffer, factor )
{
  var params = this._parameters;

  params.selectBuffer(fromBuffer);

  var width  = Math.round(params.imageWidth  * factor / 100.0);
  var height = Math.round(params.imageHeight * factor / 100.0);
  
  var bufferFormat = this._imagelist.getBufferFormat(fromBuffer);

  // dump("stretchImage width="+width+" height="+height+"\n");

  // Create Image of the appropriate size
  this._imagelist.createBuffer(toBuffer,bufferFormat,width,height);
  
  this._transforms.stretch(params,toBuffer,STRETCH_BILINEAR);
};


const SPOTLIGHTSERVICE_CONTRACTID = '@daim.project/scenari/enlightner;1';
const SPOTLIGHTSERVICE_CID        = Components.ID("{8a982f80-1934-4d6d-bc80-64ed7112fef9}");
const SPOTLIGHTSERVICE_NAME       = "Image Enlightner"

//================================================
// Batch interface
//================================================
function scImgHilight()
{
  this.sptlSrvc = new spotlightService();
  this.sptlSrvc.init(null);
  
  this._stretch       = 100;
  this._sharpness     = 0; 
  this._brightness    = -1; 
  this._alpha         = 0; 
  this._imgBrightness = 50; 
  this._imgContrast   = 50;
  this._geometry      = CI.nsIDaimGeometry.DAIM_Rectangle;
  this._borderColor   = "#ffffff";
};

scImgHilight.prototype = {

  kSrcBuffer:1,
  kWrkBuffer:2,

  set borderColor(val)   { return (this._borderColor = val); },
  get borderColor()      { return this._borderColor; },
   
  /* 0 - ??? */ 
  set borderSize(val)    { return (this.sptlSrvc.borderSize = val); },
  get borderSize()       { return this.sptlSrvc.borderSize; },

  /* 0 - 100 */ 
  set borderOpacity(val) { return (this.sptlSrvc.borderOpacity = val); },
  get borderOpacity()    { return this.sptlSrvc.borderOpacity; },

  set geometry(val)   { this._geometry = val; return val;},
  get geometry()      { return this._geometry; },

  /* ?? - 100 */ 
  set stretch(val)   { this._stretch = val; return val;},
  get stretch()      { return this._stretch; },

  set sharpness(val) { this._sharpness = val; return val;},
  get sharpness()    { return this._sharpness; },

  set brightness(val)  { this._brightness = val; return val;},
  get brightness()     { return  this._brightness; },
 
  set alpha(val)       { this._alpha = val; return val;},
  get alpha()          { return this._alpha; },

  set imgBrightness(val)  { this._imgBrightness = val; return val;},
  get imgBrightness()     { return this._imgBrightness;},

  set imgContrast(val)  { this._imgContrast = val; return val;},
  get imgContrast()     { return this._imgContrast;},
  
  process : function(url,dest,left,top,right,bottom)
  {
    var mInfos = this.sptlSrvc;
  
    if(this._stretch <= 0)
       throw Components.results.NS_ERROR_FAILURE;
  
    var stretch     = this._stretch;
    var alpha       = this._alpha; // 0 == disabled;
    var brightness  = this._brightness != -1 ? this._brightness / 100.0 : -1;
    var blendFactor = alpha != 0 ? Math.abs(alpha/100.0) : 0;
    var blendValue  = alpha < 0 ? 0 : 255;

    var sharpness     = this._sharpness;
    var imgBrightness = this._imgBrightness/100.0; 
    var imgContrast   = this._imgContrast/100.0; 
    
    mInfos.releaseAllBuffers();
    mInfos.loadImage(url,this.kSrcBuffer);
    mInfos.transformImage(stretch,this.kSrcBuffer,this.kWrkBuffer); 
    
    var hilightEnabled = (left < right && top < bottom);
    
    if(hilightEnabled)
    {
      var stretch_ratio = stretch / 100.0;
       
      var _left   = Math.round(left   * stretch_ratio);
      var _top    = Math.round(top    * stretch_ratio);
      var _right  = Math.round(right  * stretch_ratio);
      var _bottom = Math.round(bottom * stretch_ratio);

      mInfos.enableBorder = (this.borderSize >= 1);
      mInfos.borderColor  = this._borderColor;
    
      mInfos.setInnerROI(_left,_top,_right,_bottom,
                         this._geometry,
                         this.kWrkBuffer);  
    }
    
    mInfos.contrastImage(imgBrightness,imgContrast,this.kWrkBuffer);

    if(sharpness != 0)
       mInfos.sharpenImage(sharpness,this.kWrkBuffer);

    if(hilightEnabled)
       mInfos.hilightImage(blendFactor,blendValue,brightness,
                           this.kWrkBuffer); 

    // Save image
    mInfos.imagelist.saveBuffer(dest,this.kWrkBuffer);
    mInfos.releaseAllBuffers();    
  },
  
  /*!
   * nsISupports
   */
  QueryInterface : function(aIID)
  {
    if(aIID.equals(CI.nsISupports) ||
       aIID.equals(CI.scIImgHilight) )
       return this;
      
    throw Components.results.NS_NOINTERFACE;
  }                    
};

const SCIMGHILIGHT_CONTRACTID = '@scenari.org/image/hilight;1';
const SCIMGHILIGHT_CID        = Components.ID("{4b4639fc-c02f-4032-b9d9-bf37465431b7}");
const SCIMGHILIGHT_NAME       = "Image Enlightner (batch mode)"

//=================================================

/*! 
 *  spotlightServiceFactory Class Factory 
 */
var spotlightServiceFactory = {
    createInstance: function(outer, iid) 
    {
      if(outer != null)
         throw Components.results.NS_ERROR_NO_AGGREGATION;

      //CONSOLE.logStringMessage("createInstance");
      return (new spotlightService()).QueryInterface(iid);
    }
};


/*! 
 *  scImgHilightFactory Class Factory 
 */
var scImgHilightFactory = {
    createInstance: function(outer, iid) 
    {
      if(outer != null)
         throw Components.results.NS_ERROR_NO_AGGREGATION;

      //CONSOLE.logStringMessage("createInstance");
      return (new scImgHilight()).QueryInterface(iid);
    }
};


/*! 
 *  Component module 
 */
var module = {
    // registerSelf: Register this component.
    registerSelf: function (compMgr, fileSpec, location, type) 
    {
        var compReg = compMgr.QueryInterface( Components.interfaces.nsIComponentRegistrar );

        compReg.registerFactoryLocation( SPOTLIGHTSERVICE_CID,
                                         SPOTLIGHTSERVICE_NAME,
                                         SPOTLIGHTSERVICE_CONTRACTID,
                                         fileSpec,
                                         location,
                                         type );      
                                                  
        compReg.registerFactoryLocation( SCIMGHILIGHT_CID,
                                         SCIMGHILIGHT_NAME,
                                         SCIMGHILIGHT_CONTRACTID,
                                         fileSpec,
                                         location,
                                         type );               

    },

    unregisterSelf : function(compMgr, location, type) 
    {
       var compReg = compMgr.QueryInterface(nsIComponentRegistrar);
    
       compReg.unregisterFactoryLocation(SPOTLIGHTSERVICE_CID, location);
       compReg.unregisterFactoryLocation(SCIMGHILIGHT_CID,     location);
    },

    getClassObject: function(compMgr, cid, iid) 
    {
      if (!iid.equals(Components.interfaces.nsIFactory))
           throw Components.results.NS_ERROR_NOT_IMPLEMENTED; 

      if (cid.equals(SPOTLIGHTSERVICE_CID))
          return spotlightServiceFactory;

      if (cid.equals(SCIMGHILIGHT_CID))
          return scImgHilightFactory;

      throw Components.results.NS_ERROR_NO_INTERFACE;
    },

    // canUnload: n/a (returns true)
    canUnload: function(compMgr) {
      return true;
    }
};

// NSGetModule
function NSGetModule(compMgr, fileSpec) {
    return module;
}



