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

#include <stdio.h>

#include "nsCOMPtr.h"
#include "prmem.h"

#include "flockImageTransformService.h"

#include "nsString.h"

#include "nsIInterfaceRequestorUtils.h"
#include "nsIServiceManager.h"

#include "cairo.h"

#include "nsNetUtil.h"
#include "nsIObserver.h"
#include "imgIRequest.h"
#include "imgIDecoderObserver.h"
#include "imgIContainer.h"
#include "imgILoader.h"
#include "nsIImage.h"
#include "gfxIImageFrame.h"
#include "imgIEncoder.h"
#ifdef MOZILLA_1_8_BRANCH
#define imgIEncoder imgIEncoder_MOZILLA_1_8_BRANCH
#endif

#include "flockIImgContainer.h"
#include "flockIImgEncoder.h"
#include "flockIImgRequest.h"


class flockImageTransformOps : public flockIImageTransformOps {
public:
    flockImageTransformOps(imgIRequest *aRequest);
    ~flockImageTransformOps();

    NS_DECL_ISUPPORTS
    NS_DECL_FLOCKIIMAGETRANSFORMOPS

private:
    void DestroyCairoSrcObjects();
    void DestroyCairoDestObjects();

    nsresult GetCairoSurfaceFromImage();

    nsCOMPtr<imgIRequest> mImgRequest;

    PRUint32 mWidth, mHeight;

    cairo_t *mCairo;
    cairo_surface_t *mSurface;
    PRUint8 *mSurfaceData;

    cairo_surface_t *mImageSurface;
    PRUint8 *mImageSurfaceData;
};

NS_IMPL_ISUPPORTS1(flockImageTransformOps, flockIImageTransformOps)

flockImageTransformOps::flockImageTransformOps(imgIRequest *aRequest)
    : mImgRequest(aRequest), mWidth(0), mHeight(0),
      mCairo(NULL), mSurface(NULL), mSurfaceData(NULL),
      mImageSurface(NULL), mImageSurfaceData(NULL)
{
}

flockImageTransformOps::~flockImageTransformOps()
{
    DestroyCairoSrcObjects();
    DestroyCairoDestObjects();

    mImgRequest = NULL;
}

void flockImageTransformOps::DestroyCairoSrcObjects()
{
    if (mImageSurface) {
        cairo_surface_destroy(mImageSurface);
        mImageSurface = NULL;
    }

    if (mImageSurfaceData) {
        nsMemory::Free(mImageSurfaceData);
        mImageSurfaceData = NULL;
    }
}

void flockImageTransformOps::DestroyCairoDestObjects()
{
    if (mCairo) {
        cairo_destroy(mCairo);
        mCairo = NULL;
    }

    if (mSurface) {
        cairo_surface_destroy(mSurface);
        mSurface = NULL;
    }

    if (mSurfaceData) {
        nsMemory::Free(mSurfaceData);
        mSurfaceData = NULL;
    }
}

NS_IMETHODIMP flockImageTransformOps::GetURI(nsIURI **aURI)
{
    return mImgRequest->GetURI(aURI);
}

NS_IMETHODIMP flockImageTransformOps::GetFormat(char **aFormat)
{
    return mImgRequest->GetMimeType(aFormat);
}

NS_IMETHODIMP flockImageTransformOps::GetWidth(PRInt32 *aWidth)
{
    nsresult rv;

    nsCOMPtr<imgIContainer> imgContainer;
    rv = mImgRequest->GetImage(getter_AddRefs(imgContainer));
    NS_ENSURE_SUCCESS(rv, rv);

    return imgContainer->GetWidth(aWidth);
}

NS_IMETHODIMP flockImageTransformOps::GetHeight(PRInt32 *aHeight)
{
    nsresult rv;

    nsCOMPtr<imgIContainer> imgContainer;
    rv = mImgRequest->GetImage(getter_AddRefs(imgContainer));
    NS_ENSURE_SUCCESS(rv, rv);

    return imgContainer->GetHeight(aHeight);
}

NS_IMETHODIMP flockImageTransformOps::SetDimensions(PRUint32 width, PRUint32 height)
{
    DestroyCairoDestObjects();

    mWidth = width;
    mHeight = height;

    mSurfaceData = (PRUint8*) nsMemory::Alloc(mWidth * mHeight * 4);
    if (!mSurfaceData)
        return NS_ERROR_OUT_OF_MEMORY;

    mSurface = cairo_image_surface_create_for_data(mSurfaceData,
                                                   CAIRO_FORMAT_ARGB32,
                                                   mWidth, mHeight,
                                                   mWidth * 4);

    if (!mSurface || cairo_surface_status(mSurface)) {
        mSurface = NULL;
        return NS_ERROR_OUT_OF_MEMORY;
    }

    mCairo = cairo_create(mSurface);

    if (!mCairo || cairo_status(mCairo)) {
        mCairo = NULL;
        return NS_ERROR_OUT_OF_MEMORY;
    }

    return NS_OK;
}

NS_IMETHODIMP flockImageTransformOps::Translate(double tx, double ty)
{
    if (!mCairo)
        return NS_ERROR_NOT_INITIALIZED;

    cairo_translate(mCairo, tx, ty);

    return NS_OK;
}

NS_IMETHODIMP flockImageTransformOps::Scale(double sx, double sy)
{
    if (!mCairo)
        return NS_ERROR_NOT_INITIALIZED;

    cairo_scale(mCairo, sx, sy);

    return NS_OK;
}

NS_IMETHODIMP flockImageTransformOps::Rotate(double angle)
{
    if (!mCairo)
        return NS_ERROR_NOT_INITIALIZED;

    cairo_rotate(mCairo, angle);

    return NS_OK;
}

NS_IMETHODIMP flockImageTransformOps::Reset(void)
{
    if (!mCairo)
        return NS_ERROR_NOT_INITIALIZED;

    cairo_identity_matrix(mCairo);

    return NS_OK;
}

NS_IMETHODIMP flockImageTransformOps::SaveToStream(const nsACString& format,
                                                   const nsAString& options,
                                                   nsIInputStream **stream)
{
    if (!mCairo)
        return NS_ERROR_NOT_INITIALIZED;

    nsCString conid(NS_LITERAL_CSTRING("@mozilla.org/image/encoder;2?type="));
    conid += format;

    nsCOMPtr<imgIEncoder> encoder = do_CreateInstance(conid.get());
    if (!encoder)
        return NS_ERROR_FAILURE;

    nsresult rv;

    if (!mImageSurface) {
        rv = GetCairoSurfaceFromImage();
        NS_ENSURE_SUCCESS(rv, rv);
    }

    cairo_set_source_surface(mCairo, mImageSurface, 0, 0);
    cairo_paint(mCairo);

    nsCOMPtr<imgIContainer> imgContainer;
    rv = mImgRequest->GetImage(getter_AddRefs(imgContainer));
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<flockIImgContainer> flockImage =
        do_QueryInterface(imgContainer);
    if (flockImage) {
        nsCOMPtr<nsISupports> metadata;
        rv = flockImage->GetMetadata(getter_AddRefs(metadata));
        NS_ENSURE_SUCCESS(rv, rv);

        if (metadata) {
            nsCOMPtr<flockIImgEncoder> flockImageEncoder =
                do_QueryInterface(encoder);
            if (flockImageEncoder) {
              rv = flockImageEncoder->SetMetadata(metadata);
              NS_ENSURE_SUCCESS(rv, rv);
            }
        }
    }

    rv = encoder->InitFromData(mSurfaceData, mWidth * mHeight * 4,
                               mWidth, mHeight, mWidth * 4,
                               imgIEncoder::INPUT_FORMAT_HOSTARGB, options);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = CallQueryInterface(encoder, stream);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsIInputStream> imgStream = *stream;
    return NS_OK;
}


nsresult flockImageTransformOps::GetCairoSurfaceFromImage()
{
    // XXX: This is cut'n'pasted from:
    // nsCanvasRenderingContext2D::CairoSurfaceFromElement
    // When Firefox 3 rolls around, we can get a cairo surface from an
    // nsIImage directly, and this code will just go away
    nsresult rv;
    nsCOMPtr<imgIContainer> imgContainer;

    rv = mImgRequest->GetImage(getter_AddRefs(imgContainer));
    NS_ENSURE_SUCCESS(rv, rv);

    if (!imgContainer)
        return NS_ERROR_NOT_AVAILABLE;

    nsCOMPtr<gfxIImageFrame> frame;
    rv = imgContainer->GetCurrentFrame(getter_AddRefs(frame));
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsIImage> img(do_GetInterface(frame));

    PRInt32 imgWidth, imgHeight;
    rv = frame->GetWidth(&imgWidth);
    rv |= frame->GetHeight(&imgHeight);
    if (NS_FAILED(rv))
        return NS_ERROR_FAILURE;

    mImageSurfaceData = (PRUint8 *)nsMemory::Alloc(imgHeight * imgWidth * 4);
    PRUint8 *outData = mImageSurfaceData;

    gfx_format format;
    rv = frame->GetFormat(&format);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = frame->LockImageData();
    if (img->GetHasAlphaMask())
        rv |= frame->LockAlphaData();
    if (NS_FAILED(rv)) {
        nsMemory::Free(mImageSurfaceData);
        mImageSurfaceData = nsnull;
        return NS_ERROR_FAILURE;
    }

    PRUint8 *inPixBits, *inAlphaBits = nsnull;
    PRUint32 inPixStride, inAlphaStride = 0;
    inPixBits = img->GetBits();
    inPixStride = img->GetLineStride();
    if (img->GetHasAlphaMask()) {
        inAlphaBits = img->GetAlphaBits();
        inAlphaStride = img->GetAlphaLineStride();
    }

    PRBool topToBottom = img->GetIsRowOrderTopToBottom();
    PRBool useBGR;

    // The gtk backend optimizes away the alpha mask of images
    // with a fully opaque alpha, but doesn't update its format (bug?);
    // you end up with a RGB_A8 image with GetHasAlphaMask() == false.
    // We need to treat that case as RGB.  We also need to understand
    // that with 1-bit alpha the data is actually RGB_A1, not RGB_A8,
    // no matter what the format says.
#ifdef MOZ_WIDGET_GTK2
    PRInt8 alphaDepth = img->GetAlphaDepth();
    if (alphaDepth == 0) {
        if (format == gfxIFormats::RGB_A8)
            format = gfxIFormats::RGB;
        else if (format == gfxIFormats::BGR_A8)
            format = gfxIFormats::BGR;
    } else if (alphaDepth == 1) {
        if (format == gfxIFormats::RGB_A8)
            format = gfxIFormats::RGB_A1;
        else if (format == gfxIFormats::BGR_A8)
            format = gfxIFormats::BGR_A1;
    }
#endif

    // We probably don't need the GetHasAlphaMask check here, because it
    // should've been handled up above, but as this patch is going into
    // the release branch I don't want to change this code unnecessarily.
    if ((format == gfxIFormats::RGB || format == gfxIFormats::BGR) ||
        (!(img->GetHasAlphaMask()) && (format == gfxIFormats::RGB_A8 || format == gfxIFormats::BGR_A8)))
    {
        useBGR = (format & 1);

#ifdef IS_BIG_ENDIAN
        useBGR = !useBGR;
#endif

        for (PRUint32 j = 0; j < (PRUint32) imgHeight; j++) {
            PRUint32 rowIndex;
            if (topToBottom)
                rowIndex = j;
            else
                rowIndex = imgHeight - j - 1;

            PRUint8 *inrowrgb = inPixBits + (inPixStride * rowIndex);

            for (PRUint32 i = 0; i < (PRUint32) imgWidth; i++) {
                // handle rgb data; no alpha to premultiply
#ifdef XP_MACOSX
                // skip extra OSX byte
                inrowrgb++;
#endif
                PRUint8 b = *inrowrgb++;
                PRUint8 g = *inrowrgb++;
                PRUint8 r = *inrowrgb++;

#ifdef IS_BIG_ENDIAN
                // alpha
                *outData++ = 0xff;
#endif

                if (useBGR) {
                    *outData++ = b;
                    *outData++ = g;
                    *outData++ = r;
                } else {
                    *outData++ = r;
                    *outData++ = g;
                    *outData++ = b;
                }

#ifdef IS_LITTLE_ENDIAN
                // alpha
                *outData++ = 0xff;
#endif
            }
        }
        rv = NS_OK;
    } else if (format == gfxIFormats::RGB_A1 || format == gfxIFormats::BGR_A1) {
        useBGR = (format & 1);

#ifdef IS_BIG_ENDIAN
        useBGR = !useBGR;
#endif

        for (PRUint32 j = 0; j < (PRUint32) imgHeight; j++) {
            PRUint32 rowIndex;
            if (topToBottom)
                rowIndex = j;
            else
                rowIndex = imgHeight - j - 1;

            PRUint8 *inrowrgb = inPixBits + (inPixStride * rowIndex);
            PRUint8 *inrowalpha = inAlphaBits + (inAlphaStride * rowIndex);

            for (PRUint32 i = 0; i < (PRUint32) imgWidth; i++) {
                // pull out the bit value into alpha
                PRInt32 bit = i % 8;
                PRInt32 byte = i / 8;

#ifdef IS_LITTLE_ENDIAN
                PRUint8 a = (inrowalpha[byte] >> (7-bit)) & 1;
#else
                PRUint8 a = (inrowalpha[byte] >> bit) & 1;
#endif

#ifdef XP_MACOSX
                // skip extra X8 byte on OSX
                inrowrgb++;
#endif

                // handle rgb data; need to multiply the alpha out,
                // but we short-circuit that here since we know that a
                // can only be 0 or 1
                if (a) {
                    PRUint8 b = *inrowrgb++;
                    PRUint8 g = *inrowrgb++;
                    PRUint8 r = *inrowrgb++;

#ifdef IS_BIG_ENDIAN
                    // alpha
                    *outData++ = 0xff;
#endif

                    if (useBGR) {
                        *outData++ = b;
                        *outData++ = g;
                        *outData++ = r;
                    } else {
                        *outData++ = r;
                        *outData++ = g;
                        *outData++ = b;
                    }

#ifdef IS_LITTLE_ENDIAN
                    // alpha
                    *outData++ = 0xff;
#endif
                } else {
                    // alpha is 0, so we need to write all 0's,
                    // ignoring input color
                    inrowrgb += 3;
                    *outData++ = 0;
                    *outData++ = 0;
                    *outData++ = 0;
                    *outData++ = 0;
                }
            }
        }
        rv = NS_OK;
    } else if (format == gfxIFormats::RGB_A8 || format == gfxIFormats::BGR_A8) {
        useBGR = (format & 1);

#ifdef IS_BIG_ENDIAN
        useBGR = !useBGR;
#endif

        for (PRUint32 j = 0; j < (PRUint32) imgHeight; j++) {
            PRUint32 rowIndex;
            if (topToBottom)
                rowIndex = j;
            else
                rowIndex = imgHeight - j - 1;

            PRUint8 *inrowrgb = inPixBits + (inPixStride * rowIndex);
            PRUint8 *inrowalpha = inAlphaBits + (inAlphaStride * rowIndex);

            for (PRUint32 i = 0; i < (PRUint32) imgWidth; i++) {
                // pull out alpha; we'll need it to premultiply
                PRUint8 a = *inrowalpha++;

                // handle rgb data; we need to fully premultiply
                // with the alpha
#ifdef XP_MACOSX
                // skip extra X8 byte on OSX
                inrowrgb++;
#endif

                // XXX gcc bug: gcc seems to push "r" into a register
                // early, and pretends that it's in that register
                // throughout the 3 macros below.  At the end
                // of the 3rd macro, the correct r value is
                // calculated but never stored anywhere -- the r variable
                // has the value of the low byte of register that it
                // was stuffed into, which has the result of some 
                // intermediate calculation.
                // I've seen this on gcc 3.4.2 x86 (Fedora Core 3)
                // and gcc 3.3 PPC (OS X 10.3)

                //PRUint8 b, g, r;
                //FAST_DIVIDE_BY_255(b, *inrowrgb++ * a - a / 2);
                //FAST_DIVIDE_BY_255(g, *inrowrgb++ * a - a / 2);
                //FAST_DIVIDE_BY_255(r, *inrowrgb++ * a - a / 2);

                PRUint8 b = (*inrowrgb++ * a - a / 2) / 255;
                PRUint8 g = (*inrowrgb++ * a - a / 2) / 255;
                PRUint8 r = (*inrowrgb++ * a - a / 2) / 255;

#ifdef IS_BIG_ENDIAN
                *outData++ = a;
#endif

                if (useBGR) {
                    *outData++ = b;
                    *outData++ = g;
                    *outData++ = r;
                } else {
                    *outData++ = r;
                    *outData++ = g;
                    *outData++ = b;
                }

#ifdef IS_LITTLE_ENDIAN
                *outData++ = a;
#endif
            }
        }
        rv = NS_OK;
    } else {
        rv = NS_ERROR_FAILURE;
    }

    if (img->GetHasAlphaMask())
        frame->UnlockAlphaData();
    frame->UnlockImageData();

    if (NS_FAILED(rv)) {
        nsMemory::Free(mImageSurfaceData);
        mImageSurfaceData = nsnull;
        return rv;
    }

    mImageSurface =
        cairo_image_surface_create_for_data(mImageSurfaceData,
                                            CAIRO_FORMAT_ARGB32,
                                            imgWidth, imgHeight, imgWidth * 4);

    return NS_OK;
}


PR_STATIC_CALLBACK(PLDHashOperator)
CancelImageRequest(nsISupports *aKey,
                   flockIImageTransformListener *aListener,
                   void *aData)
{
  nsCOMPtr<imgIRequest> request = do_QueryInterface(aKey);
  request->Cancel(NS_BINDING_ABORTED);
  return PL_DHASH_NEXT;
}

NS_IMPL_ISUPPORTS5(flockImageTransformService, flockIImageTransformService,
                   imgIDecoderObserver, imgIDecoderObserver_MOZILLA_1_8_BRANCH,
                   imgIContainerObserver, nsIObserver)

flockImageTransformService::flockImageTransformService()
{
}

flockImageTransformService::~flockImageTransformService()
{
    mListeners.EnumerateRead(CancelImageRequest, NULL);
    mListeners.Clear();
}

NS_IMETHODIMP flockImageTransformService::Initialize()
{
    nsresult rv;

    if (!mListeners.Init()) {
        return NS_ERROR_OUT_OF_MEMORY;
    }

    mImageLoaderService = do_GetService("@mozilla.org/image/loader;1", &rv);
    return rv;
}

NS_IMETHODIMP flockImageTransformService::OnStartRequest(imgIRequest *aRequest)
{
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::OnStartDecode(imgIRequest *aRequest)
{
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::OnStartContainer(imgIRequest *aRequest,
                                                           imgIContainer *aImage)
{

    PRUint32 numFrames;
    nsresult rv = aImage->GetNumFrames(&numFrames);
    NS_ENSURE_SUCCESS(rv, rv);
   
    if (numFrames > 1) {
      /* XXX: throw error here? */
    }
 
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::OnStartFrame(imgIRequest *aRequest,
                                                       gfxIImageFrame *aFrame)
{
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::OnDataAvailable(imgIRequest *aRequest,
                                                          gfxIImageFrame *aFrame,
                                                          const nsRect *aRect)
{
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::OnStopFrame(imgIRequest *aRequest,
                                                      gfxIImageFrame *aFrame)
{
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::OnStopContainer(imgIRequest *aRequest,
                                                          imgIContainer *aImage)
{
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::OnStopDecode(imgIRequest *aRequest,
                                                       nsresult status,
                                                       const PRUnichar *statusArg)
{

    if (status == NS_OK) {
        flockIImageTransformListener *listener;
        if (!mListeners.Get(aRequest, &listener))
            return NS_ERROR_UNEXPECTED;

        flockImageTransformOps *ops = new flockImageTransformOps(aRequest);
        if (!ops)
            return NS_ERROR_OUT_OF_MEMORY;

        NS_ADDREF(ops);
        listener->OnImageTransformOpsReady(ops);
        NS_RELEASE(ops);
    }

    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::OnStopRequest(imgIRequest *aRequest,
                                                        PRBool aIsLastPart)
{
    aRequest->Cancel(NS_BINDING_ABORTED);
    mListeners.Remove(aRequest);
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::FrameChanged(imgIContainer *aContainer,
                                                       gfxIImageFrame *newframe,
                                                       nsRect *dirtyRect)
{
    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::LoadImage(nsIURI *aURI, flockIImageTransformListener *aListener)
{
    nsresult rv;

    NS_ENSURE_ARG_POINTER(aURI);
    NS_ENSURE_ARG_POINTER(aListener);

    nsCOMPtr<imgIRequest> imageRequest;
    rv = mImageLoaderService->LoadImage(aURI, NULL, NULL, NULL, this, NULL,
                                        nsIRequest::LOAD_NORMAL, NULL, NULL,
                                        getter_AddRefs(imageRequest));
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<flockIImgRequest> flockImageRequest =
        do_QueryInterface(imageRequest);
    if (flockImageRequest) {
        flockImageRequest->SaveMetadata(PR_TRUE);
    }

    mListeners.Put(imageRequest, aListener);

    return NS_OK;
}

NS_IMETHODIMP flockImageTransformService::Observe(nsISupports *aSubject,
                                                  const char *aTopic, 
                                                  const PRUnichar *data) 
{
    return NS_OK;
}

static flockImageTransformService *gSingleton = NULL;

flockImageTransformService *
flockImageTransformService::GetSingleton()
{
    if (!gSingleton) {
        gSingleton = new flockImageTransformService();
        if (gSingleton) {
            NS_ADDREF(gSingleton);
            if (NS_FAILED(gSingleton->Initialize())) {
                NS_RELEASE(gSingleton);
            }
        }
    }
    return gSingleton;
}
