/// \file
/// Very small intro stub.

//######################################
// Include #############################
//######################################

#include "dnload.h"

#if defined(USE_LD)
#include "glsl_program.hpp"
#include "image_png.hpp"
#include <cstdio>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <boost/exception/diagnostic_information.hpp>
#include <boost/filesystem.hpp>
#include <boost/program_options.hpp>
#include <boost/scoped_array.hpp>
#include <boost/tuple/tuple.hpp>
namespace fs = boost::filesystem;
namespace po = boost::program_options;
#endif

#if !defined(USE_LD)
#include "synth.h"
#endif

//######################################
// Define ##############################
//######################################

#if !defined(DISPLAY_MODE)
/// Screen mode.
///
/// Negative values windowed.
/// Positive values fullscreen.
#define DISPLAY_MODE -720
#endif

/// \cond
#if (0 > (DISPLAY_MODE))
#define SCREEN_F 0
#define SCREEN_H (-(DISPLAY_MODE))
#elif (0 < (DISPLAY_MODE))
#define SCREEN_F 1
#define SCREEN_H (DISPLAY_MODE)
#else
#error "invalid display mode (pre)"
#endif
#if ((800 == SCREEN_H) || (1200 == SCREEN_H))
#define SCREEN_W ((SCREEN_H / 10) * 16)
#else
#define SCREEN_W (((SCREEN_H * 16) / 9) - (((SCREEN_H * 16) / 9) % 4))
#endif
/// \endcond

/// Size of one sample in bytes.
#define AUDIO_SAMPLE_SIZE 4

/// \cond
#if (4 == AUDIO_SAMPLE_SIZE)
#define AUDIO_SAMPLE_TYPE_SDL AUDIO_F32SYS
typedef float sample_t;
#elif (2 == AUDIO_SAMPLE_SIZE)
#define AUDIO_SAMPLE_TYPE_SDL AUDIO_S16SYS
typedef int16_t sample_t;
#elif (1 == AUDIO_SAMPLE_SIZE)
#define AUDIO_SAMPLE_TYPE_SDL AUDIO_U8
typedef uint8_t sample_t;
#else
#error "invalid audio sample size"
#endif
#define AUDIO_POSITION_SHIFT (9 - (4 / sizeof(sample_t)))
/// \endcond

/// Audio channels.
#define AUDIO_CHANNELS 2

/// Audio samplerate.
#define AUDIO_SAMPLERATE 44100

/// Audio byterate.
#define AUDIO_BYTERATE (AUDIO_CHANNELS * AUDIO_SAMPLERATE * AUDIO_SAMPLE_SIZE)

/// Intro length (in bytes of audio).
#define INTRO_LENGTH (93 * AUDIO_BYTERATE)

/// Intro start position (in seconds).
#define INTRO_START (0 * AUDIO_BYTERATE)

/// Precalculated file to load if synth cannot be ran.
#define SYNTH_TEST_INPUT_FILE "region_de_magallanes.wav"
#if 0 // Change to 1 to enable writing synth output.
/// Output file to write for synth test.
#define SYNTH_TEST_OUTPUT_FILE SYNTH_TEST_INPUT_FILE
#endif

/// Noise texture size (one side).
#define NOISE_SIDE 512
/// Noise texture size (elements).
#define NOISE_SIZE (NOISE_SIDE * NOISE_SIDE)
/// Noise volume size (one side).
#define VOLUME_SIDE 64
/// Noise volume size (elements).
#define VOLUME_SIZE (VOLUME_SIDE * VOLUME_SIDE * VOLUME_SIDE * 3)
/// Randomization size.
#define RAND_ARRAY_SIZE NOISE_SIZE
/// \cond
#if VOLUME_SIZE > NOISE_SIZE
#undef RAND_ARRAY_SIZE
#define RAND_ARRAY_SIZE VOLUME_SIZE
#endif
/// \endcond

/// \cond
#define STARTING_POS_X 0.0f
#define STARTING_POS_Y 5.0f
#define STARTING_POS_Z 2.0f
#define STARTING_FW_X 0.0f
#define STARTING_FW_Y 0.0f
#define STARTING_FW_Z -1.0f
#define STARTING_UP_X 0.0f
#define STARTING_UP_Y 1.0f
#define STARTING_UP_Z 0.0f
/// \endcond

//######################################
// Global data #########################
//######################################

/// "Enough". Could add exact definition in generated synth.h, but this gets us started
#if !defined(SYNTH_WORK_SIZE)
#define SYNTH_WORK_SIZE (16 * 1024 * 1024)
#endif

/// Audio buffer length (should be larger than intro length for safety).
#define AUDIO_BUFFER_LENGTH (INTRO_LENGTH * 9 / 8)

/// Audio buffer for output and work.
static uint8_t g_audio_buffer[SYNTH_WORK_SIZE + AUDIO_BUFFER_LENGTH];

/// Audio output buffer.
static uint8_t* g_audio_buffer_song = g_audio_buffer + SYNTH_WORK_SIZE;

/// Current audio position.
static int g_audio_position = INTRO_START;

#if defined(USE_LD)

/// \cond
static float g_pos_x = STARTING_POS_X;
static float g_pos_y = STARTING_POS_Y;
static float g_pos_z = STARTING_POS_Z;
static float g_fw_x = STARTING_FW_X;
static float g_fw_y = STARTING_FW_Y;
static float g_fw_z = STARTING_FW_Z;
static float g_up_x = STARTING_UP_X;
static float g_up_y = STARTING_UP_Y;
static float g_up_z = STARTING_UP_Z;
/// \endcond

/// Developer mode global toggle.
static bool g_flag_developer = false;

static const char *usage = ""
"Usage: region_de_magallanes <options>\n"
"For Assembly 2019 combined 4k intro compo.\n"
"Release version does not pertain to any size limitations.\n";

#else

/// Developer mode disabled.
#define g_flag_developer 0

#endif

//######################################
// Global functions ####################
//######################################

/// Global SDL window storage.
SDL_Window *g_sdl_window;

/// Swap buffers.
///
/// Uses global data.
static void swap_buffers()
{
  dnload_SDL_GL_SwapWindow(g_sdl_window);
}

/// Tear down initialized systems.
///
/// Uses global data.
static void teardown()
{
  dnload_SDL_Quit();
}

//######################################
// Random ##############################
//######################################

/// Seed for glibc random
static int32_t g_lgc_rand_seed = 1559687923/*123456789*/;

/// Simple rand() to avoid loading libc.
///
/// Linear Congruential Generator. See:
/// https://en.wikipedia.org/wiki/Linear_congruential_generator
///
/// \return next random number.
static int16_t lgc_rand()
{
    // Use constants from some old glibc implementation.
    const int32_t LGC_A = 1103515245;
    const int32_t LGC_C = 12345;

    g_lgc_rand_seed = LGC_A * g_lgc_rand_seed + LGC_C;

    return static_cast<int16_t>(g_lgc_rand_seed >> 16);
}

#if defined(USE_LD)

/// Set the random seed.
///
/// \param op New random seed.
static void lgc_srand(int32_t op)
{
    g_lgc_rand_seed = op;
}

/// Random float value.
///
/// \param op Given maximum value.
/// \return Random value between 0 and given value.
static float frand(float op)
{
    return static_cast<float>(lgc_rand() & 0xFFFF) * ((1.0f / 65535.0f) * op);
}

#endif

//######################################
// Music ###############################
//######################################

/// \brief Update audio stream.
///
/// \param userdata Not used.
/// \param stream Target stream.
/// \param len Number of bytes to write.
static void audio_callback(void *userdata, Uint8 *stream, int len)
{
    (void)userdata;

    for(int ii = 0; (ii < len); ++ii)
    {
        stream[ii] = g_audio_buffer_song[g_audio_position + ii];
    }
    g_audio_position += len;
}

/// SDL audio specification struct.
static SDL_AudioSpec audio_spec =
{
    AUDIO_SAMPLERATE,
    AUDIO_SAMPLE_TYPE_SDL,
    AUDIO_CHANNELS,
    0,
#if defined(USE_LD)
    4096,
#else
    256, // ~172.3Hz, lower values seem to cause underruns
#endif
    0,
    0,
    audio_callback,
    NULL
};

/// Synth wrapper.
/// \param outbuf Output buffer.
/// \param samples Number of bytes to write.
static void synth_func(unsigned bytes)
{
#if defined(USE_LD)
#if defined(SYNTH_TEST_INPUT_FILE)
  const fs::path REL_PATH("rel");
  const fs::path SYNTH_TEST_INPUT_FILE_PATH(SYNTH_TEST_INPUT_FILE);
  SDL_AudioSpec wav_spec;
  SDL_AudioSpec* wav_found;
  Uint32 wav_length;
  Uint8 *wav_buffer;

  wav_found = SDL_LoadWAV(SYNTH_TEST_INPUT_FILE_PATH.string().c_str(), &wav_spec, &wav_buffer, &wav_length);
  if(!wav_found)
  {
    fs::path fname = REL_PATH / SYNTH_TEST_INPUT_FILE_PATH;
    wav_found = SDL_LoadWAV(fname.string().c_str(), &wav_spec, &wav_buffer, &wav_length);
  }
  if(!wav_found)
  {
    fs::path fname = fs::path("..") / REL_PATH / SYNTH_TEST_INPUT_FILE_PATH;
    wav_found = SDL_LoadWAV(fname.string().c_str(), &wav_spec, &wav_buffer, &wav_length);
  }

  // If successful, read floating-point data from file.
  if(wav_found)
  {
      size_t copy_length = std::min(static_cast<size_t>(wav_length), static_cast<size_t>(AUDIO_BUFFER_LENGTH));
      memcpy(g_audio_buffer_song, wav_buffer, copy_length);
      SDL_FreeWAV(wav_buffer);
  }
  // No no audio data available, fill data with 0.
  else
#endif
  {
    std::cerr << "WARNING: synth not available, filling audio with 0" << std::endl;
    while(bytes > 0)
    {
      g_audio_buffer_song[bytes - 1] = 0;
      --bytes;
    }
  }

#else
  synth(reinterpret_cast<float*>(g_audio_buffer_song), bytes / sizeof(sample_t));

  // Write raw .wav file if necessary.
#if defined(SYNTH_TEST_OUTPUT_FILE)
  {
    SF_INFO info;
    info.samplerate = AUDIO_SAMPLERATE;
    info.channels = AUDIO_CHANNELS;
    info.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT;

    SNDFILE *outfile = dnload_sf_open(SYNTH_TEST_OUTPUT_FILE, SFM_WRITE, &info);
    sf_count_t write_count = INTRO_LENGTH / AUDIO_CHANNELS / AUDIO_SAMPLE_SIZE;
    dnload_sf_writef_float(outfile, reinterpret_cast<float*>(g_audio_buffer_song), write_count);
    dnload_sf_close(outfile);
  }
#endif
#endif
}

//######################################
// Graphics data #######################
//######################################

#include "header.glsl.hpp"
#include "quad.frag.glsl.hpp"
#include "post.frag.glsl.hpp"

/// Fixed uniform location.
static const GLint g_uniform_n2 = 0;
/// Fixed uniform location.
static const GLint g_uniform_n3 = 1;
/// Fixed uniform location.
static const GLint g_uniform_fc = 2;
/// Fixed uniform location.
static const GLint g_uniform_t = 3;
#if defined(USE_LD)
/// Fixed uniform location.
static const GLint g_uniform_u = 5;
/// Fixed uniform location.
static const GLint g_uniform_r = 9;
#endif

/// GL info arrays.
/// 1 Framebuffer.
/// 3 Textures.
/// 2 Pipelines.
/// 2 Programs.
static GLuint g_gl_array[1 + 3 + 2 + 2];

/// Framebuffer.
static GLuint& g_fbo = g_gl_array[0];

/// 2D-noise.
static GLuint& g_texture_noise2 = g_gl_array[1];
/// 3D-noise.
static GLuint& g_texture_noise3 = g_gl_array[2];
/// Color framebuffer texture.
static GLuint& g_texture_color = g_gl_array[3];

/// Quad pipeline.
static GLuint& g_pipeline_quad = g_gl_array[4];
/// Post pipeline.
static GLuint& g_pipeline_post = g_gl_array[5];

/// Quad program.
static GLuint& g_program_quad = g_gl_array[6];
/// Post program.
static GLuint& g_program_post = g_gl_array[7];

/// Texture index.
static const int g_texture_index_noise2 = 0;
/// Texture index.
static const int g_texture_index_noise3 = 1;
/// Texture index.
static const int g_texture_index_color = 2;

//######################################
// Graphics functions ##################
//######################################

#if defined(USE_LD)

/// \brief Uniforms.
///
/// 0: X position.
/// 1: Y position.
/// 2: Z position.
/// 3: X forward.
/// 4: Y forward.
/// 5: Z forward.
/// 6: X up.
/// 7: Y up.
/// 8: Z up.
/// 9: Direction follow flag.
/// 10: Unused.
/// 11: Unused.
static float g_uniform_array[12] =
{
    STARTING_POS_X, STARTING_POS_Y, STARTING_POS_Z,
    STARTING_FW_X, STARTING_FW_Y, STARTING_FW_Z,
    STARTING_UP_X, STARTING_UP_Y, STARTING_UP_Z,
    0.0, 0.0, 0.0,
};

/// Matrix uniform array.
///
/// Used as the rotation matrix if not in release mode.
static float g_uniform_matrix[9] =
{
    //-0.99f, -0.16f, 0.02f, 0.14f, -0.77f, 0.63f, -0.08f, 0.62f, 0.78f
    //-0.83f, 0.52f, 0.21f, -0.55f, -0.68f, -0.47f, -0.10f, -0.51f, 0.85f
    0.94f, -0.27f, 0.20f, -0.18f, -0.90f, -0.41f, 0.29f, 0.35f, -0.89f
};

#endif

/// \brief Draw the world.
///
/// \param ticks Tick count.
/// \param aspec Screen aspect.
static void draw(int ticks)
{
#if defined(USE_LD)
    if(g_flag_developer)
    {
        g_uniform_array[0] = g_pos_x;
        g_uniform_array[1] = g_pos_y;
        g_uniform_array[2] = g_pos_z;
        g_uniform_array[3] = g_fw_x;
        g_uniform_array[4] = g_fw_y;
        g_uniform_array[5] = g_fw_z;
        g_uniform_array[6] = g_up_x;
        g_uniform_array[7] = g_up_y;
        g_uniform_array[8] = g_up_z;
    }
#endif

#if defined(USE_LD)
    glProgramUniform3fv(g_program_quad, g_uniform_u, 4, g_uniform_array);
    glProgramUniformMatrix3fv(g_program_quad, g_uniform_r, 1, GL_FALSE, g_uniform_matrix);
#endif

    dnload_glProgramUniform1i(g_program_quad, g_uniform_n2, g_texture_index_noise2);
    dnload_glProgramUniform1i(g_program_quad, g_uniform_n3, g_texture_index_noise3);
    dnload_glProgramUniform1i(g_program_post, g_uniform_fc, g_texture_index_color);
    dnload_glProgramUniform1i(g_program_quad, g_uniform_t, ticks);

    dnload_glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
    dnload_glBindProgramPipeline(g_pipeline_quad);
    dnload_glRects(-1, -1, 1, 1);

    dnload_glBindFramebuffer(GL_FRAMEBUFFER, 0);
    dnload_glBindProgramPipeline(g_pipeline_post);
    dnload_glRects(-1, -1, 1, 1);
}

//######################################
// Utility #############################
//######################################

#if defined(USE_LD)

/// Generate a ZXY rotation matrix.
///
/// \param rx Rotation x.
/// \param ry Rotation y.
/// \param rz Rotation z.
/// \param out_matrix Matrix to write into.
static void generate_rotation_matrix_zxy(float rx, float ry, float rz, float *out_matrix)
{
    float sx = sinf(rx);
    float sy = sinf(ry);
    float sz = sinf(rz);
    float cx = cosf(rx);
    float cy = cosf(ry);
    float cz = cosf(rz);

    out_matrix[0] = sx * sy * sz + cy * cz;
    out_matrix[1] = sz * cx;
    out_matrix[2] = sx * sz * cy - sy * cz;
    out_matrix[3] = sx * sy * cz - sz * cy;
    out_matrix[4] = cx * cz;
    out_matrix[5] = sx * cy * cz + sy * sz;
    out_matrix[6] = sy * cx;
    out_matrix[7] = -sx;
    out_matrix[8] = cx * cy;
}

/// Parse resolution from string input.
///
/// \param op Resolution string.
/// \return Tuple of width and height.
boost::tuple<unsigned, unsigned> parse_resolution(const std::string &op)
{
    size_t cx = op.find("x");

    if(std::string::npos == cx)
    {
        cx = op.rfind("p");

        if((std::string::npos == cx) || (0 >= cx))
        {
            std::ostringstream sstr;
            sstr << "invalid resolution string '" << op << '\'';
            BOOST_THROW_EXCEPTION(std::runtime_error(sstr.str()));
        }

        std::string sh = op.substr(0, cx);

        unsigned rh = boost::lexical_cast<unsigned>(sh);
        unsigned rw = (rh * 16) / 9;
        unsigned rem4 = rw % 4;

        return boost::make_tuple(rw - rem4, rh);
    }

    std::string sw = op.substr(0, cx);
    std::string sh = op.substr(cx + 1);

    return boost::make_tuple(boost::lexical_cast<int>(sw), boost::lexical_cast<int>(sh));
}

/// \brief Image writing callback.
///
/// \param screen_w Screen width.
/// \param screen_h Screen height.
/// \param idx Frame index to write.
void write_frame(unsigned screen_w, unsigned screen_h, unsigned idx)
{
    boost::scoped_array<uint8_t> image(new uint8_t[screen_w * screen_h * 3]);
    std::ostringstream sstr;

    glReadPixels(0, 0, static_cast<GLsizei>(screen_w), static_cast<GLsizei>(screen_h), GL_RGB, GL_UNSIGNED_BYTE,
            image.get());

    sstr << "intro_" << std::setfill('0') << std::setw(4) << idx << ".png";

    gfx::image_png_save(sstr.str(), screen_w, screen_h, 24, image.get());
    return;
}

/// Update window position.
///
/// May be NOP depending on platform.
void update_window_position()
{
    static int window_x = INT_MIN;
    static int window_y = INT_MIN;
    static int window_width = INT_MIN;
    static int window_height = INT_MIN;
    int current_window_x;
    int current_window_y;
    int current_window_width;
    int current_window_height;

    SDL_GetWindowPosition(g_sdl_window, &current_window_x, &current_window_y);
    SDL_GetWindowSize(g_sdl_window, &current_window_width, &current_window_height);
    if((current_window_x != window_x) || (current_window_y != window_y) ||
            (current_window_width != window_width) || (current_window_height != window_height))
    {
        window_x = current_window_x;
        window_y = current_window_y;
        window_width = current_window_width;
        window_height = current_window_height;
    }
}

#endif

//######################################
// intro / _start ######################
//######################################

#if defined(USE_LD)
/// \brief Intro body function.
///
/// \param screen_w Screen width.
/// \param screen_h Screen height.
/// \param flag_fullscreen Fullscreen toggle.
/// \param flag_record Record toggle.
void intro(unsigned screen_w, unsigned screen_h, bool flag_fullscreen, bool flag_record)
#else
#define screen_w static_cast<unsigned>(SCREEN_W)
#define screen_h static_cast<unsigned>(SCREEN_H)
#define flag_fullscreen static_cast<bool>(SCREEN_F)
void _start()
#endif
{
    dnload();
    dnload_SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    g_sdl_window = dnload_SDL_CreateWindow(NULL, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
            static_cast<int>(screen_w), static_cast<int>(screen_h),
            SDL_WINDOW_OPENGL | (flag_fullscreen ? SDL_WINDOW_FULLSCREEN : 0));
    dnload_SDL_GL_CreateContext(g_sdl_window);
    dnload_SDL_ShowCursor(g_flag_developer);

#if defined(USE_LD)
    {
        GLenum err = glewInit();
        if(GLEW_OK != err)
        {
            std::cerr << "glewInit(): " << glewGetErrorString(err) << std::endl;
            teardown();
            exit(1);
        }
    }
    if(!flag_fullscreen)
    {
        update_window_position();
    }
#endif

    // Run synth. This will either load a wave file or run the actual synth.
    synth_func(INTRO_LENGTH);

    // Create pipelines, overshoot by textures and FBO.
    dnload_glGenProgramPipelines(1 + 3 + 2, g_gl_array);
    // Create textures, overshoot by FBO.
    dnload_glGenTextures(1 + 3, g_gl_array);
    // Create FBO.
    dnload_glCreateFramebuffers(1, g_gl_array);

    // Generate textures.
    {
        uint16_t* noise_array = reinterpret_cast<uint16_t*>(g_audio_buffer);

        for(size_t ii = 0; (ii < RAND_ARRAY_SIZE); ++ii)
        {
            noise_array[ii] = static_cast<uint16_t>(lgc_rand());
        }

        // 2D noise.
        dnload_glActiveTexture(GL_TEXTURE0 + g_texture_index_noise2);
        dnload_glBindTexture(GL_TEXTURE_2D, g_texture_noise2);
        dnload_glTexImage2D(GL_TEXTURE_2D, 0, GL_R16, NOISE_SIDE, NOISE_SIDE, 0, GL_RED, GL_UNSIGNED_SHORT,
                noise_array);
        dnload_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
#if defined(USE_LD)
        dnload_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        dnload_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        dnload_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
#endif
        dnload_glGenerateMipmap(GL_TEXTURE_2D);

        // 3d noise.
        dnload_glActiveTexture(GL_TEXTURE0 + g_texture_index_noise3);
        dnload_glBindTexture(GL_TEXTURE_3D, g_texture_noise3);
        dnload_glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB16_SNORM, VOLUME_SIDE, VOLUME_SIDE, VOLUME_SIDE, 0, GL_RGB,
                GL_SHORT, noise_array);
        dnload_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
#if defined(USE_LD)
        dnload_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        dnload_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        dnload_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_REPEAT);
        dnload_glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
#endif
        dnload_glGenerateMipmap(GL_TEXTURE_3D);

        // Color framebuffer texture.
        dnload_glActiveTexture(GL_TEXTURE0 + g_texture_index_color);
        dnload_glBindTexture(GL_TEXTURE_2D, g_texture_color);
        dnload_glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast<GLsizei>(screen_w),
                static_cast<GLsizei>(screen_h), 0, GL_RGBA, GL_FLOAT, NULL);
        dnload_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        dnload_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        dnload_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
#if defined(USE_LD)
        dnload_glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
#endif

        // Framebuffer.
        dnload_glNamedFramebufferTexture(g_fbo, GL_COLOR_ATTACHMENT0, g_texture_color, 0);
    }

#if defined(USE_LD)
    GlslProgram program_quad;
    GlslProgram program_post;
    program_quad.addShader(GL_FRAGMENT_SHADER, g_shader_header, g_shader_fragment_quad);
    program_post.addShader(GL_FRAGMENT_SHADER, g_shader_header, g_shader_fragment_post);
    if(!program_quad.link(true) || !program_post.link(true))
    {
        BOOST_THROW_EXCEPTION(std::runtime_error("pipeline creation failure"));
    }
    g_pipeline_quad = program_quad.getPipelineId();
    g_pipeline_post = program_post.getPipelineId();
    g_program_quad = program_quad.getPipelineId(GL_FRAGMENT_SHADER);
    g_program_post = program_post.getPipelineId(GL_FRAGMENT_SHADER);
#else
    const char* shader_array[] = { g_shader_header, g_shader_fragment_quad };
    g_program_quad = dnload_glCreateShaderProgramv(GL_FRAGMENT_SHADER, 2, shader_array);
    shader_array[1] = g_shader_fragment_post;
    g_program_post = dnload_glCreateShaderProgramv(GL_FRAGMENT_SHADER, 2, shader_array);
    dnload_glUseProgramStages(g_pipeline_quad, 2, g_program_quad);
    dnload_glUseProgramStages(g_pipeline_post, 2, g_program_post);
#endif

#if defined(USE_LD)
    // Recording.
    if(flag_record)
    {
        SDL_Event event;
        unsigned frame_idx = 0;

        // video
        for(;;)
        {
            int ticks = static_cast<int>(static_cast<double>(frame_idx) / 60.0 * static_cast<double>(AUDIO_BYTERATE));

            if(ticks >= INTRO_LENGTH)
            {
                break;
            }

            if(SDL_PollEvent(&event) && (event.type == SDL_KEYDOWN) && (event.key.keysym.sym == SDLK_ESCAPE))
            {
                break;
            }

            draw(ticks >> 12);
            write_frame(screen_w, screen_h, frame_idx);
            swap_buffers();
            ++frame_idx;
        }

        teardown();
        return;
    }
#endif

    // Audio.
    dnload_SDL_OpenAudio(&audio_spec, NULL);
#if defined(USE_LD)
    if(!g_flag_developer)
    {
#endif
        dnload_SDL_PauseAudio(0);
#if defined(USE_LD)
    }
#endif

#if defined(USE_LD)
    uint32_t reference_ticks = SDL_GetTicks();
#endif

    for(;;)
    {
#if defined(USE_LD)
        static double current_time = 0.0;
        static float move_speed = 1.0f / 60.0f;
        static int time_speed = 1;
        static uint8_t mouse_look = 0;
        static int8_t move_backward = 0;
        static int8_t move_down = 0;
        static int8_t move_forward = 0;
        static int8_t move_left = 0;
        static int8_t move_right = 0;
        static int8_t move_up = 0;
        static int8_t time_delta = 0;
        static bool playback = false;
        int mouse_look_x = 0;
        int mouse_look_y = 0;
        bool reset_audio_position = false;
        bool quit = false;
#endif
        SDL_Event event;
        int curr_ticks;

#if defined(USE_LD)
        while(SDL_PollEvent(&event))
        {
            if(SDL_QUIT == event.type)
            {
                quit = true;
            }
            else if(SDL_KEYDOWN == event.type)
            {
                switch(event.key.keysym.sym)
                {
                case SDLK_a:
                    move_left = 1;
                    break;

                case SDLK_d:
                    move_right = 1;
                    break;

                case SDLK_e:
                    move_up = 1;
                    break;

                case SDLK_q:
                    move_down = 1;
                    break;

                case SDLK_s:
                    move_backward = 1;
                    break;

                case SDLK_w:
                    move_forward = 1;
                    break;

                case SDLK_p:
                    playback = !playback;
                    reset_audio_position = true;
                    break;

                case SDLK_LEFT:
                    current_time -= static_cast<double>(AUDIO_BYTERATE * 5);
                    reset_audio_position = true;
                    break;
                case SDLK_RIGHT:
                    current_time += static_cast<double>(AUDIO_BYTERATE * 5);
                    reset_audio_position = true;
                    break;
                case SDLK_UP:
                    current_time += static_cast<double>(AUDIO_BYTERATE * 30);
                    reset_audio_position = true;
                    break;
                case SDLK_DOWN:
                    current_time -= static_cast<double>(AUDIO_BYTERATE * 30);
                    reset_audio_position = true;
                    break;

                case SDLK_LSHIFT:
                case SDLK_RSHIFT:
                    move_speed = 1.0f / 5.0f;
                    time_speed = 8;
                    break;

                case SDLK_LALT:
                    time_delta = -1;
                    break;

                case SDLK_MODE:
                case SDLK_RALT:
                    time_delta = 1;
                    break;

                case SDLK_INSERT:
                    {
                        lgc_srand(static_cast<int32_t>(time(0)) ^ static_cast<int32_t>(SDL_GetTicks()));

                        float rx = frand(static_cast<float>(M_PI * 2.0));
                        float ry = frand(static_cast<float>(M_PI * 2.0));
                        float rz = frand(static_cast<float>(M_PI * 2.0));

                        generate_rotation_matrix_zxy(rx, ry, rz, g_uniform_matrix);

                        printf("mat3 rot = mat3(%1.2f, %1.2f, %1.2f, %1.2f, %1.2f, %1.2f, %1.2f, %1.2f, %1.2f); // %1.2f ; %1.2f ; %1.2f\n",
                                g_uniform_matrix[0], g_uniform_matrix[3], g_uniform_matrix[6],
                                g_uniform_matrix[1], g_uniform_matrix[4], g_uniform_matrix[7],
                                g_uniform_matrix[2], g_uniform_matrix[5], g_uniform_matrix[8],
                                rx, ry, rz);
                    }
                    break;

                case SDLK_DELETE:
                    {
                        uint16_t* noise_array = reinterpret_cast<uint16_t*>(g_audio_buffer);

                        lgc_srand(static_cast<int32_t>(time(0)) ^ static_cast<int32_t>(SDL_GetTicks()));
                        printf("noise seed: %i\n", g_lgc_rand_seed);

                        for(size_t ii = 0; (ii < RAND_ARRAY_SIZE); ++ii)
                        {
                            noise_array[ii] = static_cast<uint16_t>(lgc_rand());
                        }

                        // 2D noise.
                        glActiveTexture(GL_TEXTURE0 + g_texture_index_noise2);
                        glBindTexture(GL_TEXTURE_2D, g_texture_noise2);
                        glTexImage2D(GL_TEXTURE_2D, 0, GL_R16, NOISE_SIDE, NOISE_SIDE, 0, GL_RED, GL_UNSIGNED_SHORT,
                                noise_array);
                        glGenerateMipmap(GL_TEXTURE_2D);

                        // 3d noise.
                        glActiveTexture(GL_TEXTURE0 + g_texture_index_noise3);
                        glBindTexture(GL_TEXTURE_3D, g_texture_noise3);
                        glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB16_SNORM, VOLUME_SIDE, VOLUME_SIDE, VOLUME_SIDE, 0,
                                GL_RGB, GL_SHORT, noise_array);
                        dnload_glGenerateMipmap(GL_TEXTURE_3D);
                    }
                    break;

                case SDLK_F5:
                    if(!program_quad.link(true) || !program_post.link(true))
                    {
                        BOOST_THROW_EXCEPTION(std::runtime_error("pipeline recreation failure"));
                    }
                    g_pipeline_quad = program_quad.getPipelineId();
                    g_pipeline_post = program_post.getPipelineId();
                    g_program_quad = program_quad.getPipelineId(GL_FRAGMENT_SHADER);
                    g_program_post = program_post.getPipelineId(GL_FRAGMENT_SHADER);
                    break;

                case SDLK_SPACE:
                    {
                        double time_seconds = current_time / static_cast<double>(AUDIO_BYTERATE);
                        printf("position: %1.2f ; %1.2f ; %1.2f\nforward: %1.2f ; %1.2f ; %1.2f\nup: %1.2f ; %1.2f ; %1.2f\ntime: %i.%i ;; %i\n",
                                g_pos_x, g_pos_y, g_pos_z, g_fw_x, g_fw_y, g_fw_z, g_up_x, g_up_y, g_up_z,
                                static_cast<int>(time_seconds), static_cast<int>(time_seconds * 100.0f) % 100,
                                static_cast<int>(current_time) >> 12);
                    }
                    break;

                case SDLK_RETURN:
                    g_uniform_array[9] = 0.0f; // Enable direction follow.
                    break;

                case SDLK_ESCAPE:
                    quit = true;
                    break;

                default:
                    break;
                }
            }
            else if(SDL_KEYUP == event.type)
            {
                switch(event.key.keysym.sym)
                {
                case SDLK_a:
                    move_left = 0;
                    break;

                case SDLK_d:
                    move_right = 0;
                    break;

                case SDLK_e:
                    move_up = 0;
                    break;

                case SDLK_q:
                    move_down = 0;
                    break;

                case SDLK_s:
                    move_backward = 0;
                    break;

                case SDLK_w:
                    move_forward = 0;
                    break;

                case SDLK_LSHIFT:
                case SDLK_RSHIFT:
                    move_speed = 1.0f / 60.0f;
                    time_speed = 1;
                    break;

                case SDLK_MODE:
                case SDLK_LALT:
                case SDLK_RALT:
                    time_delta = 0;
                    break;

                default:
                    break;
                }
            }
            else if(SDL_MOUSEBUTTONDOWN == event.type)
            {
                if(1 == event.button.button)
                {
                    mouse_look = 1;
                    g_uniform_array[9] = 1.0f; // Disable direction follow.
                }
            }
            else if(SDL_MOUSEBUTTONUP == event.type)
            {
                if(1 == event.button.button)
                {
                    mouse_look = 0;
                }
            }
            else if(SDL_MOUSEMOTION == event.type)
            {
                if(0 != mouse_look)
                {
                    mouse_look_x += event.motion.xrel;
                    mouse_look_y += event.motion.yrel;
                }
            }
            else if(SDL_WINDOWEVENT == event.type)
            {
                if(!flag_fullscreen)
                {
                    update_window_position();
                }
            }
        }

        if(g_flag_developer)
        {
            float uplen = sqrtf(g_up_x * g_up_x + g_up_y * g_up_y + g_up_z * g_up_z);
            float fwlen = sqrtf(g_fw_x * g_fw_x + g_fw_y * g_fw_y + g_fw_z * g_fw_z);
            float rt_x;
            float rt_y;
            float rt_z;
            float movement_rt = static_cast<float>(move_right - move_left) * move_speed;
            float movement_up = static_cast<float>(move_up - move_down) * move_speed;
            float movement_fw = static_cast<float>(move_forward - move_backward) * move_speed;

            g_up_x /= uplen;
            g_up_y /= uplen;
            g_up_z /= uplen;

            g_fw_x /= fwlen;
            g_fw_y /= fwlen;
            g_fw_z /= fwlen;

            rt_x = g_fw_y * g_up_z - g_fw_z * g_up_y;
            rt_y = g_fw_z * g_up_x - g_fw_x * g_up_z;
            rt_z = g_fw_x * g_up_y - g_fw_y * g_up_x;

            if(0 != mouse_look_x)
            {
                float angle = static_cast<float>(mouse_look_x) / static_cast<float>(screen_h / 4) * 0.25f;
                float ca = cosf(angle);
                float sa = sinf(angle);
                float new_rt_x = ca * rt_x + sa * g_fw_x;
                float new_rt_y = ca * rt_y + sa * g_fw_y;
                float new_rt_z = ca * rt_z + sa * g_fw_z;
                float new_fw_x = ca * g_fw_x - sa * rt_x;
                float new_fw_y = ca * g_fw_y - sa * rt_y;
                float new_fw_z = ca * g_fw_z - sa * rt_z;

                rt_x = new_rt_x;
                rt_y = new_rt_y;
                rt_z = new_rt_z;
                g_fw_x = new_fw_x;
                g_fw_y = new_fw_y;
                g_fw_z = new_fw_z;
            }
            if(0 != mouse_look_y)
            {
                float angle = static_cast<float>(mouse_look_y) / static_cast<float>(screen_h / 4) * 0.25f;
                float ca = cosf(angle);
                float sa = sinf(angle);
                float new_fw_x = ca * g_fw_x + sa * g_up_x;
                float new_fw_y = ca * g_fw_y + sa * g_up_y;
                float new_fw_z = ca * g_fw_z + sa * g_up_z;
                float new_up_x = ca * g_up_x - sa * g_fw_x;
                float new_up_y = ca * g_up_y - sa * g_fw_y;
                float new_up_z = ca * g_up_z - sa * g_fw_z;

                g_fw_x = new_fw_x;
                g_fw_y = new_fw_y;
                g_fw_z = new_fw_z;
                g_up_x = new_up_x;
                g_up_y = new_up_y;
                g_up_z = new_up_z;
            }

            g_pos_x += movement_rt * rt_x + movement_up * g_up_x + movement_fw * g_fw_x;
            g_pos_y += movement_rt * rt_y + movement_up * g_up_y + movement_fw * g_fw_y;
            g_pos_z += movement_rt * rt_z + movement_up * g_up_z + movement_fw * g_fw_z;
        }

        if(g_flag_developer)
        {
            uint32_t frame_ticks = SDL_GetTicks();
            int diff_ticks = static_cast<int>(frame_ticks - reference_ticks);
            int time_multiplier = playback ? 1 : (time_delta * time_speed);
            reference_ticks = frame_ticks;

            current_time += static_cast<double>(time_multiplier * diff_ticks * AUDIO_BYTERATE) / 1000.0;
            current_time = std::max(current_time, 0.0);

            curr_ticks = static_cast<int>(current_time);

            // Developer mode cannot exit.
            if(curr_ticks >= static_cast<int>(INTRO_LENGTH))
            {
                curr_ticks = INTRO_LENGTH - 1;
                current_time = static_cast<double>(curr_ticks);
                playback = false;
                SDL_PauseAudio(1);
            }
            else if(reset_audio_position)
            {
                SDL_PauseAudio(1);
                g_audio_position = static_cast<int>(current_time);
                g_audio_position -= g_audio_position & (AUDIO_CHANNELS * AUDIO_SAMPLE_SIZE - 1);
                SDL_PauseAudio(!playback);
            }
        }
        else
        {
            float seconds_elapsed = static_cast<float>(SDL_GetTicks() - reference_ticks) / 1000.0f;

            curr_ticks = static_cast<int>(seconds_elapsed * static_cast<float>(AUDIO_BYTERATE)) + INTRO_START;
        }

        if((curr_ticks >= static_cast<int>(INTRO_LENGTH)) || quit)
        {
            break;
        }
#else
        curr_ticks = g_audio_position;

        dnload_SDL_PollEvent(&event);

        if((curr_ticks >= INTRO_LENGTH) || (event.type == SDL_KEYDOWN))
        {
            break;
        }
#endif

        draw(curr_ticks >> 12);
        swap_buffers();
    }

    teardown();
#if !defined(USE_LD)
    asm_exit();
#endif
}

//######################################
// Main ################################
//######################################

#if defined(USE_LD)
/// Main function.
///
/// \param argc Argument count.
/// \param argv Arguments.
/// \return Program return code.
int DNLOAD_MAIN(int argc, char **argv)
{
    unsigned screen_w = SCREEN_W;
    unsigned screen_h = SCREEN_H;
    bool fullscreen = true;
    bool record = false;

    try
    {
        if(argc > 0)
        {
            po::options_description desc("Options");
            desc.add_options()
                ("developer,d", "Developer mode.")
                ("help,h", "Print help text.")
                ("record,R", "Do not play intro normally, instead save audio as .wav and frames as .png -files.")
                ("resolution,r", po::value<std::string>(), "Resolution to use, specify as 'WIDTHxHEIGHT' or 'HEIGHTp'.")
                ("window,w", "Start in window instead of full-screen.");

            po::variables_map vmap;
            po::store(po::command_line_parser(argc, argv).options(desc).run(), vmap);
            po::notify(vmap);

            if(vmap.count("developer"))
            {
                g_flag_developer = true;
            }
            if(vmap.count("help"))
            {
                std::cout << usage << desc << std::endl;
                return 0;
            }
            if(vmap.count("record"))
            {
                record = true;
            }
            if(vmap.count("resolution"))
            {
                boost::tie(screen_w, screen_h) = parse_resolution(vmap["resolution"].as<std::string>());
            }
            if(vmap.count("window"))
            {
                fullscreen = false;
            }
        }

        intro(screen_w, screen_h, fullscreen, record);
    }
    catch(const boost::exception &err)
    {
        std::cerr << boost::diagnostic_information(err);
        return 1;
    }
    catch(const std::exception &err)
    {
        std::cerr << err.what() << std::endl;
        return 1;
    }
    catch(...)
    {
        std::cerr << __FILE__ << ": unknown exception caught\n";
        return -1;
    }
    return 0;
}
#endif

//######################################
// End #################################
//######################################
