/*
*   This file is part of Luma3DS
*   Copyright (C) 2016-2020 Aurora Wright, TuxSH
*
*   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/>.
*
*   Additional Terms 7.b and 7.c of GPLv3 apply to this file:
*       * Requiring preservation of specified reasonable legal notices or
*         author attributions in that material or in the Appropriate Legal
*         Notices displayed by works containing it.
*       * Prohibiting misrepresentation of the origin of that material,
*         or requiring that modified versions of such material be marked in
*         reasonable ways as different from the original version.
*/

/* This file is taken from Luma3DS project */

#include <3ds.h>
#include <math.h>
#include "luminance-ctru.h"

// For accessing physmem uncached (and directly)
#define PA_PTR(addr)            (void *)((u32)(addr) | 1 << 31)

#ifndef PA_FROM_VA_PTR
#define PA_FROM_VA_PTR(addr)    PA_PTR(svcConvertVAToPA((const void *)(addr), false))
#endif

#define REG32(addr)             (*(vu32 *)(PA_PTR(addr)))


extern bool is_n3ds;

typedef struct BlPwmData
{
    float coeffs[3][3];
    u8 numLevels;
    u8 unk;
    u16  luminanceLevels[7];
    u16  brightnessMax;
    u16  brightnessMin;
} BlPwmData;

// Calibration, with (dubious) default values as fallback
static BlPwmData s_blPwmData = {
    .coeffs = {
        { 0.00111639f, 1.41412f, 0.07178809f },
        { 0.000418169f, 0.66567f, 0.06098654f },
        { 0.00208543f, 1.55639f, 0.0385939f }
    },
    .numLevels = 5,
    .unk = 0,
    .luminanceLevels = { 20, 43, 73, 95, 117, 172, 172 },
    .brightnessMax = 512,
    .brightnessMin = 13,
};

static inline float getPwmRatio(u32 brightnessMax, u32 pwmCnt)
{
    u32 val = (pwmCnt & 0x10000) ? pwmCnt & 0x3FF : 511; // check pwm enabled flag
    return (float)brightnessMax / (val + 1);
}

// nn's asm has rounding errors (originally at 10^-3)
static inline u32 luminanceToBrightness(u32 luminance, const float coeffs[3], u32 minLuminance, float pwmRatio)
{
    float x = (float)luminance;
    float y = coeffs[0]*x*x + coeffs[1]*x + coeffs[2];
    y = (y <= minLuminance ? (float)minLuminance : y) / pwmRatio;

    return (u32)(y + 0.5f);
}

static inline u32 brightnessToLuminance(u32 brightness, const float coeffs[3], float pwmRatio)
{
    // Find polynomial root of ax^2 + bx + c = y

    float y = (float)brightness * pwmRatio;
    float a = coeffs[0];
    float b = coeffs[1];
    float c = coeffs[2] - y;

    float x0 = (-b + sqrtf(b*b - 4.0f*a*c)) / (a + a);

    return (u32)(x0 + 0.5f);
}

static void readCalibration(void)
{
    static bool calibRead = false;

    if (!calibRead) {
        /* cfguInit() is called once in system_init() */
        calibRead = R_SUCCEEDED(CFG_GetConfigInfoBlk8(sizeof(BlPwmData), 0x50002, &s_blPwmData));
    }
}

u32 getMinLuminancePreset(void)
{
    readCalibration();
    return s_blPwmData.luminanceLevels[0];
}

u32 getMaxLuminancePreset(void)
{
    readCalibration();
    return s_blPwmData.luminanceLevels[s_blPwmData.numLevels - 1];
}

u32 getCurrentLuminance(bool top)
{
    u32 regbase = top ? 0x10202200 : 0x10202A00;

    readCalibration();

    const float *coeffs = s_blPwmData.coeffs[top ? (is_n3ds ? 2 : 1) : 0];
    u32 brightness = REG32(regbase + 0x40);
    float ratio = getPwmRatio(s_blPwmData.brightnessMax, REG32(regbase + 0x44));

    return brightnessToLuminance(brightness, coeffs, ratio);
}
