/*
    Copyright (C) 2020 - 2022 Akaneyu

    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 3 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, see <http://www.gnu.org/licenses/>.
*/

#include "symtex_processor.h"

#include <cstring>
#include <cmath>
#include <iostream>
#include <algorithm>

#include "math_util.h"

using namespace std;

Processor *g_processor;

int SYMTEX_init(int type)
{
    g_processor = new Processor(type);

    return 0;
}

void SYMTEX_free()
{
    delete g_processor;
}

void SYMTEX_setRegionSize(int width, int height)
{
    g_processor->setRegionSize(width, height);
}

void SYMTEX_setOrthogonal(int isOrtho)
{
    g_processor->setOrthogonal(isOrtho != 0);
}

void SYMTEX_setPerspectiveMatrix(float *mat)
{
    g_processor->setPerspectiveMatrix(mat);
}

void SYMTEX_setViewPosition(float *pos)
{
    g_processor->setViewPosition(pos);
}

void SYMTEX_setViewDirection(float *dir)
{
    g_processor->setViewDirection(dir);
}

void SYMTEX_setVertexCoords(float *coords, int numCoords)
{
    int i;

    for (i = 0; i < numCoords; i ++) {
        g_processor->addVertexCoord(coords + 3 * i);
    }
}

void SYMTEX_setVertexNormals(float *normals, int numNormals)
{
    int i;

    for (i = 0; i < numNormals; i ++) {
        g_processor->addVertexNormal(normals + 3 * i);
    }
}

void SYMTEX_setVertexIndices(int *indices, int numIndices)
{
    int i;

    for (i = 0; i < numIndices; i++) {
        g_processor->addVertexIndex(indices[i]);
    }
}

void SYMTEX_setUVCoords(float *coords, int numCoords)
{
    int i;

    for (i = 0; i < numCoords; i++) {
        g_processor->addUVCoord(coords + 2 * i);
    }
}

void SYMTEX_setTriangles(int *indices, int numTriangles)
{
    int i;

    for (i = 0; i < numTriangles; i++) {
        g_processor->addTriangle(indices + 3 * i);
    }
}

void SYMTEX_setImageSize(int width, int height)
{
    g_processor->setImageSize(width, height);
}

void SYMTEX_setImagePixels(float *pixels)
{
    g_processor->setImagePixels(pixels);
}

void SYMTEX_setMirrorAxis(int axis)
{
    g_processor->setMirrorAxis(axis);
}

void SYMTEX_setBrushSize(float size)
{
    g_processor->setBrushSize(size);
}

void SYMTEX_setBrushStrength(float strength)
{
    g_processor->setBrushStrength(strength);
}

void SYMTEX_setBrushFalloffType(int type)
{
    g_processor->setBrushFalloffType(type);
}

void SYMTEX_prepare()
{
    g_processor->prepare();
}

void SYMTEX_processStroke(float *pos)
{
    g_processor->processStroke(pos);
}

static float checkLinePointSide2d(float *line1, float *line2, float *pt)
{
    return ((line1[0] - pt[0]) * (line2[1] - pt[1]))
        - ((line2[0] - pt[0]) * (line1[1] - pt[1]));
}

static bool checkIntersectPointPolygon2d(float *pt, float **verts, int numVerts)
{
    int i;

    if (checkLinePointSide2d(verts[numVerts - 1], verts[0], pt) < 0) {
        return false;
    }

    for (i = 1; i < numVerts; i++) {
        if (checkLinePointSide2d(verts[i - 1], verts[i], pt) < 0) {
            return false;
        }
    }

    return true;
}

static void calcBarycentricWeights(float *weights, float *v1,  float *v2, float *v3, float *coord)
{
    weights[0] = crossTriVector2f(v2, v3, coord);
    weights[1] = crossTriVector2f(v3, v1, coord);
    weights[2] = crossTriVector2f(v1, v2, coord);

    float totalWeight = weights[0] + weights[1] + weights[2];

    if (totalWeight == 0) {
        copyVector3fValue(weights, 1.0f / 3.0f);
    } else {
        multiplyVector3fValue(weights, weights, 1.0f / totalWeight);
    }
}

static void interpolateWeights3d(float *out, float *v1, float *v2, float *v3, float *weights)
{
    out[0] = v1[0] * weights[0] + v2[0] * weights[1] + v3[0] * weights[2];
    out[1] = v1[1] * weights[0] + v2[1] * weights[1] + v3[1] * weights[2];
    out[2] = v1[2] * weights[0] + v2[2] * weights[1] + v3[2] * weights[2];
}

static void calcScreenCoordOrthogonal(float *scrCoord, float *uv,
        float *v1ScrCoord, float *v2ScrCoord, float *v3ScrCoord,
        float *uv1Coord, float *uv2Coord, float *uv3Coord)
{
    float weights[3];

    calcBarycentricWeights(weights, uv1Coord, uv2Coord, uv3Coord, uv);
    interpolateWeights3d(scrCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord, weights);
}

static void calcScreenCoordPerspective(float *scrCoord, float *uv,
        float *v1ScrCoord, float *v2ScrCoord, float *v3ScrCoord,
        float *uv1Coord, float *uv2Coord, float *uv3Coord)
{
    float weights[3];

    calcBarycentricWeights(weights, uv1Coord, uv2Coord, uv3Coord, uv);

    float weightsTemp[3];
    weightsTemp[0] = weights[0] * v1ScrCoord[3];
    weightsTemp[1] = weights[1] * v2ScrCoord[3];
    weightsTemp[2] = weights[2] * v3ScrCoord[3];

    float totalWeight = weightsTemp[0] + weightsTemp[1] + weightsTemp[2];

    if (totalWeight > 0) {
        float totalWeightInv = 1.0f / totalWeight;
        multiplyVector3fValue(weightsTemp, weightsTemp, totalWeightInv);
    } else {
        copyVector3fValue(weights, 1.0f / 3.0f);
        copyVector3fValue(weightsTemp, 1.0f / 3.0f);
    }

    interpolateWeights3d(scrCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord, weightsTemp);
}

Triangle::Triangle()
{
    copyVector3iValue(m_indices, 0);
}

Triangle::Triangle(int *indices)
{
    copyVector3i(m_indices, indices);
}

Triangle::~Triangle()
{
}

PixelState::PixelState()
{
    copyVector4fValue(m_screenCoord, 0);
    copyVector2iValue(m_imageCoord, 0);
}

PixelState::~PixelState()
{
}

void PixelState::setScreenCoord(float *coord)
{
    copyVector4f(m_screenCoord, coord);
}

void PixelState::setImageCoord(int *coord)
{
    copyVector2i(m_imageCoord, coord);
}

Processor::Processor(int type) :
    m_processType(type),
    m_regionWidth(0),
    m_regionHeight(0),
    m_orthogonal(false),
    m_imageWidth(0),
    m_imageHeight(0),
    m_imagePixels(NULL),
    m_originalImagePixels(NULL),
    m_imageAlpha(NULL),
    m_mirrorAxis(0),
    m_brushSize(0),
    m_brushStrength(1.0f),
    m_brushFalloffType(0)
{
    int i;

    for (i = 0; i < 16; i++) {
        m_perspectiveMatrix[i] = 0;
    }

    copyVector3fValue(m_viewPosition, 0);
    copyVector3fValue(m_viewDirection, 0);
}

Processor::~Processor()
{
    int i;

    for (i = 0; i < (int) m_vertexCoords.size(); i++) {
        delete [] m_vertexCoords[i];
    }

    for (i = 0; i < (int) m_vertexNormals.size(); i++) {
        delete [] m_vertexNormals[i];
    }

    for (i = 0; i < (int) m_screenCoords.size(); i++) {
        delete [] m_screenCoords[i];
    }

    for (i = 0; i < (int) m_uvCoords.size(); i++) {
        delete [] m_uvCoords[i];
    }

    for (i = 0; i < (int) m_triangles.size(); i++) {
        delete m_triangles[i];
    }

    if (m_originalImagePixels != NULL) {
        delete [] m_originalImagePixels;
    }

    if (m_imageAlpha != NULL) {
        delete [] m_imageAlpha;
    }

    for (i = 0; i < (int) m_pixelStates.size(); i++) {
        delete m_pixelStates[i];
    }
}

void Processor::setRegionSize(int width, int height)
{
    m_regionWidth = width;
    m_regionHeight = height;
}

void Processor::setPerspectiveMatrix(float *mat)
{
    int i;

    for (i = 0; i < 16; i++) {
        m_perspectiveMatrix[i] = mat[i];
    }
}

void Processor::setViewPosition(float *pos)
{
    copyVector3f(m_viewPosition, pos);
}

void Processor::setViewDirection(float *dir)
{
    copyVector3f(m_viewDirection, dir);
}

void Processor::addVertexCoord(float *coord)
{
    float *coordEntry = new float[3];
    copyVector3f(coordEntry, coord);

    m_vertexCoords.push_back(coordEntry);
}

void Processor::addVertexNormal(float *norm)
{
    float *normEntry = new float[3];
    copyVector3f(normEntry, norm);

    m_vertexNormals.push_back(normEntry);
}

void Processor::addVertexIndex(int index)
{
    m_vertexIndices.push_back(index);
}

void Processor::addUVCoord(float *coord)
{
    float *coordEntry = new float[2];
    copyVector2f(coordEntry, coord);

    m_uvCoords.push_back(coordEntry);
}

void Processor::addTriangle(int *indices)
{
    Triangle *tri = new Triangle(indices);
    m_triangles.push_back(tri);
}

void Processor::setImageSize(int width, int height)
{
    m_imageWidth = width;
    m_imageHeight = height;
}

void Processor::prepare()
{
    int i, j, x, y;

    long numPixels = m_imageWidth * m_imageHeight;
    long pixelBuffSize = numPixels * 4;

    m_originalImagePixels = new float[pixelBuffSize];
    memcpy(m_originalImagePixels, m_imagePixels, sizeof(float) * pixelBuffSize);

    m_imageAlpha = new unsigned short[numPixels];
    memset(m_imageAlpha, 0, sizeof(unsigned short) * numPixels);

    if (m_processType != 0) {
        return;
    }

    //cout << m_regionWidth << ", " << m_regionHeight << endl;
    //printMatrix4f(m_perspectiveMatrix);

    const float normalAngleInner = 80.0f;
    const float normalAngle = (normalAngleInner + 90.0f) / 2.0f;
    const float normalAngleCos = (float) cos(normalAngle * PI / 180.0f);

    float viewDirPersp[3];

    for (i = 0; i < (int) m_vertexCoords.size(); i++) {
        float *vertCoord = m_vertexCoords[i];

        float *scrCoord = new float[4];
        if (m_orthogonal) {
            multiplyMatrix4fVector3f(scrCoord, m_perspectiveMatrix, vertCoord);

            scrCoord[0] = (m_regionWidth * 0.5f) + (m_regionWidth * 0.5f) * scrCoord[0];
            scrCoord[1] = (m_regionHeight * 0.5f) + (m_regionHeight * 0.5f) * scrCoord[1];
        } else {
            copyVector3f(scrCoord, vertCoord);
            scrCoord[3] = 1.0f;
            multiplyMatrix4fVector4f(scrCoord, m_perspectiveMatrix, scrCoord);

            scrCoord[0] = m_regionWidth * 0.5f
                + m_regionWidth * 0.5f * scrCoord[0] / scrCoord[3];
            scrCoord[1] = m_regionHeight * 0.5f
                + m_regionHeight * 0.5f * scrCoord[1] / scrCoord[3];
            scrCoord[2] = scrCoord[2] / scrCoord[3];
        }

        m_screenCoords.push_back(scrCoord);

        float *vertNorm = m_vertexNormals[i];

        int vertState = 0;
        if (m_orthogonal) {
            if (dotVector3f(m_viewDirection, vertNorm) <= normalAngleCos) {
                vertState |= 1;
            }
        } else {
            subtractVector3f(viewDirPersp, m_viewPosition, vertCoord);
            normalizeVector3f(viewDirPersp);

            if (dotVector3f(viewDirPersp, vertNorm) <= normalAngleCos) {
                vertState |= 1;
            }
        }

        m_vertexStates.push_back(vertState);
    }

    int triVertIndices[3];
    float *triUvCoords[3];
    float minUvCoord[2];
    float maxUvCoord[2];
    float uvCoord[2];
    int minImgRect[2];
    int maxImgRect[2];

    for (i = 0; i < (int) m_triangles.size(); i++) {
        Triangle *tri = m_triangles[i];
        int *triIndices = tri->getIndices();

        triVertIndices[0] = m_vertexIndices[triIndices[0]];
        triVertIndices[1] = m_vertexIndices[triIndices[1]];
        triVertIndices[2] = m_vertexIndices[triIndices[2]];

        bool culled = true;
        for (j = 0; j < 3; j++) {
            int vertState = m_vertexStates[triVertIndices[j]];
            if ((vertState & 1) == 0) {
                culled = false;
                break;
            }
        }

        if (culled) {
            continue;
        }

        float *v1ScrCoord = m_screenCoords[triVertIndices[0]];
        float *v2ScrCoord = m_screenCoords[triVertIndices[1]];
        float *v3ScrCoord = m_screenCoords[triVertIndices[2]];

        //cout << "Triangle #" << i << endl;

        //printVector4f(v1ScrCoord);
        //printVector4f(v2ScrCoord);
        //printVector4f(v3ScrCoord);

        triUvCoords[0] = m_uvCoords[triIndices[0]];
        triUvCoords[1] = m_uvCoords[triIndices[1]];
        triUvCoords[2] = m_uvCoords[triIndices[2]];

        //printVector2f(triUvCoords[0]);
        //printVector2f(triUvCoords[1]);
        //printVector2f(triUvCoords[2]);

        minUvCoord[0] = numeric_limits<float>::max();
        minUvCoord[1] = numeric_limits<float>::max();
        maxUvCoord[0] = -numeric_limits<float>::max();
        maxUvCoord[1] = -numeric_limits<float>::max();

        for (j = 0; j < 3; j++) {
            minUvCoord[0] = triUvCoords[j][0] < minUvCoord[0] ? triUvCoords[j][0] : minUvCoord[0];
            minUvCoord[1] = triUvCoords[j][1] < minUvCoord[1] ? triUvCoords[j][1] : minUvCoord[1];
            maxUvCoord[0] = triUvCoords[j][0] > maxUvCoord[0] ? triUvCoords[j][0] : maxUvCoord[0];
            maxUvCoord[1] = triUvCoords[j][1] > maxUvCoord[1] ? triUvCoords[j][1] : maxUvCoord[1];
        }

        minImgRect[0] = (int) (m_imageWidth * minUvCoord[0]);
        minImgRect[1] = (int) (m_imageHeight * minUvCoord[1]);
        maxImgRect[0] = (int) (m_imageWidth * maxUvCoord[0] + 1);
        maxImgRect[1] = (int) (m_imageHeight * maxUvCoord[1] + 1);

        //cout << minImgRect[0] << ", " << minImgRect[1] << endl;
        //cout << maxImgRect[0] << ", " << maxImgRect[1] << endl;

        for (y = minImgRect[1]; y < maxImgRect[1]; y++) {
            uvCoord[1] = y / (float) m_imageHeight;

            for (x = minImgRect[0]; x < maxImgRect[0]; x++) {
                uvCoord[0] = x / (float) m_imageWidth;

                preparePixel(x, y, uvCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord, triUvCoords);
            }
        }
    }
}

void Processor::processStroke(float *pos)
{
    if (m_processType == 0) {
        processStroke3d(pos);
    } else {
        processStroke2d(pos);
    }
}

void Processor::preparePixel(int x, int y, float *uvCoord,
        float *v1ScrCoord, float *v2ScrCoord, float *v3ScrCoord, float **triUvCoords)
{
    if (!checkIntersectPointPolygon2d(uvCoord, triUvCoords, 3)) {
        return;
    }

    float pixelScrCoord[4];
    if (m_orthogonal) {
        calcScreenCoordOrthogonal(pixelScrCoord,
                uvCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord,
                triUvCoords[0], triUvCoords[1], triUvCoords[2]);
    } else {
        calcScreenCoordPerspective(pixelScrCoord,
                uvCoord, v1ScrCoord, v2ScrCoord, v3ScrCoord,
                triUvCoords[0], triUvCoords[1], triUvCoords[2]);
    }

    x = x % m_imageWidth;
    y = y % m_imageHeight;

    int imgCoord[] = {x, y};

    PixelState *pixelState = new PixelState();
    pixelState->setScreenCoord(pixelScrCoord);
    pixelState->setImageCoord(imgCoord);

    m_pixelStates.push_back(pixelState);
}

void Processor::processStroke3d(float *pos)
{
    int i;

    float brushRadiusSq = m_brushSize * m_brushSize;

    for (i = 0; i < (int) m_pixelStates.size(); i++) {
        PixelState *pixelState = m_pixelStates[i];

        float distSq = lenSquaredVector2f(pos, pixelState->getScreenCoord());

        if (distSq >= brushRadiusSq) {
            continue;
        }

        int *imgCoord = pixelState->getImageCoord();

        float dist = (float) sqrt(distSq);
        float falloff = calcBrushFalloff(dist, m_brushSize);

        processStrokePixel(imgCoord[0], imgCoord[1], falloff);
    }
}

void Processor::processStroke2d(float *pos)
{
    int x, y;

    int px = (int) (m_imageWidth * pos[0]);
    int py = (int) (m_imageHeight * pos[1]);

    int brushRadius = (int) (m_imageWidth * m_brushSize);

    int x1 = px - brushRadius;
    int y1 = py - brushRadius;
    int x2 = px + brushRadius;
    int y2 = py + brushRadius;

    x1 = max(min(x1, m_imageWidth), 0);
    y1 = max(min(y1, m_imageHeight), 0);
    x2 = max(min(x2, m_imageWidth), 0);
    y2 = max(min(y2, m_imageHeight), 0);

    float brushRadiusSq = brushRadius * (float) brushRadius;

    for (y = y1; y < y2; y++) {
        for (x = x1; x < x2; x++) {
            float distSq = (px - x) * (float) (px - x) + (py - y) * (float) (py - y);

            if (distSq >= brushRadiusSq) {
                continue;
            }

            float dist = (float) sqrt(distSq);
            float falloff = calcBrushFalloff(dist, (float) brushRadius);

            processStrokePixel(x, y, falloff);
        }
    }
}

void Processor::processStrokePixel(int x, int y, float falloff)
{
    long alphaOffset = m_imageWidth * y + x;

    float alphaTemp = falloff * m_brushStrength;

    float alphaSaved = m_imageAlpha[alphaOffset] / 65535.0f;
    float alpha = (alphaTemp - alphaSaved * falloff) + alphaSaved;

    if (alpha > 1.0f) {
        alpha = 1.0f;
    }

    if (alpha > alphaSaved) {
        m_imageAlpha[alphaOffset] = (unsigned short) (alpha * 65535.0f);
    }
    else {
        return;
    }

    if (alpha <= 0) {
        return;
    }

    long pixelOffset = (m_imageWidth * y + x) * 4;

    float *addr = m_imagePixels + pixelOffset;
    float *origAddr = m_originalImagePixels + pixelOffset;

    int mirrorOffset = 0;
    if (m_mirrorAxis == 0) {
        mirrorOffset = (m_imageWidth * y + (m_imageWidth - x)) * 4;
    } else {
        mirrorOffset = (m_imageWidth * (m_imageHeight - y - 1) + x) * 4;
    }

    float *mirrorAddr = m_imagePixels + mirrorOffset;

    float srcColor[4];
    float destColor[4];

    srcColor[0] = *mirrorAddr;
    srcColor[1] = *(mirrorAddr + 1);
    srcColor[2] = *(mirrorAddr + 2);
    srcColor[3] = *(mirrorAddr + 3);

    //srcColor[0] = 1.0f;
    //srcColor[1] = 0;
    //srcColor[2] = 0;
    //srcColor[3] = 1.0f;

    destColor[0] = *origAddr;
    destColor[1] = *(origAddr + 1);
    destColor[2] = *(origAddr + 2);
    destColor[3] = *(origAddr + 3);

    multiplyVector4fValue(srcColor, srcColor, alpha);
    //srcColor[3] = alpha;

    if (srcColor[3] > 0) {
        float t = srcColor[3];
        float tComp = 1.0f - t;

        destColor[0] = tComp * destColor[0] + srcColor[0];
        destColor[1] = tComp * destColor[1] + srcColor[1];
        destColor[2] = tComp * destColor[2] + srcColor[2];
        destColor[3] = tComp * destColor[3] + t;

        *addr = destColor[0];
        *(addr + 1) = destColor[1];
        *(addr + 2) = destColor[2];
        *(addr + 3) = destColor[3];
    }
}

float Processor::calcBrushFalloff(float dist, float len)
{
    float falloff = 1.0f;   // default: constant

    if (dist >= len) {
        return 0;
    }

    float p = 1.0f - dist / len;

    if (m_brushFalloffType == 0) {  // smooth
        falloff = 3.0f * p * p - 2.0f * p * p * p;
    }

    if (falloff < 0) {
        falloff = 0;
    } else if (falloff > 1.0f) {
        falloff = 1.0f;
    }

    return falloff;
}
