
// hairline.cpp - test bed for faster GPU-based algorithms to draw stroked paths

#include <assert.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <GL/glew.h>
#ifdef __APPLE__
# include <GLUT/glut.h>
#else
# include <GL/glut.h>
# ifdef _WIN32
#  include <windows.h>  // for wglGetProcAddress
# else
#  include <GL/glx.h>  // for glXGetProcAddress
# endif
#endif

#include <vector>

#include <Cg/double.hpp>
#include <Cg/vector/xyzw.hpp>
#include <Cg/matrix/1based.hpp>
#include <Cg/matrix/rows.hpp>
#include <Cg/vector.hpp>
#include <Cg/matrix.hpp>

#include <Cg/all.hpp>
#include <Cg/any.hpp>
#include <Cg/degrees.hpp>
#include <Cg/distance.hpp>
#include <Cg/dot.hpp>
#include <Cg/inverse.hpp>
#include <Cg/mul.hpp>
#include <Cg/normalize.hpp>
#include <Cg/radians.hpp>
#include <Cg/round.hpp>
#include <Cg/sqrt.hpp>
#include <Cg/upper2x2.hpp>

#include <Cg/iostream.hpp>

#include "polar/global_polar_settings.hpp"
#include "polar/StrokingBufferState_global.hpp"
#include "polar/polar_shader_state.hpp"
#include "polar/StrokerShaderConfig_util.hpp"

using namespace Cg;
using std::vector;

// Make using iostream easier.
using std::cout;
using std::endl;

#include "sRGB_math.h"
#include "countof.h"
#include "request_vsync.h"
#include "showfps.h"
#include "cg4cpp_xform.hpp"
#include "gl_framebuffer.hpp"
#include "gl_program.hpp"
#include "gl_debug_callback.h"  // for GLRegisterDebugCallback
#include "gl_texture.hpp"
#include "glcolor.hpp"
#include "textures.hpp"

#include "sg/path.hpp"

#include "data/pr_dogs.hpp"
#include "data/pr_psi.hpp"

#include "polar/cpu/CPUStrokeTessellator.hpp"

#include "PathContainer.hpp"

#include "CurveConfig.hpp"

#include "color_space.hpp"

#include "Paint.hpp"
#include "polar/StrokeTessellator.hpp"
#include "hairline.hpp"
#include "menus.hpp"  // GLUT menu creation
#include "hodograph.hpp"
#include "graph.hpp"
#include "keyinput.hpp"
#include "mouse.hpp"
#include "load_paths.hpp"
#include "overlay.hpp"
#include "simple_path.hpp"
#include "main_window.hpp"
#include "decoration.hpp"
#include "cmdline.hpp"
#include "textures.hpp"
#include "glcolor.hpp"

#ifndef GLAPIENTRYP
# ifdef _WIN32
#  define GLAPIENTRYP __stdcall *
# else
#  define GLAPIENTRYP *
# endif
#endif

#define hairline_epsilon 0.0001

const char *program_name = "hairline";
bool animating = false;
int canvas_width = 800, canvas_height = 600;
bool use_supersample = false;
#if SUPPORT_POLAR_VISUALIZE
bool draw_raw_grid = false;  // visualize tessellator's "raw" grid
#endif
bool initially_no_caps = false;  // enable with -noCaps
bool initially_no_joins = false;  // enable with -noJoins

float initial_miter_limit = 1;

// projective warping state
float2 corners[4];
float3x3 model;
float3x3 modelview, inverse_modelview;

float4 box;

Paint current_paint;

bool use_NV_path_rendering = false;  // use NV_path_rendering or not
bool frame_sync = false;
bool view_conservative_rasterization = false;
bool draw_filled = false;
bool native_quadratics = true;
bool enable_gl_debug_output_synchronous = false;  // -glsync enables this

int decoration = DRAW_CONTROL_POINTS | DRAW_HULL_LINES | DRAW_SPLINE_POINTS;
bool show_status_text = true;

static GLQueryObjects gl_query_objs;

extern "C" {
int verbose;  // declared in "common/verbose.h"
}
int curveType = 0;

GLFramebufferCapabilities capabilities;
GLFramebuffer *fb;
int has_NV_conservative_raster;

int initial_stencil_samples = 8;
int initial_color_samples = 8;

// State for dealing with sRGB color spaces.
ColorSpace color_space = CORRECTED_SRGB;  // Insert toggles, best edge antialiasing quality when CORRECTED_SRGB

struct MainMouseState : MouseState {
    int close_corner;
    int close_vert;
    float2 moved_corners[4];

    MainMouseState()
        : close_corner(-1)
        , close_vert(-1)
    {}
};

MainMouseState ms_main;
int max_turning_angle_anchor = -1;  // Middle Alt drag controls fine-grain max turning angle

/* Global variables */
int background = 2;

int report_gl_stats_info = 0;  // 'I' key cycles: 0 == silent, 1 == visual results, 2 == printed results
bool dump_buffers = false;  // Alt-I toggles

bool outline = false;  // uses glPolygonMode GL_LINE
bool overlay_outline = true;
bool skip_paint_update = false;  // force to true during overlay_outline rendering

int main_window = -1;

FPScontext gl_fps_context;

static const struct BackgroundTable {
    float3 background_color;
    float3 contrast_color;
    const char* name;
} background_table[] = {
    { float3(0,0,0),         float3(1,1,1), "black" },
    { float3(1,1,1),         float3(0,0,0), "white" },
    { float3(0.2,0.15,0.35), float3(1,1,0), "purple" },
    { float3(0.5,0.5,0.5),   float3(1,1,1), "gray" },
};

void setBackground()
{
    float r, g, b, a;

    r = background_table[background].background_color.r;
    g = background_table[background].background_color.g;
    b = background_table[background].background_color.b;
    if (color_space == CORRECTED_SRGB) {
        r = convertSRGBColorComponentToLinearf(r);
        g = convertSRGBColorComponentToLinearf(g);
        b = convertSRGBColorComponentToLinearf(b);
    }
    a = 1.0;
    glClearColor(r,g,b,a);
}

float3 getBackgroundContrastColor()
{
    return background_table[background].contrast_color;
}

const char* backgroundString()
{
    return background_table[background].name;
}

bool isBackgroundWhite()
{
    const float3 bg = background_table[background].background_color;
    if (bg.r > 0.9f && bg.g > 0.9f && bg.b > 0.9f) {
        return true;
    } else {
        return false;
    }
}

static void fatalError(const char *message)
{
    fprintf(stderr, "%s: %s\n", program_name, message);
    exit(1);
}


bool isMainFramebuffer(const GLFramebuffer *this_fb)
{
    return this_fb == fb;
}

void updateColorSpace(GLFramebuffer *fb)
{
    if (isHodoFramebuffer(fb)) {
        // Nothing
    } else {
        setBackground();
    }
    if (color_space == CORRECTED_SRGB) {
        if (fb) {
            fb->colorEncodingConfiguration(GL_SRGB);
        }
    } else {
        if (fb) {
            fb->colorEncodingConfiguration(GL_LINEAR);
        }
    }
}

void
initGraphics()
{
    const int verticesPerPatch = 6;
    glPatchParameteri(GL_PATCH_VERTICES, verticesPerPatch);
}

void validateModelview()
{
    modelview = mul(ms_main.view, model);
    inverse_modelview = inverse(modelview);
}

float2 project(float3 v)
{
    return v.xy/v.z;
}

void setModelMatrix()
{
    model = quad2quad(corners, ms_main.moved_corners);
    validateModelview();
}

void initCornersFromBox(float2 corners[4], const float4 box)
{
    corners[0] = box.xy;
    corners[1] = box.zy;
    corners[2] = box.zw;
    corners[3] = box.xw;
}

void initModelMatrix()
{
    float percent_in = 0.80;
    box = float4(canvas_width/2-percent_in*canvas_height/2, (1-percent_in)*canvas_height,
        canvas_width/2+percent_in*canvas_height/2, percent_in*canvas_height);
    initCornersFromBox(corners, box);

    float4 moved_box = float4(canvas_width/2-percent_in*canvas_height/2, (1-percent_in)*canvas_height,
        canvas_width/2+percent_in*canvas_height/2, percent_in*canvas_height);
    initCornersFromBox(ms_main.moved_corners, moved_box);
    setModelMatrix();
}

void initModelAndViewMatrices()
{
    ms_main.view = float3x3(1,0,0,
        0,1,0,
        0,0,1);
    ms_main.inverse_view = inverse(ms_main.view);

    initHodographViewMatrices();
    initParametricGraphViewMatrices();

    initModelMatrix();
    setModelMatrix();
}

void configureProjection()
{
    float3x3 inverse_projection, viewport;

    viewport = ortho(0,ms_main.window_width, 0,ms_main.window_height);
    float left = 0, right = canvas_width, top = 0, bottom = canvas_height;
#if 0
    if (aspect_ratio > 1) {
        top *= aspect_ratio;
        bottom *= aspect_ratio;
    } else {
        left /= aspect_ratio;
        right /= aspect_ratio;
    }
#endif
    glMatrixLoadIdentityEXT(GL_PROJECTION);
    glMatrixOrthoEXT(GL_PROJECTION, left, right, bottom, top,
        -1, 1);
    ms_main.projection = ortho(left, right, bottom, top);
    inverse_projection = inverse_ortho(left, right, bottom, top);
    ms_main.view_width = right - left;
    ms_main.view_height = bottom - top;
    ms_main.win2view = mul(inverse_projection, viewport);
    ms_main.view2win = inverse(ms_main.win2view);
}

void establishFramebuffer(GLFramebuffer*& fb, int w, int h)
{
    if (fb) {
        fb->resize(w, h);
    } else {
        fb = new GLFramebuffer(capabilities,
            GL_SRGB8_ALPHA8,
            GL_STENCIL_INDEX8,
            w, h, 1,
            initial_stencil_samples, initial_color_samples,
            use_supersample ? GLFramebuffer::Supersample : GLFramebuffer::Resolve);
        fb->setAlphaHandling(GLFramebuffer::PremultipliedAlpha);
        fb->lineWidth(1.0f);

#if 0
        GLenum fb_encoding = fb->getRenderFramebufferColorEncoding(0);
        printf("framebuffer encoding = %s (0x%X)\n",
            fb_encoding == GL_SRGB ? "sRGB" : "linear", fb_encoding);
#endif

        updateColorSpace(fb);
    }
}

float getMainWindowWidth()
{
    return ms_main.window_width;
}

float getMainWindowHeight()
{
    return ms_main.window_height;
}

const float3x3& getMainProjectionMatrix()
{
    return ms_main.projection;
}

void reshape(int w, int h)
{
    reshapeFPScontext(&gl_fps_context, w, h);
    glViewport(0,0,w,h);

    ms_main.window_width = w;
    ms_main.window_height = h;
    ms_main.aspect_ratio = ms_main.window_height/ms_main.window_width;

    establishFramebuffer(fb, w, h);

    configureProjection();
}

const JoinStyleConfig joinStyleConfig[] = {
    { "Round",         GL_ROUND_NV,          false },
    { "Bevel",         GL_BEVEL_NV,          false },
    { "Triangular",    GL_TRIANGULAR_NV,     false },
    { "MiterRevert",   GL_MITER_REVERT_NV,   true  },  // PDF
    { "MiterTruncate", GL_MITER_TRUNCATE_NV, true  },  // Cairo
    { "None",          GL_NONE,              false }
};

size_t joinStyleConfigCount()
{
    return countof(joinStyleConfig);
}

GLenum currentGLJoinStyle()
{
    return joinStyleConfig[current_paint.JoinStyle()].gl_enum;
}

typedef StrokeConfigTypes::CurveType CurveType;

int second_draw = 0;  // Tab toggles.

static void drawRepeatedPath(float dx, float dy)
{
    float3x3 save_modelview = modelview;
    float3x3 save_inverse_modelview = inverse_modelview;

    glMatrixPushEXT(GL_PROJECTION); {
        const float3x3 trans(1,0,dx,
                             0,1,dy,
                             0,0,1);
        modelview = mul(modelview, trans);
        inverse_modelview = inverse(modelview);

        MatrixMultToGL(GL_PROJECTION, trans);

        LinkRenderStats stats;
        GLLinkRenderStats glstats(gl_query_objs);

        drawSimplePath(stats);
    } glMatrixPopEXT(GL_PROJECTION);

    modelview = save_modelview;
    inverse_modelview = save_inverse_modelview;
}

// returns count of quads drawn.
void drawScene(LinkRenderStats& stats)
{
    drawSimplePath(stats);
    if (second_draw) {
        float dx = 0;
        float dy = 200;
        drawRepeatedPath(dx, dy);
    }
}

void drawSceneViewStuff()
{
    glBindVertexArray(0);
    glUseProgram(0);
}

static void startQueriesIfNeeded(GLLinkRenderStats& glstats)
{
    if (report_gl_stats_info) {
        glstats.beginQueries();
    }
}

static void endQueriesIfNeeded(GLLinkRenderStats& glstats)
{
    if (report_gl_stats_info) {
        glstats.endQueries();
    }
}

static void renderScene(LinkRenderStats& stats)
{
    if (g_stroker_context.isWindowSpace()) {
        // Window space stroking (technically eye space).
        MatrixLoadToGL(GL_MODELVIEW, modelview);
        drawScene(stats);
        MatrixLoadToGL(GL_MODELVIEW, ms_main.view);
        drawSceneViewStuff();
    } else {
        // Object space stroking (technically eye space).
        glMatrixLoadIdentityEXT(GL_MODELVIEW);
        glMatrixPushEXT(GL_PROJECTION); {
            MatrixMultToGL(GL_PROJECTION, modelview);
            drawScene(stats);
        } glMatrixPopEXT(GL_PROJECTION);

        glMatrixPushEXT(GL_PROJECTION); {
            MatrixMultToGL(GL_PROJECTION, ms_main.view);
            drawSceneViewStuff();
        } glMatrixPopEXT(GL_PROJECTION);
    }
}

static void renderOverlayOutline()
{
    // Draw outlines in opaque black.
    const float3 color = getBackgroundContrastColor();
    current_paint.setStrokeColor(color);
    current_paint.setStrokeOpacity(1);
    sRGB_glColor3fv(&color[0]);
    skip_paint_update = true;  // Don't let rendering change the paint color!
    
    StrokeConfigTypes::ShadingMode save_shading_mode = g_stroker_context.getShadingMode();
    g_stroker_context.setShadingMode(StrokeConfigTypes::CONSTANT_COLOR);

    // XXX hacky global variable save/restore
    const bool save_outline = outline;
    outline = true;
    LinkRenderStats ignored_stats;
    renderScene(ignored_stats);
    outline = save_outline;

    g_stroker_context.setShadingMode(save_shading_mode);
    skip_paint_update = false;
}

void
display()
{
    gl_query_objs.ensureInitialized();

    LinkRenderStats stats;
    GLLinkRenderStats glstats(gl_query_objs);

    updateColorSpace(fb);
    fb->bind(); {
        startQueriesIfNeeded(glstats); {
            glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
            renderScene(stats);
        } endQueriesIfNeeded(glstats);
        if (overlay_outline) {
            renderOverlayOutline();
        }
    } fb->copy();
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    if (outline || overlay_outline) {
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);  // so handleFPS will work
    }
    if (fb->isSupersampled()) {
        glViewport(0,0, ms_main.window_width, ms_main.window_height);
        handleFPS(&gl_fps_context);
        fb->viewport();
    } else {
        handleFPS(&gl_fps_context);
    }

    if (report_gl_stats_info) {
        glstats.queryResults();
        if (report_gl_stats_info > 1) {
            glstats.printResults();
        }
    }

    if (show_status_text) {
        drawOverlayStatusText(fb, current_paint, stats);
        if (report_gl_stats_info) {
            drawOverlayGLStatsText(fb, glstats);
        }
    }

    glutSwapBuffers();
}

static bool selectControlPoint(const float2& win)
{
    float closest = 10;  // selected vertex no more than 10 pixel radius away from mouse click
    ms_main.close_vert = -1;
    for(size_t i = 0; i < inputPoints.size(); i++){
        float3 p = float3(inputPoints[i].x, inputPoints[i].y, 1.0f);
        p = mul(modelview, p);
        p = mul(ms_main.view2win, p);
        float2 window_p = project(p);
        float distance_to_vert = distance(window_p, win);

        if (distance_to_vert < closest) {
            ms_main.close_vert = int(i);
            closest = distance_to_vert;
        }
    }
    if (ms_main.close_vert >= 0) {
        // Selected the closest vertex for dragging.
        ms_main.zooming = 0;
        ms_main.scaling = 0;
        return true;
    }
    return false;
}

void beginZoomAndScaleView(int mouse_space_x, int mouse_space_y)
{
    assert(ms_main.close_vert == -1);
    ms_main.anchor = project(mul(ms_main.win2view, float3(mouse_space_x, mouse_space_y,1)));

    ms_main.rotate_x = mouse_space_x;
    ms_main.scale_y = mouse_space_y;
    if (!(glutGetModifiers() & GLUT_ACTIVE_CTRL)) {
        ms_main.scaling = 1;
    } else {
        ms_main.scaling = 0;
    }
    if (!(glutGetModifiers() & GLUT_ACTIVE_SHIFT)) {
        ms_main.zooming = 1;
    } else {
        ms_main.zooming = 0;
    }
}

static void stopLeftMouseOperation()
{
    ms_main.zooming = 0;
    ms_main.scaling = 0;
    ms_main.close_vert = -1;
    ms_main.close_corner = -1;
}

void beginPanView(int mouse_space_x, int mouse_space_y, const int modifiers)
{
    ms_main.close_vert = -1;
    if (modifiers & GLUT_ACTIVE_CTRL) {
        ms_main.slide_scale = 1.0/8.0;
    } else {
        ms_main.slide_scale = 1.0;
    }
    ms_main.slide_x = mouse_space_x;
    ms_main.slide_y = mouse_space_y;
    if (modifiers & GLUT_ACTIVE_SHIFT) {
        // If Shiftheld down, confine translation to Y direction
    } else {
        ms_main.x_sliding = true;
    }
    ms_main.y_sliding = true;
}

static void stopMiddleMouseOperation()
{
    max_turning_angle_anchor = -1;
    ms_main.x_sliding = false;
    ms_main.y_sliding = false;
}

void
mouse(int button, int state, int mouse_space_x, int mouse_space_y)
{
    mouse_space_y = ms_main.window_height - mouse_space_y;
    const int modifiers = glutGetModifiers() & (GLUT_ACTIVE_CTRL|GLUT_ACTIVE_SHIFT|GLUT_ACTIVE_ALT);

    if (button == GLUT_LEFT_BUTTON) {
        if (state == GLUT_DOWN) { 
            const float2 win = float2(mouse_space_x, mouse_space_y);

            bool got_point = selectControlPoint(win);
            if (got_point) {
                return;
            }

            beginZoomAndScaleView(mouse_space_x, mouse_space_y);
        } else {
            assert(state == GLUT_UP);
            stopLeftMouseOperation();
        }
    }
    if (button == GLUT_MIDDLE_BUTTON) {
        if (state == GLUT_DOWN) {
            if (modifiers & GLUT_ACTIVE_ALT) {
                max_turning_angle_anchor = mouse_space_y;
                return;
            }
            beginPanView(mouse_space_x, mouse_space_y, modifiers);
        } else {
            assert(state == GLUT_UP);
            stopMiddleMouseOperation();
        }
        if (verbose) {
            printf("sliding%s%s: %f,%f\n",
                ms_main.x_sliding ? "X" : "",
                ms_main.y_sliding ? "Y" : "",
                ms_main.slide_x, ms_main.slide_y);
        }
    }
}

static void moveProjectionBoxCorner(int mouse_space_x, int mouse_space_y)
{
    assert(!ms_main.zooming);
    assert(!ms_main.scaling);
    const float3 win = float3(mouse_space_x, mouse_space_y, 1);
    float3 p = mul(ms_main.win2view, win);
    p = mul(ms_main.inverse_view, p);
    const float2 new_corner = project(p);
    if (verbose) {
        cout << "old corner = " << ms_main.moved_corners[ms_main.close_corner] << endl;
        cout << "new corner = " << new_corner << endl;
    }
    ms_main.moved_corners[ms_main.close_corner] = new_corner;
    setModelMatrix();
    postAllWindowsRedisplay();
}

static void moveControlPoint(int mouse_space_x, int mouse_space_y)
{
    float3 win = float3(mouse_space_x, mouse_space_y, 1);
    float3 p = mul(ms_main.win2view, win);
    p = mul(inverse_modelview, p);
    updatePoint(ms_main.close_vert, project(p));
    postAllWindowsRedisplay();
}

static void zoomAndScaleView(int mouse_space_x, int mouse_space_y)
{
    float angle = 0;
    float zoom = 1;
    if (ms_main.scaling) {
        angle = 0.3 * (ms_main.rotate_x - mouse_space_x) * canvas_width/ms_main.window_width;
    }
    if (ms_main.zooming) {
        zoom = pow(1.003f, (ms_main.scale_y - mouse_space_y) * canvas_height/ms_main.window_height);
    }

    float3x3 t = translate(ms_main.anchor);
    float3x3 r = rotate(angle);
    float3x3 s = scale(zoom, zoom);

    r = mul(r, s);
    float3x3 m = mul(t, r);
    t = translate(-ms_main.anchor);
    m = mul(m, t);
    ms_main.view = mul(m, ms_main.view);
    ms_main.inverse_view = inverse(ms_main.view);
    validateModelview();

    ms_main.rotate_x = mouse_space_x;
    ms_main.scale_y = mouse_space_y;
    postAllWindowsRedisplay();
}

static void panView(int mouse_space_x, int mouse_space_y)
{
    float x_offset = 0,
          y_offset = 0;

    if (ms_main.x_sliding) {
        x_offset = (mouse_space_x - ms_main.slide_x) * ms_main.view_width/ms_main.window_width;
        x_offset *= ms_main.slide_scale;
    }
    if (ms_main.y_sliding) {
        y_offset = (ms_main.slide_y - mouse_space_y) * ms_main.view_height/ms_main.window_height;
        y_offset *= ms_main.slide_scale;
    }

    float3x3 m = translate(x_offset, y_offset);
    ms_main.view = mul(m, ms_main.view);
    ms_main.inverse_view = inverse(ms_main.view);
    validateModelview();

    ms_main.slide_y = mouse_space_y;
    ms_main.slide_x = mouse_space_x;
    ms_main.scale_y = mouse_space_y;
    postAllWindowsRedisplay();
}

void
motion(int mouse_space_x, int mouse_space_y)
{
    mouse_space_y = ms_main.window_height - mouse_space_y;

    if (max_turning_angle_anchor != -1) {
        int diff = mouse_space_y - max_turning_angle_anchor;
#if SUPPORT_POLAR_VISUALIZE
        if (stroke_tessellator_settings.isUniformStroking()){
            const int pixels_per_segment_bias = 3;
            const int segment_change = diff/pixels_per_segment_bias;
            if (verbose) {
                printf("diff = %d, segment_change = %d\n", diff, segment_change);
            }
            unsigned int uniform_segments = stroke_tessellator_settings.UniformSegments();
            if (uniform_segments + segment_change > 1) {
                uniform_segments += segment_change;
                stroke_tessellator_settings.setUniformSegments(uniform_segments);
                max_turning_angle_anchor += segment_change*pixels_per_segment_bias;
                postAllWindowsRedisplay();
            }
        } else 
#endif
        {
            float turning_angle_change = 0.0005*diff;
            if (verbose) {
                printf("diff = %d, turning_angle_change = %g\n", diff, float(degrees(turning_angle_change)));
            }
            stroke_tessellator_settings.biasMaxTurningAngleDelta(turning_angle_change);
            postAllWindowsRedisplay();
            max_turning_angle_anchor = mouse_space_y;
        }
        return;
    }

    // Is a control point selected?
    if (ms_main.close_vert >= 0){
        moveControlPoint(mouse_space_x, mouse_space_y);
    } else {
        // No control point selected...
        if (ms_main.zooming || ms_main.scaling) {
            zoomAndScaleView(mouse_space_x, mouse_space_y);
        }
        if (ms_main.x_sliding || ms_main.y_sliding) {
            panView(mouse_space_x, mouse_space_y);
        }
    }
}

void dumpCorners()
{
    printf("  { float2(%g,%g), float2(%g,%g), float2(%g,%g), float2(%g,%g) }\n", 
        float(ms_main.moved_corners[0].x),
        float(ms_main.moved_corners[0].y),
        float(ms_main.moved_corners[1].x),
        float(ms_main.moved_corners[1].y),
        float(ms_main.moved_corners[2].x),
        float(ms_main.moved_corners[2].y),
        float(ms_main.moved_corners[3].x),
        float(ms_main.moved_corners[3].y));
    dumpInputPoints();
}

void initPaint()
{
    current_paint.setStrokeWidth(25);  // Override initial value of 1
    current_paint.setArcLengthExtension(0);
    current_paint.setMiterLimit(initial_miter_limit);  // Override initial value of 4
    current_paint.setJoinStyle(Paint::ROUND_JOIN);  // Override initial value of MITER_REVERT
    current_paint.setCapsIdentical(Paint::ROUND_CAP);
    if (initially_no_caps) {
        current_paint.forceNoCaps();
    }
    if (initially_no_joins) {
        current_paint.setJoinStyle(Paint::NO_JOIN);
    }
}

int
main(int argc, char **argv)
{
    GLenum status;
    GLboolean hasDSA;
    GLboolean has_NV_path_rendering;
    int samples = 0;

    glutInitWindowSize(canvas_width, canvas_height);
    glutInit(&argc, argv);
    parseCommandLine(argc, argv);

    // Just need double buffered window since real rendering goes to the fb FBO.
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);

    main_window = glutCreateWindow("Test bed for faster GPU-based algorithms to draw stroked paths");
    printf("vendor: %s\n", glGetString(GL_VENDOR));
    printf("version: %s\n", glGetString(GL_VERSION));
    printf("renderer: %s\n", glGetString(GL_RENDERER));
    printf("samples = %d\n", glutGet(GLUT_WINDOW_NUM_SAMPLES));
    printf("stencilBits = %d\n", glutGet(GLUT_WINDOW_STENCIL_SIZE));
    printf("Executable: %d bit\n", (int)(8*sizeof(int*)));

    // Initialized OpenGL Extension Wrangler (GLEW) so OpenGL extensions functions are initialized.
    status = glewInit();
    if (status != GLEW_OK) {
        fatalError("OpenGL Extension Wrangler (GLEW) failed to initialize");
    }
    if (registerDebugCallback) {
        GLRegisterDebugCallback();
        if (enable_GL_DEBUG_OUTPUT_SYNCHRONOUS) {
            glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
        }
    }
    // Use glutExtensionSupported because glewIsSupported is unreliable for DSA.
    hasDSA = glutExtensionSupported("GL_EXT_direct_state_access");
    if (!hasDSA) {
        fatalError("OpenGL implementation doesn't support GL_EXT_direct_state_access...");
    }

    has_NV_path_rendering = glutExtensionSupported("GL_NV_path_rendering");
    if (!has_NV_path_rendering) {
        fatalError("required NV_path_rendering OpenGL extension is not present");
    }

    has_NV_conservative_raster = glutExtensionSupported("GL_NV_conservative_raster");
    if (initial_stencil_samples != initial_color_samples) {
        if (capabilities.hasFramebufferMixedSamples()) {
            if (has_NV_conservative_raster) {
                // Good, required for proper blending.
            } else {
                // This should never happen as Maxwell 2 introduced both
                // NV_framebuffer_mixed_samples & NV_conservative_raster in
                // the same architecture.
                fatalError("requires NV_conservative_raster when rendering with mixed samples");
            }
        } else {
            fatalError("requires NV_framebuffer_mixed_samples for different color and coverage samples");
        }
    }

    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutKeyboardFunc(keyboard);
    glutSpecialFunc(special);
    glutMouseFunc(mouse);
    glutMotionFunc(motion);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glPixelStorei(GL_PACK_ALIGNMENT, 1);  // avoid GL's dumb default of 4
    loadTextures("textures");

    initPaint();
    createMenu();
    initGraphics();
    initModelAndViewMatrices();

    // Request dumping of combined shader source (helps debugging GLSL compilation failures).
    GLSLDumpSource(true);

    requestSynchornizedSwapBuffers(frame_sync);
    updateAnimation();
    disableFPS();
    if (enable_gl_debug_output_synchronous) {
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
    }

    initFPScontext(&gl_fps_context, FPS_USAGE_TEXTURE);
    setFPSorigin(FPS_UPPER_RIGHT);
    scaleFPS(3);

    establishArcLengthBuffers();

    loadPaths();

#if 0
    test_angleIntervalAdd();
    test_isWithinRange();
#endif

    glutMainLoop();
    return 0;
}

void postMainWindowRedisplay()
{
    glutPostWindowRedisplay(main_window);
}

void postAllWindowsRedisplay()
{
    postMainWindowRedisplay();
    postHodographWindowRedisplay();
    postGraphWindowRedisplay();
}

void postGraphingWindowsRedisplay()
{
    postHodographWindowRedisplay();
    postGraphWindowRedisplay();
}

void setMainWindow()
{
    glutSetWindow(main_window);
}

void setSupersampledMainWindow(bool use_supersample)
{
    fb->setSupersampled(use_supersample);
}
