
// keyinput.cpp - implementation of keyboard callbacks for "hairline" stroking algorithm testbed

#define _USE_MATH_DEFINES
#include <assert.h>
#include <stdio.h>
#include <stdlib.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/degrees.hpp>

using namespace Cg;
using std::vector;

#include "sRGB_math.h"
#include "request_vsync.h"
#include "showfps.h"  // for invalidateFPS

#include "PathContainer.hpp"

#include "CurveConfig.hpp"

#include "color_space.hpp"

#include "Paint.hpp"
#include "polar/StrokeTessellator.hpp"
#include "polar/global_polar_settings.hpp"
#include "polar/StrokingBufferState_global.hpp"

#include "hairline.hpp"
#include "load_paths.hpp"
#include "menus.hpp"
#include "hodograph.hpp"
#include "graph.hpp"
#include "keyinput.hpp"
#include "simple_path.hpp"
#include "cmdline.hpp"
#include "main_window.hpp"
#include "decoration.hpp"

static bool force_software_rendering = false;

bool force_analyze = false;

float w_exponent = 0.0;
float w_scale = 1.0;

float arc_length_animation_speed = 3.0f;
float arc_length_direction = -1;  // toggle between +1 and -1; -1 actually offsets what appears to be "forward"

static int last_time = 0;

static void advanceArcLengthOffset()
{
    int this_time = glutGet(GLUT_ELAPSED_TIME);
    if (last_time != 0) {
        int delta_time = this_time - last_time;
        float delta_time_ms = delta_time/1000.0;
        // Scaling by texture scale allows textures of different aspect ratios to scroll at the same speed.
        float speed = arc_length_animation_speed * getCurrentTextureScale();
        float shift = speed * delta_time_ms;
        arc_length_offset += shift * arc_length_direction;
    }
    last_time = this_time;
}

static void idle()
{
    advanceArcLengthOffset();
    postMainWindowRedisplay();
}

// Callback function for tessellateCubic method.
static void genPoint(int rib_index, const float2 p[2], float t, void* handle)
{
    // Ignore handle.
    assert(handle == NULL);
    printf("%4d: t=%g, Pn = %g,%g -- Pp = %g,%g\n", rib_index, t,
        p[0][0], p[0][1], p[1][0], p[1][1]);
}

static void loadStrokerWithPaint(CPUStrokeTessellator& stroker, const Paint& paint)
{
    stroker.setStrokeWidth(paint.StrokeWidth());
    stroker.setStrokeRadiusFraction(paint.ArcLengthExtension() * paint.StrokeWidth() * 0.5f);
    stroker.setMaxJoinSteps(StrokeTessellator::JoinEnumToMaxJoinSteps(paint.StrokeTessellatorJoinStyle()));
    stroker.setEffectiveMiterLimit(paint.EffectiveMiterLimit());
    stroker.setCapMask(paint.StrokeTessellatorCapMask());
}

static void testStroking()
{
    // Setup stroke tessellator settings
    StrokeTessellatorSettings settings;
    settings.disableMinWindowStrokeRadiusClamp();
    settings.setMaxTurningAngleDelta(stroke_tessellator_settings.MaxTurningAngleDelta());

    // Setup CPU stroke tessellator context
    CPUStrokeTessellator stroker(settings);
    stroker.setStrokeSpace(CPUStrokeTessellator::OBJECT_SPACE);
    const float2 vs = float2(1, 1); // XXX
    const float3x3 identity = float3x3(1,0,0, 0,1,0, 0,0,1);
    stroker.setTransforms(identity, identity, identity, vs);

    loadStrokerWithPaint(stroker, current_paint);

    // "Serpentine Curve" from CurveConfig.cpp
    static const float2 serpentine[4] = {
        float2(489.0625, 21.25),
        float2(212.1875, 19.583333333333314),
        float2(471.5625, 368.75),
        float2(222.8125, 379.58333333333331)
    };

    stroker.tessellateCubicOffsets(genPoint, NULL, serpentine);
    if (verbose) {
        stroker.print(stdout);
    }

    static const float2 loop[4] = {
        float2(200,100),
        float2(650,200),
        float2(50,200),
        float2(400,100)
    };

    stroker.tessellateCubicOffsets(genPoint, NULL, loop);
    if (verbose) {
        stroker.print(stdout);
    }

    static const float2 parabola[4] = {
        float2(489.0625, 21.25),
        float2(212.1875, 19.583333333333314),
        float2(471.5625, 368.75)
    };

    stroker.tessellateQuadraticOffsets(genPoint, NULL, parabola);
    if (verbose) {
        stroker.print(stdout);
    }

    static const float2 conic[3] = {
        float2(487,333),
        float2(210,20),
        float2(470.788,361.421)
    };
    static float conic_w = 2;

    stroker.tessellateConicOffsets(genPoint, NULL, conic, conic_w);
    if (verbose) {
        stroker.print(stdout);
    }
}

void updateAnimation()
{
    setMainWindow(); // Only the main window animates.
    if (animating) {
        last_time = glutGet(GLUT_ELAPSED_TIME);
        glutIdleFunc(idle);
        invalidateFPS();
        enableFPS();
    } else {
        glutIdleFunc(NULL);
        disableFPS();
        glutPostRedisplay();
    }
}

void special(int key, int x, int y)
{
    const int modifiers = glutGetModifiers() & (GLUT_ACTIVE_CTRL|GLUT_ACTIVE_SHIFT|GLUT_ACTIVE_ALT);
    switch (key) {
    case GLUT_KEY_F1:
        force_analyze = true;
        break;
    case GLUT_KEY_F3:
        {
            extern int do_decoration;
            do_decoration = !do_decoration;
            printf("do_decoration = %d\n", do_decoration);
        }
        break;
    case GLUT_KEY_F4:
        printf("reset stroking buffer\n");
        cpu_path_count = 0;
        resetArcLengthBuffers();
        break;
    case GLUT_KEY_F9:
        toggleAnnotations();
        return;
    case GLUT_KEY_F10:
        testStroking();
        break;
    case GLUT_KEY_INSERT:
        if (color_space == UNCORRECTED) {
            color_space = CORRECTED_SRGB;
        } else {
            assert(color_space == CORRECTED_SRGB);
            color_space = UNCORRECTED;
        }
        printf("color_space = %s\n",
            color_space == CORRECTED_SRGB ? "corrected sRGB" : "uncorrected");
        break;
    case GLUT_KEY_HOME:
        if (modifiers & GLUT_ACTIVE_CTRL) {
            initModelMatrix();
        } else {
            initModelAndViewMatrices();
        }
        break;
    case GLUT_KEY_END:
        dumpCorners();
        break;
    case GLUT_KEY_PAGE_DOWN:
        if (modifiers & GLUT_ACTIVE_CTRL) {
            draw_ref_t = !draw_ref_t;
            printf("draw_ref_t = %d\n", draw_ref_t);
            break;
        }
        if (modifiers & GLUT_ACTIVE_ALT) {
            current_paint.decreaseArcLengthExtension(0.125*0.25);
        } else {
            current_paint.decreaseArcLengthExtension(0.125);
        }
        if (1||verbose) {
            printf("arc_length_extension = %g\n", current_paint.ArcLengthExtension());
        }
        break;
    case GLUT_KEY_PAGE_UP:
        if (modifiers & GLUT_ACTIVE_ALT) {
            current_paint.increaseArcLengthExtension(0.125*0.25);
        } else {
            current_paint.increaseArcLengthExtension(0.125);
        }
        if (1||verbose) {
            printf("arc_length_extension = %g\n", current_paint.ArcLengthExtension());
        }
        break;
    case GLUT_KEY_UP:
        if (modifiers & GLUT_ACTIVE_CTRL) {
            w_exponent += 0.1;
            w_scale = exp(w_exponent);
            curveTypeSelect = true;
            printf("w_bias = %g (w_exponent = %g)\n", w_scale, w_exponent);
            break;

        }
        arc_length_animation_speed *= 1.125;
        printf("arc_length_animation_speed = %f\n", arc_length_animation_speed);
        break;
    case GLUT_KEY_DOWN:
        if (modifiers & GLUT_ACTIVE_CTRL) {
            w_exponent -= 0.1;
            w_scale = exp(w_exponent);
            curveTypeSelect = true;
            printf("w_bias = %g (w_exponent = %g)\n", w_scale, w_exponent);
            break;
        }
        arc_length_animation_speed /= 1.125;
        printf("arc_length_animation_speed = %f\n", arc_length_animation_speed);
        break;
    case GLUT_KEY_LEFT:
        if (modifiers & GLUT_ACTIVE_CTRL) {
            ref_t -= 0.1;
        } else if (modifiers & GLUT_ACTIVE_ALT) {
            ref_t -= 0.001;
        } else {
            ref_t -= 0.01;
        }
        printf("ref_t = %g\n", ref_t);
        break;
    case GLUT_KEY_RIGHT:
        if (modifiers & GLUT_ACTIVE_CTRL) {
            ref_t += 0.1;
        } else if (modifiers & GLUT_ACTIVE_ALT) {
            ref_t += 0.001;
        } else {
            ref_t += 0.01;
        }
        printf("ref_t = %g\n", ref_t);
        break;
    default:
        // ignore other keys
        return;
    }
    postAllWindowsRedisplay();
}

void keyboard(unsigned char c, int x, int y)
{
    const int modifiers = glutGetModifiers();
    switch (c) {
    case '\t':
        {
            extern int second_draw;
            second_draw = !second_draw;
        }
        break;
    case 27:  /* Esc quits */
        exit(0);
        return;
    case '[':
        current_paint.decreaseMiterLimit(0.1f);
        updatePathListMiterLimit(current_paint.MiterLimit());
        break;
    case ']':
        current_paint.increaseMiterLimit(0.1f);
        updatePathListMiterLimit(current_paint.MiterLimit());
        break;
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
        {
            if (modifiers & GLUT_ACTIVE_ALT) {
                c += 9;
            }
            size_t ndx = size_t(c-'1');
            if (ndx < path_list.size()) {
                path_list[ndx]->toggleShowMe();
            }
        }
        break;
    case '#':
        {
            use_native_atan2 = !use_native_atan2;
            printf("use_native_atan2 = %d\n", use_native_atan2);
        }
        break;
    case '0':
        initModelMatrix();
        break;
    case 'W':
        stroke_tessellator_settings.toggleMinWindowStrokeRadiusClamp();
        printf("min_window_stroke_radius_clamp = %d (min_window_stroke_radius = %g)\n",
            stroke_tessellator_settings.MinWindowStrokeRadiusClamp(),
            stroke_tessellator_settings.MinWindowStrokeRadius());
        break;
    case '<':
    case '>':
        {
            const float scale = (c == '>') ? 2.0f : 0.5f;
            stroke_tessellator_settings.scaleMinWindowStrokeRadius(scale);
            printf("min_window_stroke_radius = %g%s\n",
                stroke_tessellator_settings.MinWindowStrokeRadius(),
                !stroke_tessellator_settings.MinWindowStrokeRadiusClamp() ? " (OFF)" : "");
        }
        break;
    case 'T':
        printf("Resetting shaders (for lazy reload)...\n");
        invalidateSimplePathShaders();
        invalidateFPS();
        break;
    case 'R':
        reprocessPaths(true,true);  // Make cap and join links
        break;
    case 'E':
        reprocessPaths(false,false);  // Don't make cap or join links (just for performance evaluation)
        break;
    case 3: // Ctrl-C
        disableCaps();
        break;
    case '(':
        if (modifiers & GLUT_ACTIVE_ALT) {
            current_dash_offset -= 5;
        } else {
            current_dash_offset -= 0.5;
        }
        simple.process(true, true);
        break;
    case ')':
        if (modifiers & GLUT_ACTIVE_ALT) {
            current_dash_offset += 5;
        } else {
            current_dash_offset += 0.5;
        }
        simple.process(true, true);
        break;
    case 10:  // Ctrl-J
        stroke_tessellator_settings.cycleInsideRoundJoinMode();
        printf("inside join mode = %s\n", stroke_tessellator_settings.InsideRoundJoinModeString());
        break;
    case 'J':
        {
            Paint::JoinEnum join_style = current_paint.JoinStyle();
            join_style = Paint::JoinEnum(int(join_style)+1);
            if (join_style == Paint::JOIN_ENUM_COUNT) {
                join_style = Paint::JoinEnum(0);
            }
            current_paint.setJoinStyle(join_style);
            updatePathListJoinStyle(currentGLJoinStyle());
        }
        break;
    case '+':
        current_paint.increaseStrokeWidth(0.125);
        if (verbose) {
            printf("stroke_width = %g\n", current_paint.StrokeWidth());
        }
        updatePathListStrokeWidth(current_paint.StrokeWidth());
        break;
    case '=':
        if (modifiers & GLUT_ACTIVE_ALT) {
            current_paint.increaseStrokeWidth(10);
        } else {
            current_paint.increaseStrokeWidth(1);
        }
        if (verbose) {
            printf("stroke_width = %g\n", current_paint.StrokeWidth());
        }
        updatePathListStrokeWidth(current_paint.StrokeWidth());
        break;
    case '_':
        current_paint.decreaseStrokeWidth(0.125f);
        if (verbose) {
            printf("stroke_width = %g\n", current_paint.StrokeWidth());
        }
        updatePathListStrokeWidth(current_paint.StrokeWidth());
        break;
    case '-':
        if (modifiers & GLUT_ACTIVE_ALT) {
            current_paint.decreaseStrokeWidth(10);
        } else {
            current_paint.decreaseStrokeWidth(1);
        }
        if (verbose) {
            printf("stroke_width = %g\n", current_paint.StrokeWidth());
        }
        updatePathListStrokeWidth(current_paint.StrokeWidth());
        break;
    case ',':
        curveType--;
        if (curveType < 0) {
            curveType = num_configs-1;
        }
        printf("Forward: %s\n", config_list[curveType].description);
        config_menu(curveType);
        break;
    case '.':
        curveType++;
        if (curveType >= num_configs) {
            curveType = 0;
        }
        printf("Backward: %s\n", config_list[curveType].description);
        config_menu(curveType);
        break;
    case '/':
        config_menu(curveType);
        printf("Reload: %s\n", config_list[curveType].description);
        break;
    case 'f':
        colorFPS(0,1,0);
        toggleFPS();
        break;
    case 'F':
        draw_filled = !draw_filled;
        break;
    case 'v':
        frame_sync = !frame_sync;
        requestSynchornizedSwapBuffers(frame_sync);
        break;
    case '\\':
        arc_length_offset = 0;
        break;
    case '|':
        arc_length_direction = -arc_length_direction;
        printf("arc_length_direction = %f\n", arc_length_direction);
        break;
    case ' ':
        if (modifiers & GLUT_ACTIVE_ALT) {
            arc_length_offset += arc_length_direction * 0.05;
            break;
        }
        animating = !animating;
        show_status_text = !animating;
        updateAnimation();
        return;
    case 'r':
        use_NV_path_rendering = false;
#if SUPPORT_POLAR_VISUALIZE
        stroke_tessellator_settings.setParameterization(StrokeSettingTypes::POLAR);
        stroke_tessellator_settings.setExtraSteps(0);
#endif
        initModelAndViewMatrices();
        break;
    case 13:  /* Enter redisplays */
        printf("redraw win=%d...\n", glutGetWindow());
        glutPostRedisplay();
        return;
    case 'C':
        stroke_tessellator_settings.toggleTrivialCulling();
        printf("trivial_culling = %d\n", stroke_tessellator_settings.TrivialCulling());
        invalidateFPS();
        break;
    case 'c':
        if (modifiers & GLUT_ACTIVE_ALT) {
            color_by_segment_length = !color_by_segment_length;
            printf("color_by_segment_length = %d\n", color_by_segment_length);
        } else {
            cycleCompositeMode();
        }
        break;
    case 'b':
        background = (background+1)%4;
        setBackground();
        printf("background = %s\n", backgroundString());
        break;
    case 'q':
        simple_opacity += 0.0625;
        if (simple_opacity > 1) {
            simple_opacity = 1;
        }
        printf("opacity = %g\n", simple_opacity);
        simple.setOpacity(simple_opacity);
        break;
    case 'Q':
        simple_opacity -= 0.0625;
        if (simple_opacity > 1) {
            simple_opacity = 1;
        }
        printf("simple_opacity = %g\n", simple_opacity);
        simple.setOpacity(simple_opacity);
        break;
    case 'B':
        simple_color_ndx = (simple_color_ndx+1)%4;
        setSimpleColor();
        break;
#if SUPPORT_POLAR_VISUALIZE
    case 's':
        stroke_tessellator_settings.toggleParameterization();
        invalidateFPS();
        use_NV_path_rendering = false;  // Side-effect: force of NV_path_rendering stroking ('p' toggles this otherwise).
        break;
    case 'j':
        stroke_tessellator_settings.toggleShowJoins();
        printf("show_joins = %d\n", stroke_tessellator_settings.ShowJoins());
        break;
#endif
    case 'S':
        use_supersample = !use_supersample;
        setSupersampledMainWindow(use_supersample);
        invalidateFPS();
        printf("use_supersample = %d\n", use_supersample);
        break;
    case 'i':
        if (modifiers & GLUT_ACTIVE_ALT) {
            dump_buffers = !dump_buffers;
            printf("dump_buffers = %d\n", dump_buffers);
            return;
        }
        break;
    case 'a':
#if SUPPORT_POLAR_VISUALIZE
        if (stroke_tessellator_settings.isUniformStroking()){
            unsigned int uniform_segments = stroke_tessellator_settings.UniformSegments();
            uniform_segments++;
            stroke_tessellator_settings.setUniformSegments(uniform_segments);
        } else 
#endif
        {
            stroke_tessellator_settings.scaleMaxTurningAngleDelta(0.8);
        }
        break;
    case 'z':
#if SUPPORT_POLAR_VISUALIZE
        if (stroke_tessellator_settings.isUniformStroking()) {
            unsigned int uniform_segments = stroke_tessellator_settings.UniformSegments();
            if (uniform_segments>1){
                uniform_segments--;
                stroke_tessellator_settings.setUniformSegments(uniform_segments);
            }
        } else 
#endif
        {
            // Is updated angle still below 90 degrees?
            if (stroke_tessellator_settings.MaxTurningAngleDelta() * 1.25 < M_PI_2) {
                stroke_tessellator_settings.scaleMaxTurningAngleDelta(1.25);
            } else {
                printf("Saturaging max_turning_angle_delta (%g degrees) to less than 90 degrees\n",
                    float(degrees(stroke_tessellator_settings.MaxTurningAngleDelta())));
            }
        }
        break;
    case 'X':
        force_software_rendering = !force_software_rendering;
        printf("force_software_rendering = %d\n", force_software_rendering);
#define GL_FORCE_SOFTWARE_NV 0x6007
        if (force_software_rendering) {
            glEnable(GL_FORCE_SOFTWARE_NV);
        } else {
            glDisable(GL_FORCE_SOFTWARE_NV);
        }
        break;
    case 'o':
        if (modifiers & GLUT_ACTIVE_ALT) {
            if (overlay_outline) {
                overlay_outline = false;
            } else {
                overlay_outline = true;
            }
            printf("overlay_outline = %d\n", overlay_outline);
            break;
        }
        if (outline) {
            outline = false;
        } else {
            outline = true;
        }
        break;
    case 'O':
        view_conservative_rasterization = !view_conservative_rasterization;
        printf("view_conservative_rasterization = %d\n", view_conservative_rasterization);
        break;
    case 'V':
        verbose = !verbose;
        printf("verbose = %d\n", verbose);
        break;
    case 'N':
        native_quadratics = !native_quadratics;
        printf("native_quadratics = %d\n", native_quadratics);
        config_menu(curveType);  // reload curve since might have quadratics
        break;
    case 'n':
        toggleHodographWindow();
        break;
    case 'm':
        if (modifiers & GLUT_ACTIVE_ALT) {
            toggleFPSunits();
            return;
        }
        toggleParametricGraphWindow();
        break;
    case 'l':
        setShadingMode(StrokeConfigTypes::ARC_LENGTH_TEXTURE_2D);
        return;
    case 'L':
        cycleShadingMode();
        break;
    case 'P':
        advanceTexture();
        break;
    case 'p':
        use_NV_path_rendering = !use_NV_path_rendering;
        invalidateFPS();
        break;
    case 'M':
        toggleNormalExtensions();
        break;
    case '`':
        updateSpline();
        break;
    default:
        return;
    }
    postAllWindowsRedisplay();
}
