/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2015-2016 Mario Luzeiro <mrluzeiro@ua.pt>
 * Copyright (C) 2015-2020 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
 */

 /**
 * @file image.cpp
 * @brief one 8bit-channel image implementation.
 */

#include "image.h"
#include "buffers_debug.h"
#include <cstring> // For memcpy

#include <algorithm>
#include <atomic>
#include <thread>
#include <chrono>


#ifndef CLAMP
#define CLAMP( n, min, max ) {if( n < min ) n=min; else if( n > max ) n = max;}
#endif


IMAGE::IMAGE( unsigned int aXsize, unsigned int aYsize )
{
    m_wxh    = aXsize * aYsize;
    m_pixels = new unsigned char[m_wxh];
    memset( m_pixels, 0, m_wxh );
    m_width   = aXsize;
    m_height  = aYsize;
    m_wraping = IMAGE_WRAP::CLAMP;
}


IMAGE::IMAGE( const IMAGE& aSrcImage )
{
    m_wxh    = aSrcImage.GetWidth() * aSrcImage.GetHeight();
    m_pixels = new unsigned char[m_wxh];
    memcpy( m_pixels, aSrcImage.GetBuffer(), m_wxh );
    m_width   = aSrcImage.GetWidth();
    m_height  = aSrcImage.GetHeight();
    m_wraping = IMAGE_WRAP::CLAMP;
}


IMAGE::~IMAGE()
{
    delete[] m_pixels;
}


unsigned char* IMAGE::GetBuffer() const
{
    return m_pixels;
}


bool IMAGE::wrapCoords( int* aXo, int* aYo ) const
{
    int x = *aXo;
    int y = *aYo;

    switch( m_wraping )
    {
    case IMAGE_WRAP::CLAMP:
        x = ( x < 0 ) ? 0 : x;
        x = ( x >= (int) ( m_width - 1 ) ) ? ( m_width - 1 ) : x;
        y = ( y < 0 ) ? 0 : y;
        y = ( y >= (int) ( m_height - 1 ) ) ? ( m_height - 1 ) : y;
        break;

    case IMAGE_WRAP::WRAP:
        x = ( x < 0 ) ? ( ( m_width - 1 ) + x ) : x;
        x = ( x >= (int) ( m_width - 1 ) ) ? ( x - m_width ) : x;
        y = ( y < 0 ) ? ( ( m_height - 1 ) + y ) : y;
        y = ( y >= (int) ( m_height - 1 ) ) ? ( y - m_height ) : y;
        break;

    default:
        break;
    }

    if( ( x < 0 ) || ( x >= (int) m_width ) || ( y < 0 ) || ( y >= (int) m_height ) )
        return false;

    *aXo = x;
    *aYo = y;

    return true;
}


void IMAGE::plot8CircleLines( int aCx, int aCy, int aX, int aY, unsigned char aValue )
{
    Hline( aCx - aX, aCx + aX, aCy + aY, aValue );
    Hline( aCx - aX, aCx + aX, aCy - aY, aValue );
    Hline( aCx - aY, aCx + aY, aCy + aX, aValue );
    Hline( aCx - aY, aCx + aY, aCy - aX, aValue );
}


void IMAGE::Setpixel( int aX, int aY, unsigned char aValue )
{
    if( wrapCoords( &aX, &aY ) )
        m_pixels[aX + aY * m_width] = aValue;
}


unsigned char IMAGE::Getpixel( int aX, int aY ) const
{
    if( wrapCoords( &aX, &aY ) )
        return m_pixels[aX + aY * m_width];
    else
        return 0;
}


void IMAGE::Hline( int aXStart, int aXEnd, int aY, unsigned char aValue )
{
    if( ( aY < 0 ) || ( aY >= (int) m_height ) || ( ( aXStart < 0 ) && ( aXEnd < 0 ) )
      || ( ( aXStart >= (int) m_width ) && ( aXEnd >= (int) m_width ) ) )
        return;

    if( aXStart > aXEnd )
    {
        int swap = aXStart;

        aXStart = aXEnd;
        aXEnd = swap;
    }

    // Clamp line
    if( aXStart < 0 )
        aXStart = 0;

    if( aXEnd >= (int)m_width )
        aXEnd = m_width - 1;

    unsigned char* pixelPtr = &m_pixels[aXStart + aY * m_width];
    unsigned char* pixelPtrEnd = pixelPtr + (unsigned int) ( ( aXEnd - aXStart ) + 1 );

    while( pixelPtr < pixelPtrEnd )
    {
        *pixelPtr = aValue;
        pixelPtr++;
    }
}


// Based on paper
// http://web.engr.oregonstate.edu/~sllu/bcircle.pdf
void IMAGE::CircleFilled( int aCx, int aCy, int aRadius, unsigned char aValue )
{
    int x = aRadius;
    int y = 0;
    int xChange = 1 - 2 * aRadius;
    int yChange = 0;
    int radiusError = 0;

    while( x >= y )
    {
        plot8CircleLines( aCx, aCy, x, y, aValue );
        y++;
        radiusError += yChange;
        yChange += 2;

        if( ( 2 * radiusError + xChange ) > 0 )
        {
            x--;
            radiusError += xChange;
            xChange += 2;
        }
    }
}


void IMAGE::Invert()
{
    for( unsigned int it = 0; it < m_wxh; it++ )
        m_pixels[it] = 255 - m_pixels[it];
}


void IMAGE::CopyFull( const IMAGE* aImgA, const IMAGE* aImgB, IMAGE_OP aOperation )
{
    int aV, bV;

    if( aOperation == IMAGE_OP::RAW )
    {
        if( aImgA == nullptr )
            return;
    }
    else
    {
        if( ( aImgA == nullptr ) || ( aImgB == nullptr ) )
            return;
    }

    switch( aOperation )
    {
    case IMAGE_OP::RAW:
        memcpy( m_pixels, aImgA->m_pixels, m_wxh );
        break;

    case IMAGE_OP::ADD:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            aV = aImgA->m_pixels[it];
            bV = aImgB->m_pixels[it];

            aV = (aV + bV);
            aV = (aV > 255)?255:aV;

            m_pixels[it] = aV;
        }
        break;

    case IMAGE_OP::SUB:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            aV = aImgA->m_pixels[it];
            bV = aImgB->m_pixels[it];

            aV = (aV - bV);
            aV = (aV < 0)?0:aV;

            m_pixels[it] = aV;
        }
        break;

    case IMAGE_OP::DIF:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            aV = aImgA->m_pixels[it];
            bV = aImgB->m_pixels[it];

            m_pixels[it] = abs( aV - bV );
        }
        break;

    case IMAGE_OP::MUL:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            aV = aImgA->m_pixels[it];
            bV = aImgB->m_pixels[it];

            m_pixels[it] =
                    (unsigned char) ( ( ( (float) aV / 255.0f ) * ( (float) bV / 255.0f ) ) * 255 );
        }
        break;

    case IMAGE_OP::AND:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            m_pixels[it] = aImgA->m_pixels[it] & aImgB->m_pixels[it];
        }
        break;

    case IMAGE_OP::OR:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            m_pixels[it] = aImgA->m_pixels[it] | aImgB->m_pixels[it];
        }
        break;

    case IMAGE_OP::XOR:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            m_pixels[it] = aImgA->m_pixels[it] ^ aImgB->m_pixels[it];
        }
        break;

    case IMAGE_OP::BLEND50:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            aV = aImgA->m_pixels[it];
            bV = aImgB->m_pixels[it];

            m_pixels[it] = (aV + bV) / 2;
        }
        break;

    case IMAGE_OP::MIN:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            aV = aImgA->m_pixels[it];
            bV = aImgB->m_pixels[it];

            m_pixels[it] = (aV < bV)?aV:bV;
        }
        break;

    case IMAGE_OP::MAX:
        for( unsigned int it = 0;it < m_wxh; it++ )
        {
            aV = aImgA->m_pixels[it];
            bV = aImgB->m_pixels[it];

            m_pixels[it] = (aV > bV)?aV:bV;
        }
        break;

    default:
        break;
    }
}


// TIP: If you want create or test filters you can use GIMP
// with a generic convolution matrix and get the values from there.
// http://docs.gimp.org/nl/plug-in-convmatrix.html
// clang-format off
static const S_FILTER FILTERS[] =   {
    // IMAGE_FILTER::HIPASS
    {
    {   { 0, -1, -1, -1,  0},
        {-1,  2, -4,  2, -1},
        {-1, -4, 13, -4, -1},
        {-1,  2, -4,  2, -1},
        { 0, -1, -1, -1,  0}
    },
        7,
        255
    },

    // IMAGE_FILTER::GAUSSIAN_BLUR
    {
    {   { 3,  5,  7,  5,  3},
        { 5,  9, 12,  9,  5},
        { 7, 12, 20, 12,  7},
        { 5,  9, 12,  9,  5},
        { 3,  5,  7,  5,  3}
    },
        182,
        0
    },

    // IMAGE_FILTER::GAUSSIAN_BLUR2
    {
    {   { 1,  4,  7,  4,  1},
        { 4, 16, 26, 16,  4},
        { 7, 26, 41, 26,  7},
        { 4, 16, 26, 16,  4},
        { 1,  4,  7,  4,  1}
    },
        273,
        0
    },

    // IMAGE_FILTER::INVERT_BLUR
    {
    {   { 0,  0,  0,  0,  0},
        { 0,  0, -1,  0,  0},
        { 0, -1,  0, -1,  0},
        { 0,  0, -1,  0,  0},
        { 0,  0,  0,  0,  0}
    },
        4,
        255
    },

    // IMAGE_FILTER::CARTOON
    {
    {   {-1, -1, -1, -1,  0},
        {-1,  0,  0,  0,  0},
        {-1,  0,  4,  0,  0},
        { 0,  0,  0,  1,  0},
        { 0,  0,  0,  0,  4}
    },
        3,
        0
    },

    // IMAGE_FILTER::EMBOSS
    {
    {   {-1, -1, -1, -1,  0},
        {-1, -1, -1,  0,  1},
        {-1, -1,  0,  1,  1},
        {-1,  0,  1,  1,  1},
        { 0,  1,  1,  1,  1}
    },
        1,
        128
    },

    // IMAGE_FILTER::SHARPEN
    {
    {   {-1, -1, -1, -1, -1},
        {-1,  2,  2,  2, -1},
        {-1,  2,  8,  2, -1},
        {-1,  2,  2,  2, -1},
        {-1, -1, -1, -1, -1}
    },
        8,
        0
    },

    // IMAGE_FILTER::MELT
    {
    {   { 4,  2,  6,  8,  1},
        { 1,  2,  5,  4,  2},
        { 0, -1,  1, -1,  0},
        { 0,  0, -2,  0,  0},
        { 0,  0,  0,  0,  0}
    },
        32,
        0
    },

    // IMAGE_FILTER::SOBEL_GX
    {
    {   { 0,  0,  0,  0,  0},
        { 0, -1,  0,  1,  0},
        { 0, -2,  0,  2,  0},
        { 0, -1,  0,  1,  0},
        { 0,  0,  0,  0,  0}
    },
        1,
        0
    },

    // IMAGE_FILTER::SOBEL_GY
    {
    {   { 1,  2,  4,  2,  1},
        {-1, -1,  0,  1,  1},
        {-2, -2,  0,  2,  2},
        {-1, -1,  0,  1,  1},
        {-1, -2, -4, -2, -1},
    },
        1,
        0
    },

    // IMAGE_FILTER::BLUR_3X3
    {
    {   { 0,  0,  0,  0,  0},
        { 0,  1,  2,  1,  0},
        { 0,  2,  4,  2,  0},
        { 0,  1,  2,  1,  0},
        { 0,  0,  0,  0,  0},
    },
        16,
        0
    }
};// Filters
// clang-format on


/// @todo: This function can be optimized slipping it between the edges and
///        do it without use the getpixel function.
///        Optimization can be done to m_pixels[ix + iy * m_width]
///        but keep in mind the parallel process of the algorithm
void IMAGE::EfxFilter( IMAGE* aInImg, IMAGE_FILTER aFilterType )
{
    S_FILTER filter = FILTERS[static_cast<int>( aFilterType )];

    aInImg->m_wraping = IMAGE_WRAP::CLAMP;
    m_wraping         = IMAGE_WRAP::CLAMP;

    std::atomic<size_t> nextRow( 0 );
    std::atomic<size_t> threadsFinished( 0 );

    size_t parallelThreadCount = std::max<size_t>( std::thread::hardware_concurrency(), 2 );

    for( size_t ii = 0; ii < parallelThreadCount; ++ii )
    {
        std::thread t = std::thread( [&]()
        {
            for( size_t iy = nextRow.fetch_add( 1 ); iy < m_height; iy = nextRow.fetch_add( 1 ) )
            {
                for( size_t ix = 0; ix < m_width; ix++ )
                {
                    int v = 0;

                    for( size_t sy = 0; sy < 5; sy++ )
                    {
                        for( size_t sx = 0; sx < 5; sx++ )
                        {
                            int factor = filter.kernel[sx][sy];
                            unsigned char pixelv = aInImg->Getpixel( ix + sx - 2, iy + sy - 2 );

                            v += pixelv * factor;
                        }
                    }

                    v /= filter.div;
                    v += filter.offset;
                    CLAMP(v, 0, 255);

                    /// @todo This needs to write to a separate buffer.
                    m_pixels[ix + iy * m_width] = v;
                }
            }

            threadsFinished++;
        } );

        t.detach();
    }

    while( threadsFinished < parallelThreadCount )
        std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
}


void IMAGE::EfxFilter_SkipCenter( IMAGE* aInImg, IMAGE_FILTER aFilterType, unsigned int aRadius )
{
    if( ( !aInImg ) || ( m_width != aInImg->m_width ) || ( m_height != aInImg->m_height ) )
        return;

    S_FILTER filter = FILTERS[static_cast<int>( aFilterType )];

    aInImg->m_wraping = IMAGE_WRAP::ZERO;

    const unsigned int radiusSquared = aRadius * aRadius;

    const unsigned int xCenter = m_width / 2;
    const unsigned int yCenter = m_height / 2;

    for( size_t iy = 0; iy < m_height; iy++ )
    {
        int yc = iy - yCenter;

        unsigned int ycsq = yc * yc;

        for( size_t ix = 0; ix < m_width; ix++ )
        {
            int xc = ix - xCenter;

            unsigned int xcsq = xc * xc;

            if( ( xcsq + ycsq ) < radiusSquared )
            {
                const unsigned int offset = ix + iy * m_width;

                m_pixels[offset] = aInImg->m_pixels[offset];

                continue;
            }

            int v = 0;

            for( size_t sy = 0; sy < 5; sy++ )
            {
                for( size_t sx = 0; sx < 5; sx++ )
                {
                    int factor = filter.kernel[sx][sy];
                    unsigned char pixelv = aInImg->Getpixel( ix + sx - 2, iy + sy - 2 );

                    v += pixelv * factor;
                }
            }

            v /= filter.div;
            v += filter.offset;
            CLAMP(v, 0, 255);

            m_pixels[ix + iy * m_width] = v;
        }
    }
}


void IMAGE::SetPixelsFromNormalizedFloat( const float* aNormalizedFloatArray )
{
    for( unsigned int i = 0; i < m_wxh; i++ )
    {
        int v = aNormalizedFloatArray[i] * 255;

        CLAMP( v, 0, 255 );
        m_pixels[i] = v;
    }
}


void IMAGE::SaveAsPNG( const wxString& aFileName ) const
{
    DBG_SaveBuffer( aFileName, m_pixels, m_width, m_height );
}