#include "CLight.h"
#include "Core/Render/CGraphics.h"
#include <Common/Common.h>
#include <cmath>
#include <float.h>

#define CLIGHT_NO_RADIUS 0x40
#define CLIGHT_NO_INTENSITY 0x80

CLight::CLight()
    : mPosition(skDefaultLightPos)
    , mDirection(skDefaultLightDir)
    , mDistAttenCoefficients(0.f, 1.f, 0.f)
    , mAngleAttenCoefficients(0.f, 1.f, 0.f)
    , mCachedRadius(0.f)
    , mCachedIntensity(0.f)
    , mDirtyFlags(CLIGHT_NO_RADIUS | CLIGHT_NO_INTENSITY)
{
}

// ************ DATA MANIPULATION ************

// This function is reverse engineered from the kiosk demo's code
float CLight::CalculateRadius() const
{
    if (mDistAttenCoefficients.Y < FLT_EPSILON && mDistAttenCoefficients.Z < FLT_EPSILON)
        return FLT_MAX;

    float Intensity = GetIntensity();
    if (mDistAttenCoefficients.Z > FLT_EPSILON) {
        if (Intensity <= FLT_EPSILON)
            return 0.f;
        return std::sqrt(Intensity / (0.0588235f * mDistAttenCoefficients.Z));
    }

    if (mDistAttenCoefficients.Y > FLT_EPSILON)
        return Intensity / (0.0588235f * mDistAttenCoefficients.Y);
    return 0.f;

#if 0
    if ((mDistAttenCoefficients.Y >= FLT_EPSILON) ||
        (mDistAttenCoefficients.Z >= FLT_EPSILON))
    {
        float Intensity = GetIntensity();

        if (mDistAttenCoefficients.Z > FLT_EPSILON)
        {
            if (Intensity <= FLT_EPSILON)
                return 0.f;

            float IntensityMod = (Intensity * 5.f / 255.f * mDistAttenCoefficients.Z);
            return sqrtf(Intensity / IntensityMod);
        }

        else
        {
            if (mDistAttenCoefficients.Y <= FLT_EPSILON)
                return 0.f;

            float IntensityMod = (Intensity * 5.f) / 255.f;
            if (IntensityMod < 0.2f)
                IntensityMod = 0.2f;

            return Intensity / (IntensityMod * mDistAttenCoefficients.Y);
        }
    }

    else return 3000000000000000000000000000000000000.f;
#endif
}

// This function is also reverse engineered from the kiosk demo's code
float CLight::CalculateIntensity() const
{
    float coef = 1.f;
    if (mType == ELightType::Custom)
        coef = mAngleAttenCoefficients.X;

    return coef * std::max(mColor.R, std::max(mColor.G, mColor.B));
#if 0
    // Get the color component with the greatest numeric value
    float Greatest = (mColor.G >= mColor.B) ? mColor.G : mColor.B;
    Greatest = (mColor.R >= Greatest) ? mColor.R : Greatest;

    float Multiplier = (mType == ELightType::Custom) ? mAngleAttenCoefficients.X : 1.0f;
    return Greatest * Multiplier;
#endif
}

// As is this one... partly
CVector3f CLight::CalculateSpotAngleAtten()
{
    if (mType != ELightType::Spot) return CVector3f(1.f, 0.f, 0.f);

    float RadianCutoff = mSpotCutoff * (3.1415927f / 180.f);
    float RadianCosine = cosf(RadianCutoff);
    float InvCosine = 1.f - RadianCosine;

    return CVector3f(0.f, -RadianCosine / InvCosine, 1.f / InvCosine);
}

// ************ ACCESSORS ************
float CLight::GetRadius() const
{
    if (mDirtyFlags & CLIGHT_NO_RADIUS)
    {
        mCachedRadius = CalculateRadius();
        mDirtyFlags &= ~CLIGHT_NO_RADIUS;
    }

    return mCachedRadius;
}

float CLight::GetIntensity() const
{
    if (mDirtyFlags & CLIGHT_NO_INTENSITY)
    {
        mCachedIntensity = CalculateIntensity();
        mDirtyFlags &= ~CLIGHT_NO_INTENSITY;
    }

    return mCachedIntensity;
}

void CLight::SetColor(const CColor& rkColor)
{
    mColor = rkColor;
    mDirtyFlags = CLIGHT_NO_RADIUS | CLIGHT_NO_INTENSITY;
}

void CLight::SetSpotCutoff(float Cutoff)
{
    mSpotCutoff = Cutoff * 0.5f;
    CalculateSpotAngleAtten();
}

void CLight::SetDistAtten(float DistCoefA, float DistCoefB, float DistCoefC)
{
    mDistAttenCoefficients.X = DistCoefA;
    mDistAttenCoefficients.Y = DistCoefB;
    mDistAttenCoefficients.Z = DistCoefC;
}

void CLight::SetAngleAtten(float AngleCoefA, float AngleCoefB, float AngleCoefC)
{
    mAngleAttenCoefficients.X = AngleCoefA;
    mAngleAttenCoefficients.Y = AngleCoefB;
    mAngleAttenCoefficients.Z = AngleCoefC;
}

CStructProperty* CLight::GetProperties() const
{
    //@todo MP1 properties only
    //@todo we cannot display full properties because a lot of them are discarded on load
    static CStructProperty* pProperties = nullptr;

    if (!pProperties)
    {
        pProperties = (CStructProperty*) IProperty::CreateIntrinsic(EPropertyType::Struct,
                                                                          EGame::Prime,
                                                                          0,
                                                                          "Light");

        //@todo it would be really cool if the property could detect all possible values automatically from TEnumReflection
        CChoiceProperty* pLightType = (CChoiceProperty*) IProperty::CreateIntrinsic(EPropertyType::Choice,
                                                                                       pProperties,
                                                                                       MEMBER_OFFSET(CLight, mType),
                                                                                       "LightType");
        pLightType->AddValue("LocalAmbient", (uint32) ELightType::LocalAmbient);
        pLightType->AddValue("Directional", (uint32) ELightType::Directional);
        pLightType->AddValue("Spot", (uint32) ELightType::Spot);
        pLightType->AddValue("Custom", (uint32) ELightType::Custom);

        IProperty::CreateIntrinsic(EPropertyType::Color,
                                      pProperties,
                                      MEMBER_OFFSET(CLight, mColor),
                                      "Color");

        IProperty::CreateIntrinsic(EPropertyType::Vector,
                                      pProperties,
                                      MEMBER_OFFSET(CLight, mPosition),
                                      "Position");

        IProperty::CreateIntrinsic(EPropertyType::Vector,
                                      pProperties,
                                      MEMBER_OFFSET(CLight, mDirection),
                                      "Direction");

        IProperty::CreateIntrinsic(EPropertyType::Float,
                                      pProperties,
                                      MEMBER_OFFSET(CLight, mSpotCutoff),
                                      "SpotCutoff");
    }

    return pProperties;
}

// ************ OTHER ************
void CLight::Load() const
{
    uint8 Index = (uint8) CGraphics::sNumLights;
    if (Index >= 8) return;

    CGraphics::SLightBlock::SGXLight *pLight = &CGraphics::sLightBlock.Lights[Index];

    switch (mType)
    {
    case ELightType::LocalAmbient:
        // LocalAmbient is already accounted for in CGraphics::sAreaAmbientColor
        return;
    case ELightType::Directional:
        pLight->Position = CVector4f(-mDirection * 1048576.f, 1.f);
        pLight->Direction = CVector4f(mDirection, 0.f);
        pLight->Color = mColor * CGraphics::sWorldLightMultiplier;
        pLight->DistAtten = CVector4f(1.f, 0.f, 0.f, 0.f);
        pLight->AngleAtten = CVector4f(1.f, 0.f, 0.f, 0.f);
        break;
    case ELightType::Spot:
        pLight->Position = CVector4f(mPosition,  1.f);
        pLight->Direction = CVector4f(mDirection, 0.f);
        pLight->Color = mColor * CGraphics::sWorldLightMultiplier;
        pLight->DistAtten = mDistAttenCoefficients;
        pLight->AngleAtten = mAngleAttenCoefficients;
        break;
    case ELightType::Custom:
        pLight->Position = CVector4f(mPosition,  1.f);
        pLight->Direction = CVector4f(mDirection, 0.f);
        pLight->Color = mColor * CGraphics::sWorldLightMultiplier;
        pLight->DistAtten = mDistAttenCoefficients;
        pLight->AngleAtten = mAngleAttenCoefficients;
        break;
    default:
        return;
    }
    CGraphics::sNumLights++;
}

// ************ STATIC ************
CLight CLight::BuildLocalAmbient(const CVector3f& rkPosition, const CColor& rkColor)
{
    CLight pLight;
    pLight.mType = ELightType::LocalAmbient;
    pLight.mPosition = rkPosition;
    pLight.mDirection = skDefaultLightDir;
    pLight.mColor = rkColor;
    pLight.mSpotCutoff = 180.f;
    return pLight;
}

CLight CLight::BuildDirectional(const CVector3f& rkPosition, const CVector3f& rkDirection, const CColor& rkColor)
{
    CLight pLight;
    pLight.mType = ELightType::Directional;
    pLight.mPosition = rkPosition;
    pLight.mDirection = rkDirection;
    pLight.mColor = rkColor;
    pLight.mSpotCutoff = 180.f;
    return pLight;
}

CLight CLight::BuildSpot(const CVector3f& rkPosition, const CVector3f& rkDirection, const CColor& rkColor, float Cutoff)
{
    CLight pLight;
    pLight.mType = ELightType::Spot;
    pLight.mPosition = rkPosition;
    pLight.mDirection = -rkDirection.Normalized();
    pLight.mColor = rkColor;
    pLight.mSpotCutoff = Cutoff;
    pLight.mAngleAttenCoefficients = pLight.CalculateSpotAngleAtten();
    return pLight;
}

CLight CLight::BuildCustom(const CVector3f& rkPosition, const CVector3f& rkDirection, const CColor& rkColor,
                           float DistAttenA, float DistAttenB, float DistAttenC,
                           float AngleAttenA, float AngleAttenB, float AngleAttenC)
{
    CLight pLight;
    pLight.mType = ELightType::Custom;
    pLight.mPosition = rkPosition;
    pLight.mDirection = rkDirection;
    pLight.mColor = rkColor;
    pLight.mSpotCutoff = 0.f;
    pLight.mDistAttenCoefficients.X = DistAttenA;
    pLight.mDistAttenCoefficients.Y = DistAttenB;
    pLight.mDistAttenCoefficients.Z = DistAttenC;
    pLight.mAngleAttenCoefficients.X = AngleAttenA;
    pLight.mAngleAttenCoefficients.Y = AngleAttenB;
    pLight.mAngleAttenCoefficients.Z = AngleAttenC * AngleAttenC;
    return pLight;
}

// ************ CONSTANTS ************
const CVector3f CLight::skDefaultLightPos(0.f, 0.f, 0.f);
const CVector3f CLight::skDefaultLightDir(0.f,-1.f, 0.f);
