/*
 * Copyright 2018 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#ifdef _MSC_VER
    #define _CRT_SECURE_NO_WARNINGS
    #define SKCMS_NORETURN __declspec(noreturn)
#else
    #include <dlfcn.h>
    #include <stdnoreturn.h>
    #define SKCMS_NORETURN noreturn
#endif

#include "test_only.h"
#include "src/skcms_internals.h"
#include "src/skcms_public.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

SKCMS_NORETURN
static void fatal(const char* msg) {
    fprintf(stderr, "ERROR: %s\n", msg);
    exit(1);
}

// xy co-ordinates of the CIE 1931 standard observer XYZ functions.
// wavelength is sampled every 5 nm in [360, 700].
// This is effectively the hull of the horseshoe in a chromaticity diagram.
static const double kSpectralHull[] = {
    0.17556, 0.00529384,
    0.175161, 0.00525635,
    0.174821, 0.0052206,
    0.17451, 0.00518164,
    0.174112, 0.00496373,
    0.174008, 0.00498055,
    0.173801, 0.00491541,
    0.17356, 0.0049232,
    0.173337, 0.00479674,
    0.173021, 0.00477505,
    0.172577, 0.0047993,
    0.172087, 0.00483252,
    0.171407, 0.00510217,
    0.170301, 0.00578851,
    0.168878, 0.00690024,
    0.166895, 0.00855561,
    0.164412, 0.0108576,
    0.161105, 0.0137934,
    0.156641, 0.0177048,
    0.150985, 0.0227402,
    0.14396, 0.029703,
    0.135503, 0.0398791,
    0.124118, 0.0578025,
    0.109594, 0.0868425,
    0.0912935, 0.132702,
    0.0687059, 0.200723,
    0.0453907, 0.294976,
    0.0234599, 0.412703,
    0.00816803, 0.538423,
    0.00385852, 0.654823,
    0.0138702, 0.750186,
    0.0388518, 0.812016,
    0.0743024, 0.833803,
    0.114161, 0.826207,
    0.154722, 0.805863,
    0.192876, 0.781629,
    0.22962, 0.754329,
    0.265775, 0.724324,
    0.301604, 0.692308,
    0.337363, 0.658848,
    0.373102, 0.624451,
    0.408736, 0.589607,
    0.444062, 0.554714,
    0.478775, 0.520202,
    0.512486, 0.486591,
    0.544787, 0.454434,
    0.575151, 0.424232,
    0.602933, 0.396497,
    0.627037, 0.372491,
    0.648233, 0.351395,
    0.665764, 0.334011,
    0.680079, 0.319747,
    0.691504, 0.308342,
    0.700606, 0.299301,
    0.707918, 0.292027,
    0.714032, 0.285929,
    0.719033, 0.280935,
    0.723032, 0.276948,
    0.725992, 0.274008,
    0.728272, 0.271728,
    0.729969, 0.270031,
    0.731089, 0.268911,
    0.731993, 0.268007,
    0.732719, 0.267281,
    0.733417, 0.266583,
    0.734047, 0.265953,
    0.73439, 0.26561,
    0.734592, 0.265408,
    0.73469, 0.26531,
};

static uint16_t read_big_u16(const uint8_t* ptr) {
    uint16_t be;
    memcpy(&be, ptr, sizeof(be));
#if defined(_MSC_VER)
    return _byteswap_ushort(be);
#else
    return __builtin_bswap16(be);
#endif
}

static uint32_t read_big_u32(const uint8_t* ptr) {
    uint32_t be;
    memcpy(&be, ptr, sizeof(be));
#if defined(_MSC_VER)
    return _byteswap_ulong(be);
#else
    return __builtin_bswap32(be);
#endif
}

// TODO: Put state into struct with FP
static int desmos_id = 0;

static FILE* desmos_open(const char* filename) {
    FILE* fp = fopen(filename, "wb");
    if (!fp) {
        fatal("Unable to open output file");
    }

    fprintf(fp, "<!DOCTYPE html>\n");
    fprintf(fp, "<html>\n");
    fprintf(fp, "<head>\n");
    fprintf(fp, "<script src=\"https://www.desmos.com/api/v1.1/calculator.js?apiKey=dcb31709b452b1cf9dc26972add0fda6\"></script>\n");
    fprintf(fp, "<style>\n");
    fprintf(fp, "  html, body{ width: 100%%; height: 100%%; margin: 0; padding: 0; overflow: hidden; }\n");
    fprintf(fp, "  #calculator { width: 100%%; height: 100%%; }\n");
    fprintf(fp, "</style>\n");
    fprintf(fp, "</head>\n");
    fprintf(fp, "<body>\n");
    fprintf(fp, "<div id=\"calculator\"></div>\n");
    fprintf(fp, "<script>\n");
    fprintf(fp, "var elt = document.getElementById('calculator');\n");
    fprintf(fp, "var c = Desmos.GraphingCalculator(elt);\n");
    fprintf(fp, "c.setState({\n");
    fprintf(fp, "\"version\": 5,\n");
    fprintf(fp, "\"expressions\": {\n");
    fprintf(fp, "\"list\": [\n");

    desmos_id = 0;
    return fp;
}

static void desmos_close(FILE* fp) {
    fprintf(fp, "] } } );\n");
    fprintf(fp, "c.setMathBounds({left: -0.1, right: 1.1, bottom: -0.1, top: 1.1});\n");
    fprintf(fp, "</script>\n");
    fprintf(fp, "</body>\n");
    fprintf(fp, "</html>\n");
    fclose(fp);
}

static void desmos_transfer_function(FILE* fp, const skcms_TransferFunction* tf,
                                     const char* color) {
    fprintf(fp, "{\n");
    fprintf(fp, " \"type\": \"expression\",\n");
    fprintf(fp, " \"id\": \"%d\",\n", desmos_id++);
    fprintf(fp, " \"color\": \"%s\",\n", color);
    fprintf(fp, " \"latex\": \"\\\\left\\\\{"
            "0 \\\\le x < %.5f: %.5fx + %.5f, "                    // 0 <= x < d: cx + f
            "%.5f \\\\le x \\\\le 1: (%.5fx + %.5f)^{%.5f} + %.5f" // d <= x <= 1: (ax + b)^g + e
            "\\\\right\\\\}\"\n",
            tf->d, tf->c, tf->f,
            tf->d, tf->a, tf->b, tf->g, tf->e);
    fprintf(fp, "},\n");
}

typedef double table_func(int i, const void* ctx);

static void desmos_table(FILE* fp, int N, const char* label, const char* color,
                         table_func* x, const void* x_ctx,
                         table_func* y, const void* y_ctx) {
    int folder_id = desmos_id++,
        table_id  = desmos_id++,
        subscript = desmos_id++;

    // Folder
    fprintf(fp, "{\n");
    fprintf(fp, " \"type\": \"folder\",\n");
    fprintf(fp, " \"id\": \"%d\",\n", folder_id);
    fprintf(fp, " \"title\": \"%s\",\n", label);
    fprintf(fp, " \"collapsed\": true,\n");
    fprintf(fp, " \"memberIds\": { \"%d\": true }\n", table_id);
    fprintf(fp, "},\n");

    // Table
    fprintf(fp, "{\n");
    fprintf(fp, " \"type\": \"table\",\n");
    fprintf(fp, " \"id\": \"%d\",\n", table_id);
    fprintf(fp, " \"columns\": [\n");

    // X Column
    fprintf(fp, " {\n");
    fprintf(fp, "  \"values\": [");

    for (int i = 0; i < N; ++i) {
        if (i % 6 == 0) {
            fprintf(fp, "\n  ");
        }
        fprintf(fp, " \"%.5f\",", x(i, x_ctx));
    }

    fprintf(fp, "  ],\n");
    fprintf(fp, "  \"hidden\": true,\n");
    fprintf(fp, "  \"id\": \"%d\",\n", desmos_id++);
    fprintf(fp, "  \"color\": \"%s\",\n", color);
    fprintf(fp, "  \"latex\": \"x_{%d}\"\n", subscript);
    fprintf(fp, " },\n");

    // Y Column
    fprintf(fp, " {\n");
    fprintf(fp, "  \"values\": [\n");

    for (int i = 0; i < N; ++i) {
        if (i % 6 == 0) {
            fprintf(fp, "\n  ");
        }
        fprintf(fp, " \"%.5f\",", y(i, y_ctx));
    }
    fprintf(fp, "  ],\n");
    fprintf(fp, "  \"id\": \"%d\",\n", desmos_id++);
    fprintf(fp, "  \"color\": \"%s\",\n", color);
    fprintf(fp, "  \"latex\": \"y_{%d}\"\n", subscript);
    fprintf(fp, " }\n");
    fprintf(fp, " ]\n");
    fprintf(fp, "},\n");
}

static double uniform_scale_table_func(int i, const void* ctx) {
    double scale = *((const double*)ctx);
    return i * scale;
}

static double curve_table_func(int i, const void* ctx) {
    const skcms_Curve* curve = (const skcms_Curve*)ctx;
    return curve->table_8 ? curve->table_8[i] / 255.0
                          : read_big_u16(curve->table_16 + 2*i) / 65535.0;
}

static void desmos_curve(FILE* fp, const skcms_Curve* curve, const char* color) {
    if (!curve->table_entries) {
        desmos_transfer_function(fp, &curve->parametric, color);
        return;
    }

    char label[64];
    (void)snprintf(label, sizeof(label), "%s Table", color);

    double xScale = 1.0 / (curve->table_entries - 1.0);
    desmos_table(fp, (int)curve->table_entries, label, color,
                 uniform_scale_table_func, &xScale,
                 curve_table_func, curve);

    char approx_color[64];
    (void)snprintf(approx_color, sizeof(approx_color), "Dark%s", color);

    skcms_TransferFunction approx_tf;
    float max_error;
    if (skcms_ApproximateCurve(curve, &approx_tf, &max_error)) {
        desmos_transfer_function(fp, &approx_tf, approx_color);
    }
}

static void desmos_curves(FILE* fp, uint32_t num_curves, const skcms_Curve* curves,
                          const char** colors) {
    for (uint32_t c = 0; c < num_curves; ++c) {
        desmos_curve(fp, curves + c, colors[c]);
    }
}

static void desmos_inv_curve(FILE* fp, const skcms_Curve* curve, const char* color) {
    if (!curve->table_entries) {
        skcms_TransferFunction inv;
        if (skcms_TransferFunction_invert(&curve->parametric, &inv)) {
            desmos_transfer_function(fp, &inv, color);
        }
        return;
    }

    char label[64];
    (void)snprintf(label, sizeof(label), "%s Inverse Table", color);

    double xScale = 1.0 / (curve->table_entries - 1.0);
    desmos_table(fp, (int)curve->table_entries, label, color,
                 curve_table_func, curve,
                 uniform_scale_table_func, &xScale);

    char approx_color[64];
    (void)snprintf(approx_color, sizeof(approx_color), "Dark%s", color);

    skcms_TransferFunction approx_tf;
    float max_error;
    if (skcms_ApproximateCurve(curve, &approx_tf, &max_error)) {
        skcms_TransferFunction inv;
        if (skcms_TransferFunction_invert(&approx_tf, &inv)) {
            desmos_transfer_function(fp, &inv, approx_color);
        }
    }
}

static void desmos_inv_curves(FILE* fp, uint32_t num_curves, const skcms_Curve* curves,
                              const char** colors) {
    for (uint32_t c = 0; c < num_curves; ++c) {
        desmos_inv_curve(fp, curves + c, colors[c]);
    }
}

static const double kSVGMarginLeft   = 100.0;
static const double kSVGMarginRight  = 10.0;
static const double kSVGMarginTop    = 10.0;
static const double kSVGMarginBottom = 50.0;

static const double kSVGScaleX = 800.0;
static const double kSVGScaleY = 800.0;

static const char* kSVG_RGB_Colors[3] = { "Red", "Green", "Blue" };
static const char* kSVG_CMYK_Colors[4] = { "cyan", "magenta", "yellow", "black" };

static FILE* svg_open(const char* filename) {
    FILE* fp = fopen(filename, "wb");
    if (!fp) {
        fatal("Unable to open output file");
    }

    fprintf(fp, "<svg width=\"%g\" height=\"%g\" xmlns=\"http://www.w3.org/2000/svg\">\n",
            kSVGMarginLeft + kSVGScaleX + kSVGMarginRight,
            kSVGMarginTop + kSVGScaleY + kSVGMarginBottom);
    return fp;
}

static void svg_close(FILE* fp) {
    fprintf(fp, "</svg>\n");
    fclose(fp);
}

#define svg_push_group(fp, fmt, ...) fprintf(fp, "<g " fmt ">\n", __VA_ARGS__)

static void svg_pop_group(FILE* fp) {
    fprintf(fp, "</g>\n");
}

static void svg_axes(FILE* fp) {
    fprintf(fp, "<polyline fill=\"none\" stroke=\"black\" vector-effect=\"non-scaling-stroke\" "
                "points=\"0,1 0,0 1,0\"/>\n");
}

static void svg_transfer_function(FILE* fp, const skcms_TransferFunction* tf, const char* color) {
    fprintf(fp, "<polyline fill=\"none\" stroke=\"%s\" vector-effect=\"non-scaling-stroke\" "
            "points=\"\n", color);

    for (int i = 0; i < 256; ++i) {
        float x = (float)i / 255.0f;
        float t = skcms_TransferFunction_eval(tf, x);
        fprintf(fp, "%g, %g\n", x, t);
    }
    fprintf(fp, "\"/>\n");
}

static void svg_curve(FILE* fp, const skcms_Curve* curve, const char* color) {
    if (!curve->table_entries) {
        svg_transfer_function(fp, &curve->parametric, color);
        return;
    }

    double xScale = 1.0 / (curve->table_entries - 1.0);
    double yScale = curve->table_8 ? (1.0 / 255) : (1.0 / 65535);
    fprintf(fp, "<polyline fill=\"none\" stroke=\"%s\" vector-effect=\"non-scaling-stroke\" "
            "transform=\"scale(%g %g)\" points=\"\n",
            color, xScale, yScale);

    for (uint32_t i = 0; i < curve->table_entries; ++i) {
        if (curve->table_8) {
            fprintf(fp, "%3u, %3u\n", i, curve->table_8[i]);
        } else {
            fprintf(fp, "%4u, %5u\n", i, read_big_u16(curve->table_16 + 2 * i));
        }
    }
    fprintf(fp, "\"/>\n");

    skcms_TransferFunction approx_tf;
    float max_error;
    if (skcms_ApproximateCurve(curve, &approx_tf, &max_error)) {
        svg_transfer_function(fp, &approx_tf, "magenta");
    }
}

static void svg_curves(FILE* fp, uint32_t num_curves, const skcms_Curve* curves,
                       const char** colors) {
    for (uint32_t c = 0; c < num_curves; ++c) {
        svg_curve(fp, curves + c, colors[c]);
    }
}

static void dump_curves_svg(const char* filename, uint32_t num_curves, const skcms_Curve* curves) {
    FILE* fp = svg_open(filename);
    svg_push_group(fp, "transform=\"translate(%g %g) scale(%g %g)\"",
                   kSVGMarginLeft, kSVGMarginTop + kSVGScaleY, kSVGScaleX, -kSVGScaleY);
    svg_axes(fp);
    svg_curves(fp, num_curves, curves, (num_curves == 3) ? kSVG_RGB_Colors : kSVG_CMYK_Colors);
    svg_pop_group(fp);
    svg_close(fp);
}

static const uint8_t png_signature[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };

#if defined(_MSC_VER)
    static bool parse_png_profile(const uint8_t* buf, size_t len, skcms_ICCProfile* profile) {
        (void)buf;
        (void)len;
        (void)profile;
        (void)read_big_u32;
        return false;
    }
#else
    static bool parse_png_profile(const uint8_t* buf, size_t len, skcms_ICCProfile* profile) {
        void* zlib = NULL;
        if (!zlib) { zlib = dlopen("libz.so",    RTLD_LAZY); }
        if (!zlib) { zlib = dlopen("libz.dylib", RTLD_LAZY); }
        if (!zlib) {
            return false;
        }

        typedef int(*UncompressFn)(uint8_t*, unsigned long*, const uint8_t*, unsigned long);
        UncompressFn uncompress = (UncompressFn)dlsym(zlib, "uncompress");
        if (!uncompress) {
            return false;
        }

        const uint8_t* end = buf+len;

        // skip over signature
        buf += sizeof(png_signature);

        const uint32_t IEND = 0x49454e44,
                       iCCP = 0x69434350;

        uint32_t size, tag = 0;

        while (buf < end && tag != IEND) {
            size = read_big_u32(buf+0);
            tag  = read_big_u32(buf+4);
            buf += 8;

            if (tag == iCCP) {
                const char* name = (const char*)buf;
                printf("Profile name from .png: '%s'\n", name);

                size_t header = strlen(name)
                              + 1/*NUL*/
                              + 1/*PNG compression method, always 0 == zlib*/;

                unsigned long inf_size,
                              guess = len;
                void* inflated = NULL;

                int err;
                do {
                    inf_size = guess;
                    inflated = realloc(inflated, inf_size);

                    err = uncompress(inflated, &inf_size,
                                     (const uint8_t*)name+header, size-header);
                    guess *= 2;
                } while (err == -5/*Z_BUF_ERROR*/);

                bool ok = err == 0/*Z_OK*/
                       && skcms_Parse(inflated, inf_size, profile);
                free(inflated);
                return ok;
            }

            buf += size;
            buf += 4/*skip the PNG CRC*/;
        }
        return false;
    }
#endif

int main(int argc, char** argv) {
    const char* filename = NULL;
    bool svg = false;
    bool desmos = false;

    for (int i = 1; i < argc; ++i) {
        if (0 == strcmp(argv[i], "-s")) {
            svg = true;
        } else if (0 == strcmp(argv[i], "-d")) {
            desmos = true;
        } else {
            filename = argv[i];
        }
    }

    if (!filename) {
        printf("usage: %s [-s] <ICC filename>\n", argv[0]);
        return 1;
    }

    void* buf = NULL;
    size_t len = 0;
    if (!load_file(filename, &buf, &len)) {
        fatal("Unable to load input file");
    }

    skcms_ICCProfile profile;
    if (len >= sizeof(png_signature) && 0 == memcmp(buf, png_signature, sizeof(png_signature))) {
        if (!parse_png_profile(buf, len, &profile)) {
            fatal("Could not find an ICC profile in this .png");
        }
    } else if (!skcms_Parse(buf, len, &profile)) {
        fatal("Unable to parse ICC profile");
    }

    dump_profile(&profile, stdout);

    if (desmos) {
        if (profile.has_trc) {
            FILE* fp = desmos_open("TRC_curves.html");
            desmos_curves(fp, 3, profile.trc, kSVG_RGB_Colors);
            desmos_inv_curves(fp, 3, profile.trc, kSVG_RGB_Colors);
            desmos_close(fp);
        }
    }

    if (svg) {
        if (profile.has_toXYZD50) {
            FILE* fp = svg_open("gamut.svg");
            svg_push_group(fp, "transform=\"translate(%g %g) scale(%g %g)\"",
                           kSVGMarginLeft, kSVGMarginTop + kSVGScaleY, kSVGScaleX, -kSVGScaleY);
            svg_axes(fp);

            fprintf(fp, "<polygon fill=\"none\" stroke=\"black\" "
                    "vector-effect=\"non-scaling-stroke\" points=\"\n");
            for (int i = 0; i < ARRAY_COUNT(kSpectralHull); i += 2) {
                fprintf(fp, "%g, %g\n", kSpectralHull[i], kSpectralHull[i + 1]);
            }
            fprintf(fp, "\"/>\n");

            skcms_Matrix3x3 m = profile.toXYZD50;
            skcms_Matrix3x3 chad;
            if (skcms_GetCHAD(&profile, &chad) && skcms_Matrix3x3_invert(&chad, &chad)) {
                m = skcms_Matrix3x3_concat(&chad, &m);
            }

            float rSum = m.vals[0][0] + m.vals[1][0] + m.vals[2][0];
            float gSum = m.vals[0][1] + m.vals[1][1] + m.vals[2][1];
            float bSum = m.vals[0][2] + m.vals[1][2] + m.vals[2][2];
            fprintf(fp, "<polygon fill=\"none\" stroke=\"black\" "
                    "vector-effect=\"non-scaling-stroke\" points=\"%g,%g %g,%g %g,%g\"/>\n",
                    (m.vals[0][0] / rSum), (m.vals[1][0] / rSum),
                    (m.vals[0][1] / gSum), (m.vals[1][1] / gSum),
                    (m.vals[0][2] / bSum), (m.vals[1][2] / bSum));

            svg_pop_group(fp);
            svg_close(fp);
        }

        if (profile.has_trc) {
            FILE* fp = svg_open("TRC_curves.svg");
            svg_push_group(fp, "transform=\"translate(%g %g) scale(%g %g)\"",
                           kSVGMarginLeft, kSVGMarginTop + kSVGScaleY, kSVGScaleX, -kSVGScaleY);
            svg_axes(fp);
            svg_curves(fp, 3, profile.trc, kSVG_RGB_Colors);
            svg_pop_group(fp);
            svg_close(fp);
        }

        if (profile.has_A2B) {
            const skcms_A2B* a2b = &profile.A2B;
            if (a2b->input_channels) {
                dump_curves_svg("A_curves.svg", a2b->input_channels, a2b->input_curves);
            }

            if (a2b->matrix_channels) {
                dump_curves_svg("M_curves.svg", a2b->matrix_channels, a2b->matrix_curves);
            }

            dump_curves_svg("B_curves.svg", a2b->output_channels, a2b->output_curves);
        }
    }

    return 0;
}
