/*
 * This program source code file is part of KICAD, a free EDA CAD application.
 *
 * Copyright (C) 2016-2021 Kicad Developers, see AUTHORS.txt for contributors.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you may find one here:
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * or you may search the http://www.gnu.org website for the version 2 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include <gal/opengl/antialiasing.h>
#include <gal/opengl/opengl_compositor.h>
#include <gal/opengl/utils.h>
#include <gal/color4d.h>

#include <memory>
#include <tuple>

#include "gl_builtin_shaders.h"
#include "SmaaAreaTex.h"
#include "SmaaSearchTex.h"

using namespace KIGFX;

// =========================
// ANTIALIASING_NONE
// =========================

ANTIALIASING_NONE::ANTIALIASING_NONE( OPENGL_COMPOSITOR* aCompositor ) :
    compositor( aCompositor )
{
}


bool ANTIALIASING_NONE::Init()
{
    // Nothing to initialize
    return true;
}


VECTOR2U ANTIALIASING_NONE::GetInternalBufferSize()
{
    return compositor->GetScreenSize();
}


void ANTIALIASING_NONE::DrawBuffer( GLuint buffer )
{
    compositor->DrawBuffer( buffer, OPENGL_COMPOSITOR::DIRECT_RENDERING );
}


void ANTIALIASING_NONE::Present()
{
    // Nothing to present, draw_buffer already drew to the screen
}


void ANTIALIASING_NONE::OnLostBuffers()
{
    // Nothing to do
}


void ANTIALIASING_NONE::Begin()
{
    // Nothing to do
}


unsigned int ANTIALIASING_NONE::CreateBuffer()
{
    return compositor->CreateBuffer( compositor->GetScreenSize() );
}


namespace
{
void draw_fullscreen_primitive()
{
    glMatrixMode( GL_MODELVIEW );
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode( GL_PROJECTION );
    glPushMatrix();
    glLoadIdentity();


    glBegin( GL_TRIANGLES );
    glTexCoord2f( 0.0f, 1.0f );
    glVertex2f( -1.0f, 1.0f );
    glTexCoord2f( 0.0f, 0.0f );
    glVertex2f( -1.0f, -1.0f );
    glTexCoord2f( 1.0f, 1.0f );
    glVertex2f( 1.0f, 1.0f );

    glTexCoord2f( 1.0f, 1.0f );
    glVertex2f( 1.0f, 1.0f );
    glTexCoord2f( 0.0f, 0.0f );
    glVertex2f( -1.0f, -1.0f );
    glTexCoord2f( 1.0f, 0.0f );
    glVertex2f( 1.0f, -1.0f );
    glEnd();

    glPopMatrix();
    glMatrixMode( GL_MODELVIEW );
    glPopMatrix();
}

} // namespace

// =========================
// ANTIALIASING_SUPERSAMPLING
// =========================

ANTIALIASING_SUPERSAMPLING::ANTIALIASING_SUPERSAMPLING( OPENGL_COMPOSITOR* aCompositor ) :
        compositor( aCompositor ),
        ssaaMainBuffer( 0 ), areBuffersCreated( false ), areShadersCreated( false )
{
}


bool ANTIALIASING_SUPERSAMPLING::Init()
{
    areShadersCreated = false;

    if( !areBuffersCreated )
    {
        ssaaMainBuffer = compositor->CreateBuffer();
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

        areBuffersCreated = true;
    }

    return true;
}


VECTOR2U ANTIALIASING_SUPERSAMPLING::GetInternalBufferSize()
{
    return compositor->GetScreenSize() * 2;
}


void ANTIALIASING_SUPERSAMPLING::Begin()
{
    compositor->SetBuffer( ssaaMainBuffer );
    compositor->ClearBuffer( COLOR4D::BLACK );
}


void ANTIALIASING_SUPERSAMPLING::DrawBuffer( GLuint aBuffer )
{
    compositor->DrawBuffer( aBuffer, ssaaMainBuffer );
}


void ANTIALIASING_SUPERSAMPLING::Present()
{
    glDisable( GL_BLEND );
    glDisable( GL_DEPTH_TEST );
    glActiveTexture( GL_TEXTURE0 );
    glBindTexture( GL_TEXTURE_2D, compositor->GetBufferTexture( ssaaMainBuffer ) );
    compositor->SetBuffer( OPENGL_COMPOSITOR::DIRECT_RENDERING );

    draw_fullscreen_primitive();
}


void ANTIALIASING_SUPERSAMPLING::OnLostBuffers()
{
    areBuffersCreated = false;
}


unsigned int ANTIALIASING_SUPERSAMPLING::CreateBuffer()
{
    return compositor->CreateBuffer( GetInternalBufferSize() );
}

// ===============================
// ANTIALIASING_SMAA
// ===============================

ANTIALIASING_SMAA::ANTIALIASING_SMAA( OPENGL_COMPOSITOR* aCompositor ) :
        areBuffersInitialized( false ),
        shadersLoaded( false ),
        compositor( aCompositor )
{
    smaaBaseBuffer = 0;
    smaaEdgesBuffer = 0;
    smaaBlendBuffer = 0;
    smaaAreaTex = 0;
    smaaSearchTex = 0;

    pass_1_metrics = 0;
    pass_2_metrics = 0;
    pass_3_metrics = 0;
}


VECTOR2U ANTIALIASING_SMAA::GetInternalBufferSize()
{
    return compositor->GetScreenSize();
}


void ANTIALIASING_SMAA::loadShaders()
{
    // Load constant textures
    glEnable( GL_TEXTURE_2D );
    glActiveTexture( GL_TEXTURE0 );

    glGenTextures( 1, &smaaAreaTex );
    glBindTexture( GL_TEXTURE_2D, smaaAreaTex );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RG8, AREATEX_WIDTH, AREATEX_HEIGHT, 0, GL_RG,
                  GL_UNSIGNED_BYTE, areaTexBytes );
    checkGlError( "loading smaa area tex", __FILE__, __LINE__ );

    glGenTextures( 1, &smaaSearchTex );
    glBindTexture( GL_TEXTURE_2D, smaaSearchTex );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexImage2D( GL_TEXTURE_2D, 0, GL_R8, SEARCHTEX_WIDTH, SEARCHTEX_HEIGHT, 0, GL_RED,
                  GL_UNSIGNED_BYTE, searchTexBytes );
    checkGlError( "loading smaa search tex", __FILE__, __LINE__ );

    // Quality settings:
    // THRESHOLD: intended to exclude spurious edges in photorealistic game graphics
    //            but in a high-contrast CAD application, all edges are intentional
    //            should be set fairly low, so user color choices do not affect antialiasing
    // MAX_SEARCH_STEPS: steps of 2px, searched in H/V direction to discover true angle of edges
    //                   improves AA for lines close H/V but creates fuzzyness at junctions
    // MAX_SEARCH_STEPS_DIAG: steps of 1px, searched in diagonal direction
    //                        improves lines close to 45deg but turns small circles into octagons
    // CORNER_ROUNDING: SMAA can distinguish actual corners from aliasing jaggies,
    //                  we want to preserve those as much as possible
    // Edge Detection: In Eeschema, when a single pixel line changes color, edge detection using
    //                 color is too aggressive and leads to a white spot at the transition point
    std::string quality_string;
    const char* edge_detect_shader;

    // trades imperfect AA of shallow angles for a near artifact-free reproduction of fine features
    // jaggies are smoothed over max 5px (original step + 2px in both directions)
    quality_string = "#define SMAA_THRESHOLD 0.005\n"
                     "#define SMAA_MAX_SEARCH_STEPS 1\n"
                     "#define SMAA_MAX_SEARCH_STEPS_DIAG 2\n"
                     "#define SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR 1.5\n"
                     "#define SMAA_CORNER_ROUNDING 0\n";
    edge_detect_shader = BUILTIN_SHADERS::smaa_pass_1_fragment_shader_luma;

    // set up shaders
    std::string vert_preamble( R"SHADER(
#version 120
#define SMAA_GLSL_2_1
#define SMAA_INCLUDE_VS 1
#define SMAA_INCLUDE_PS 0
uniform vec4 SMAA_RT_METRICS;
)SHADER" );

    std::string frag_preamble( R"SHADER(
#version 120
#define SMAA_GLSL_2_1
#define SMAA_INCLUDE_VS 0
#define SMAA_INCLUDE_PS 1
uniform vec4 SMAA_RT_METRICS;
)SHADER" );

    std::string smaa_source = std::string( BUILTIN_SHADERS::smaa_base_shader_p1 )
                              + std::string( BUILTIN_SHADERS::smaa_base_shader_p2 )
                              + std::string( BUILTIN_SHADERS::smaa_base_shader_p3 )
                              + std::string( BUILTIN_SHADERS::smaa_base_shader_p4 );

    //
    // Set up pass 1 Shader
    //
    pass_1_shader = std::make_unique<SHADER>();
    pass_1_shader->LoadShaderFromStrings( KIGFX::SHADER_TYPE_VERTEX, vert_preamble, quality_string,
                                          smaa_source, BUILTIN_SHADERS::smaa_pass_1_vertex_shader );
    pass_1_shader->LoadShaderFromStrings( KIGFX::SHADER_TYPE_FRAGMENT, frag_preamble,
                                          quality_string, smaa_source, edge_detect_shader );
    pass_1_shader->Link();
    checkGlError( "linking pass 1 shader", __FILE__, __LINE__ );

    GLint smaaColorTexParameter = pass_1_shader->AddParameter( "colorTex" );
    checkGlError( "pass1: getting colorTex uniform", __FILE__, __LINE__ );
    pass_1_metrics = pass_1_shader->AddParameter( "SMAA_RT_METRICS" );
    checkGlError( "pass1: getting metrics uniform", __FILE__, __LINE__ );

    pass_1_shader->Use();
    checkGlError( "pass1: using shader", __FILE__, __LINE__ );
    pass_1_shader->SetParameter( smaaColorTexParameter, 0 );
    checkGlError( "pass1: setting colorTex uniform", __FILE__, __LINE__ );
    pass_1_shader->Deactivate();
    checkGlError( "pass1: deactivating shader", __FILE__, __LINE__ );

    //
    // set up pass 2 shader
    //
    pass_2_shader = std::make_unique<SHADER>();
    pass_2_shader->LoadShaderFromStrings( KIGFX::SHADER_TYPE_VERTEX, vert_preamble, quality_string,
                                          smaa_source, BUILTIN_SHADERS::smaa_pass_2_vertex_shader );
    pass_2_shader->LoadShaderFromStrings( KIGFX::SHADER_TYPE_FRAGMENT, frag_preamble,
                                          quality_string, smaa_source,
                                          BUILTIN_SHADERS::smaa_pass_2_fragment_shader );
    pass_2_shader->Link();
    checkGlError( "linking pass 2 shader", __FILE__, __LINE__ );

    GLint smaaEdgesTexParameter = pass_2_shader->AddParameter( "edgesTex" );
    checkGlError( "pass2: getting colorTex uniform", __FILE__, __LINE__ );
    GLint smaaAreaTexParameter = pass_2_shader->AddParameter( "areaTex" );
    checkGlError( "pass2: getting areaTex uniform", __FILE__, __LINE__ );
    GLint smaaSearchTexParameter = pass_2_shader->AddParameter( "searchTex" );
    checkGlError( "pass2: getting searchTex uniform", __FILE__, __LINE__ );
    pass_2_metrics = pass_2_shader->AddParameter( "SMAA_RT_METRICS" );
    checkGlError( "pass2: getting metrics uniform", __FILE__, __LINE__ );

    pass_2_shader->Use();
    checkGlError( "pass2: using shader", __FILE__, __LINE__ );
    pass_2_shader->SetParameter( smaaEdgesTexParameter, 0 );
    checkGlError( "pass2: setting colorTex uniform", __FILE__, __LINE__ );
    pass_2_shader->SetParameter( smaaAreaTexParameter, 1 );
    checkGlError( "pass2: setting areaTex uniform", __FILE__, __LINE__ );
    pass_2_shader->SetParameter( smaaSearchTexParameter, 3 );
    checkGlError( "pass2: setting searchTex uniform", __FILE__, __LINE__ );
    pass_2_shader->Deactivate();
    checkGlError( "pass2: deactivating shader", __FILE__, __LINE__ );

    //
    // set up pass 3 shader
    //
    pass_3_shader = std::make_unique<SHADER>();
    pass_3_shader->LoadShaderFromStrings( KIGFX::SHADER_TYPE_VERTEX, vert_preamble, quality_string,
                                          smaa_source, BUILTIN_SHADERS::smaa_pass_3_vertex_shader );
    pass_3_shader->LoadShaderFromStrings( KIGFX::SHADER_TYPE_FRAGMENT, frag_preamble,
                                          quality_string, smaa_source,
                                          BUILTIN_SHADERS::smaa_pass_3_fragment_shader );
    pass_3_shader->Link();

    GLint smaaP3ColorTexParameter = pass_3_shader->AddParameter( "colorTex" );
    checkGlError( "pass3: getting colorTex uniform", __FILE__, __LINE__ );
    GLint smaaBlendTexParameter = pass_3_shader->AddParameter( "blendTex" );
    checkGlError( "pass3: getting blendTex uniform", __FILE__, __LINE__ );
    pass_3_metrics = pass_3_shader->AddParameter( "SMAA_RT_METRICS" );
    checkGlError( "pass3: getting metrics uniform", __FILE__, __LINE__ );

    pass_3_shader->Use();
    checkGlError( "pass3: using shader", __FILE__, __LINE__ );
    pass_3_shader->SetParameter( smaaP3ColorTexParameter, 0 );
    checkGlError( "pass3: setting colorTex uniform", __FILE__, __LINE__ );
    pass_3_shader->SetParameter( smaaBlendTexParameter, 1 );
    checkGlError( "pass3: setting blendTex uniform", __FILE__, __LINE__ );
    pass_3_shader->Deactivate();
    checkGlError( "pass3: deactivating shader", __FILE__, __LINE__ );

    shadersLoaded = true;
}


void ANTIALIASING_SMAA::updateUniforms()
{
    auto dims = compositor->GetScreenSize();

    pass_1_shader->Use();
    checkGlError( "pass1: using shader", __FILE__, __LINE__ );
    pass_1_shader->SetParameter( pass_1_metrics, 1.f / float( dims.x ), 1.f / float( dims.y ),
                                 float( dims.x ), float( dims.y ) );
    checkGlError( "pass1: setting metrics uniform", __FILE__, __LINE__ );
    pass_1_shader->Deactivate();
    checkGlError( "pass1: deactivating shader", __FILE__, __LINE__ );

    pass_2_shader->Use();
    checkGlError( "pass2: using shader", __FILE__, __LINE__ );
    pass_2_shader->SetParameter( pass_2_metrics, 1.f / float( dims.x ), 1.f / float( dims.y ),
                                 float( dims.x ), float( dims.y ) );
    checkGlError( "pass2: setting metrics uniform", __FILE__, __LINE__ );
    pass_2_shader->Deactivate();
    checkGlError( "pass2: deactivating shader", __FILE__, __LINE__ );

    pass_3_shader->Use();
    checkGlError( "pass3: using shader", __FILE__, __LINE__ );
    pass_3_shader->SetParameter( pass_3_metrics, 1.f / float( dims.x ), 1.f / float( dims.y ),
                                 float( dims.x ), float( dims.y ) );
    checkGlError( "pass3: setting metrics uniform", __FILE__, __LINE__ );
    pass_3_shader->Deactivate();
    checkGlError( "pass3: deactivating shader", __FILE__, __LINE__ );
}


bool ANTIALIASING_SMAA::Init()
{
    if( !shadersLoaded )
        loadShaders();

    if( !areBuffersInitialized )
    {
        smaaBaseBuffer = compositor->CreateBuffer();
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

        smaaEdgesBuffer = compositor->CreateBuffer();
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

        smaaBlendBuffer = compositor->CreateBuffer();
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );

        updateUniforms();
        areBuffersInitialized = true;
    }

    // Nothing to initialize
    return true;
}


void ANTIALIASING_SMAA::OnLostBuffers()
{
    areBuffersInitialized = false;
}


unsigned int ANTIALIASING_SMAA::CreateBuffer()
{
    return compositor->CreateBuffer( compositor->GetScreenSize() );
}


void ANTIALIASING_SMAA::DrawBuffer( GLuint buffer )
{
    // draw to internal buffer
    compositor->DrawBuffer( buffer, smaaBaseBuffer );
}


void ANTIALIASING_SMAA::Begin()
{
    compositor->SetBuffer( smaaBaseBuffer );
    compositor->ClearBuffer( COLOR4D::BLACK );
}


namespace
{
void draw_fullscreen_triangle()
{
    glMatrixMode( GL_MODELVIEW );
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode( GL_PROJECTION );
    glPushMatrix();
    glLoadIdentity();

    glBegin( GL_TRIANGLES );
    glTexCoord2f( 0.0f, 1.0f );
    glVertex2f( -1.0f, 1.0f );
    glTexCoord2f( 0.0f, -1.0f );
    glVertex2f( -1.0f, -3.0f );
    glTexCoord2f( 2.0f, 1.0f );
    glVertex2f( 3.0f, 1.0f );
    glEnd();

    glPopMatrix();
    glMatrixMode( GL_MODELVIEW );
    glPopMatrix();
}
} // namespace


void ANTIALIASING_SMAA::Present()
{
    auto sourceTexture = compositor->GetBufferTexture( smaaBaseBuffer );

    glDisable( GL_BLEND );
    glDisable( GL_DEPTH_TEST );
    glEnable( GL_TEXTURE_2D );

    //
    // pass 1: main-buffer -> smaaEdgesBuffer
    //
    compositor->SetBuffer( smaaEdgesBuffer );
    compositor->ClearBuffer( COLOR4D::BLACK );

    glActiveTexture( GL_TEXTURE0 );
    glBindTexture( GL_TEXTURE_2D, sourceTexture );
    checkGlError( "binding colorTex", __FILE__, __LINE__ );
    pass_1_shader->Use();
    checkGlError( "using smaa pass 1 shader", __FILE__, __LINE__ );
    draw_fullscreen_triangle();
    pass_1_shader->Deactivate();

    //
    // pass 2: smaaEdgesBuffer -> smaaBlendBuffer
    //
    compositor->SetBuffer( smaaBlendBuffer );
    compositor->ClearBuffer( COLOR4D::BLACK );

    auto edgesTex = compositor->GetBufferTexture( smaaEdgesBuffer );

    glActiveTexture( GL_TEXTURE0 );
    glBindTexture( GL_TEXTURE_2D, edgesTex );
    glActiveTexture( GL_TEXTURE1 );
    glBindTexture( GL_TEXTURE_2D, smaaAreaTex );
    glActiveTexture( GL_TEXTURE3 );
    glBindTexture( GL_TEXTURE_2D, smaaSearchTex );

    pass_2_shader->Use();
    draw_fullscreen_triangle();
    pass_2_shader->Deactivate();

    //
    // pass 3: colorTex + BlendBuffer -> output
    //
    compositor->SetBuffer( OPENGL_COMPOSITOR::DIRECT_RENDERING );
    compositor->ClearBuffer( COLOR4D::BLACK );
    auto blendTex = compositor->GetBufferTexture( smaaBlendBuffer );

    glActiveTexture( GL_TEXTURE0 );
    glBindTexture( GL_TEXTURE_2D, sourceTexture );
    glActiveTexture( GL_TEXTURE1 );
    glBindTexture( GL_TEXTURE_2D, blendTex );

    pass_3_shader->Use();
    draw_fullscreen_triangle();
    pass_3_shader->Deactivate();
}
