/*
 * Copyright (C) 2005, 2007 Apple Inc. All rights reserved.
 * Copyright (C) 2005 Ben La Monica <ben.lamonica@gmail.com>.  All rights reserved.
 * Copyright (C) 2011 Brent Fulgham. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// FIXME: We need to be able to include these defines from a config.h somewhere.
#define JS_EXPORT_PRIVATE
#define WTF_EXPORT_PRIVATE

#include <cairo.h>
#include <stdio.h>
#include <wtf/Platform.h>
#include <wtf/RefPtr.h>
#include <wtf/RetainPtr.h>

#if PLATFORM(WIN)
#include <fcntl.h>
#include <io.h>
#include <windows.h>
#include <wtf/MathExtras.h>
#endif

using namespace std;

static const int s_bufferSize = 2048;
static const int s_bytesPerPixel = 4;
static cairo_user_data_key_t s_imageDataKey;


#if PLATFORM(WIN)
#undef min
#undef max

static inline float strtof(const char* inputString, char** endptr)
{
    return strtod(inputString, endptr);
}
#endif

static cairo_status_t readFromData(void* closure, unsigned char* data, unsigned int length)
{
    CFMutableDataRef dataSource = reinterpret_cast<CFMutableDataRef>(closure);

    CFRange range = CFRangeMake(0, length);
    CFDataGetBytes(dataSource, range, data);
    CFDataDeleteBytes(dataSource, range);

    return CAIRO_STATUS_SUCCESS;
}

static cairo_surface_t* createImageFromStdin(int bytesRemaining)
{
    unsigned char buffer[s_bufferSize];
    RetainPtr<CFMutableDataRef> data(AdoptCF, CFDataCreateMutable(0, bytesRemaining));

    while (bytesRemaining > 0) {
        size_t bytesToRead = min(bytesRemaining, s_bufferSize);
        size_t bytesRead = fread(buffer, 1, bytesToRead, stdin);
        CFDataAppendBytes(data.get(), buffer, static_cast<CFIndex>(bytesRead));
        bytesRemaining -= static_cast<int>(bytesRead);
    }

    return cairo_image_surface_create_from_png_stream (static_cast<cairo_read_func_t>(readFromData), data.get());
}

static void releaseMallocBuffer(void* data)
{
    free(data);
}

static inline float pixelDifference(float expected, float actual)
{
    return (actual - expected) / max<float>(255 - expected, expected);
}

static inline void normalizeBuffer(float maxDistance, unsigned char* buffer, size_t length)
{
    if (maxDistance >= 1)
        return;

    for (size_t p = 0; p < length; ++p)
        buffer[p] /= maxDistance;
}

static cairo_surface_t* createDifferenceImage(cairo_surface_t* baselineImage, cairo_surface_t* actualImage, float& difference)
{
    size_t width = cairo_image_surface_get_width(baselineImage);
    size_t height = cairo_image_surface_get_height(baselineImage);

    unsigned char* baselinePixel = cairo_image_surface_get_data(baselineImage);
    unsigned char* actualPixel = cairo_image_surface_get_data(actualImage);

    // Compare the content of the 2 bitmaps
    void* diffBuffer = malloc(width * height);
    unsigned char* diffPixel = reinterpret_cast<unsigned char*>(diffBuffer);

    float count = 0;
    float sum = 0;
    float maxDistance = 0;
    for (size_t y = 0; y < height; ++y) {
        for (size_t x = 0; x < width; ++x) {
            float red = pixelDifference(baselinePixel[0], actualPixel[0]);
            float green = pixelDifference(baselinePixel[1], actualPixel[1]);
            float blue = pixelDifference(baselinePixel[2], actualPixel[2]);
            float alpha = pixelDifference(baselinePixel[3], actualPixel[3]);

            float distance = sqrtf(red * red + green * green + blue * blue + alpha * alpha) / 2.0;
            
            *diffPixel++ = static_cast<unsigned char>(distance * 255);
            
            if (distance >= 1.0 / 255.0) {
                ++count;
                sum += distance;
                if (distance > maxDistance)
                    maxDistance = distance;
            }
            
            baselinePixel += s_bytesPerPixel;
            actualPixel += s_bytesPerPixel;
        }
    }
    
    // Compute the difference as a percentage combining both the number of different pixels and their difference amount i.e. the average distance over the entire image
    if (count > 0)
        difference = 100.0f * sum / (height * width);
    else
        difference = 0;

    if (!difference) {
        free(diffBuffer);
        return 0;
    }

    // Generate a normalized diff image
    normalizeBuffer(maxDistance, reinterpret_cast<unsigned char*>(diffBuffer), height * width);
        
    cairo_surface_t* diffImage = cairo_image_surface_create_for_data(diffPixel, CAIRO_FORMAT_ARGB32, width, height, width * s_bytesPerPixel); 
    cairo_surface_set_user_data(diffImage, &s_imageDataKey, diffBuffer, releaseMallocBuffer);
    
    return diffImage;
}

static inline bool imageHasAlpha(cairo_surface_t* image)
{
    return (cairo_image_surface_get_format(image) == CAIRO_FORMAT_ARGB32);
}

static cairo_status_t writeToData(void* closure, unsigned char* data, unsigned int length)
{
    CFMutableDataRef dataTarget = reinterpret_cast<CFMutableDataRef>(closure);

    CFDataAppendBytes(dataTarget, data, length);

    return CAIRO_STATUS_SUCCESS;
}

int main(int argc, const char* argv[])
{
#if PLATFORM(WIN)
    _setmode(0, _O_BINARY);
    _setmode(1, _O_BINARY);
#endif

    float tolerance = 0;

    for (int i = 1; i < argc; ++i) {
        if (!strcmp(argv[i], "-t") || !strcmp(argv[i], "--tolerance")) {
            if (i >= argc - 1)
                exit(1);
            tolerance = strtof(argv[i + 1], 0);
            ++i;
            continue;
        }
    }

    char buffer[s_bufferSize];
    cairo_surface_t* actualImage = 0;
    cairo_surface_t* baselineImage = 0;

    while (fgets(buffer, sizeof(buffer), stdin)) {
        char* newLineCharacter = strchr(buffer, '\n');
        if (newLineCharacter)
            *newLineCharacter = '\0';

        if (!strncmp("Content-Length: ", buffer, 16)) {
            strtok(buffer, " ");
            int imageSize = strtol(strtok(0, " "), 0, 10);

            if (imageSize > 0 && !actualImage)
                actualImage = createImageFromStdin(imageSize);
            else if (imageSize > 0 && !baselineImage)
                baselineImage = createImageFromStdin(imageSize);
            else
                fputs("error, image size must be specified.\n", stdout);
        }

        if (actualImage && baselineImage) {
            cairo_surface_t* diffImage = 0;
            float difference = 100.0;
            
            if ((cairo_image_surface_get_width(actualImage) == cairo_image_surface_get_width(baselineImage))
                && (cairo_image_surface_get_height(actualImage) == cairo_image_surface_get_height(baselineImage))
                && (imageHasAlpha(actualImage) == imageHasAlpha(baselineImage))) {
                diffImage = createDifferenceImage(actualImage, baselineImage, difference); // difference is passed by reference
                if (difference <= tolerance)
                    difference = 0;
                else {
                    difference = roundf(difference * 100.0) / 100.0;
                    difference = max<float>(difference, 0.01); // round to 2 decimal places
                }
            } else
                fputs("error, test and reference image have different properties.\n", stderr);
                
            if (difference > 0.0) {
                if (diffImage) {
                    RetainPtr<CFMutableDataRef> imageData(AdoptCF, CFDataCreateMutable(0, 0));
                    cairo_surface_write_to_png_stream(diffImage, (cairo_write_func_t)writeToData, imageData.get());
                    printf("Content-Length: %lu\n", CFDataGetLength(imageData.get()));
                    fwrite(CFDataGetBytePtr(imageData.get()), 1, CFDataGetLength(imageData.get()), stdout);
                    cairo_surface_destroy(diffImage);
                    diffImage = 0;
                }
                
                fprintf(stdout, "diff: %01.2f%% failed\n", difference);
            } else
                fprintf(stdout, "diff: %01.2f%% passed\n", difference);
            
            cairo_surface_destroy(actualImage);
            cairo_surface_destroy(baselineImage);
            actualImage = 0;
            baselineImage = 0;
        }

        fflush(stdout);
    }

    return 0;
}
