/* SPDX-License-Identifier: GPL-3.0-or-later */
/*
 *  Copyright (C) 2015-2021, Rasmus Althoff <info@ct800.net>
 *
 *  This file is part of the CT800 (signal interface / ARM).
 *
 *  CT800/NGPlay 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.
 *
 *  CT800/NGPlay 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 CT800/NGPlay. If not, see <http://www.gnu.org/licenses/>.
 *
 */
#include <stdint.h>
#include <stddef.h>
#include "ctdefs.h"
/*configuration reference is needed because the backlight handling depends
  on the current device configuration.*/
#include "confdefs.h"
#include "hardware.h"
#include "arm_driver.h"

/*---------- external variables ----------*/
/*-- READ-ONLY --*/
extern uint64_t hw_config;
extern volatile uint32_t battery_status;

/*output GPIO*/
#define LIGHT_GPIO_BSRR      GPIOA_BSRR
#define LED_GPIO_BSRR        GPIOB_BSRR
#define BUZ_GPIO_BSRR        GPIOB_BSRR

/*the states what features are being active.
  bits 30/31 are for the state, bit 0-29 for the remaining time.
  red / green LEDs do not have state bits, just time!=0.
  putting state and time into one uint32_t allows atomic read/write so
  that no locking mechanism is necessary.*/
static volatile uint32_t hw_state_led_green;
static volatile uint32_t hw_state_led_red;
static volatile uint32_t hw_state_led_back;
static volatile uint32_t hw_state_beep;

#define SIG_STATE_INIT       0

/*leave the two upper bits free. this still gives 12 days, and the maximum
  signal duration is just 30 seconds anyway.*/
#define SIG_STATE_MAX_TIME   0x3FFFFFFFU

#define SIG_STATE_BACK_INHIB ((uint32_t)(1UL << 30))

#define SIG_STATE_BEEP_ON    ((uint32_t)(1UL << 30))
#define SIG_STATE_BEEP_INT   ((uint32_t)(2UL << 30))
#define SIG_STATE_BEEP_ERR   ((uint32_t)(3UL << 30))
#define SIG_STATE_BEEP_ANY   ((uint32_t)(3UL << 30))

/*this needs to be a separate handler: the keyboard click is
  10ms, but with a 10ms system time resolution, this might be anything
  from 1ms to 10ms. Likewise putting it to 20ms would be anything
  between 10ms and 20ms. The other signals are much longer than that,
  so it doesn't matter.

  Just like the signal handler (see below), this routine is for
  switching OFF the beeper after its time has elapsed.*/
void Hw_Sig_Beep_Handler(uint32_t timer_period)
{
    uint32_t beep_mode = hw_state_beep;

    if (beep_mode != SIG_STATE_INIT) /*some beep is active*/
    {
        uint32_t beep_runtime = beep_mode & SIG_STATE_MAX_TIME;

        beep_mode &= SIG_STATE_BEEP_ANY;

        if (beep_runtime > timer_period) /*time count down*/
        {
            beep_runtime -= timer_period;
            hw_state_beep = beep_mode | beep_runtime;
        } else /*end of signal*/
        {
            /*switch off beeper*/
            Drv_SETRESET_BITS(BUZ_GPIO_BSRR, BUZZ_ODR_OUT << 16);
            hw_state_beep = SIG_STATE_INIT;
            return;
        }

        if (beep_mode == SIG_STATE_BEEP_ERR) /*error beeping signal?*/
        {
            /*the error beep signal is a distorted beep, generated by quickly
              switching it on/off: 5 ms on / 5 ms off.*/
            if ((beep_runtime / 5U) & 1U)
                Drv_SETRESET_BITS(BUZ_GPIO_BSRR, BUZZ_ODR_OUT); /*on*/
            else
                Drv_SETRESET_BITS(BUZ_GPIO_BSRR, BUZZ_ODR_OUT << 16); /*off*/
        } else if (beep_mode == SIG_STATE_BEEP_INT) /*interrupted beeping signal?*/
        {
            /*the "move found" beep is a double beep with 130 ms beep,
              70 ms pause, then 130 ms beep again. quite cute.*/
            if (beep_runtime <= BEEP_MOVE_ON) /*second beep*/
                Drv_SETRESET_BITS(BUZ_GPIO_BSRR, BUZZ_ODR_OUT);
            else if (beep_runtime <= BEEP_MOVE_OFF) /*intermediate pause*/
                Drv_SETRESET_BITS(BUZ_GPIO_BSRR, BUZZ_ODR_OUT << 16);
        }
    }
}

/*the signals get switched ON from within the application, along
  with the information for how long they shall be switched on. This
  signal handler counts the time down and switches them off after
  their time has elapsed.*/
void Hw_Sig_Handler(uint32_t timer_period)
{
    uint32_t led_runtime;

    led_runtime = hw_state_led_green;
    if (led_runtime != SIG_STATE_INIT) /*green LED active*/
    {
        if (led_runtime > timer_period) /*time count down*/
            hw_state_led_green = led_runtime - timer_period;
        else /*end of signal*/
        {
            /*switch off green LED*/
            Drv_SETRESET_BITS(LED_GPIO_BSRR, LED_GR_ODR_OUT << 16);
            hw_state_led_green = SIG_STATE_INIT;
        }
    }

    led_runtime = hw_state_led_red;
    if (led_runtime != SIG_STATE_INIT) /*red LED active*/
    {
        if (led_runtime > timer_period) /*time count down*/
            hw_state_led_red = led_runtime - timer_period;
        else /*end of signal*/
        {
            /*switch off red LED*/
            Drv_SETRESET_BITS(LED_GPIO_BSRR, LED_RD_ODR_OUT << 16);
            hw_state_led_red = SIG_STATE_INIT;
        }
    }

    /*filter out backlight inhibit bit.
      when the inhibit bit is set, the runtime is always 0.*/
    led_runtime = hw_state_led_back & SIG_STATE_MAX_TIME;
    if (led_runtime > 0) /*display backlight active*/
    {
        /*the display backlight may be switched off not only when
          time runs out, but also when the battery level drops too
          much so that we need to reduce energy consumption.*/
        if (battery_status & BATTERY_LOW)
        {
            /*drop the light immediately.*/
            Drv_SETRESET_BITS(LIGHT_GPIO_BSRR, LIGHT_ODR_OUT << 16);
            hw_state_led_back = SIG_STATE_INIT;
        } else
        {
            uint64_t light_mode = CFG_GET_OPT(CFG_LIGHT_MODE);
            /*what is the configured backlight mode? can be
              OFF, AUTO or ALWAYS.*/
            if (light_mode != CFG_LIGHT_ON) /*OFF or AUTO*/
            /*note: if the light gets switched to OFF from either ON or
              AUTO, this is not handled here. Instead, the menu system
              will call Hw_Sig_Send_Msg() with HW_MSG_LED_BACK_FADE,
              which reduces the remaining ON time to about half a
              second. that way, the user can see that the setting has
              been accepted before the light goes off.*/
            {
                /*switch off the light if the running time is over, but
                  not in "always on" mode.*/
                if (led_runtime > timer_period) /*time count down*/
                    hw_state_led_back = led_runtime - timer_period;
                else
                {
                    /*switch off backlight*/
                    Drv_SETRESET_BITS(LIGHT_GPIO_BSRR, LIGHT_ODR_OUT << 16);
                    hw_state_led_back = SIG_STATE_INIT;
                }
            } else
            {
                /*in "always on" mode, keep the light running, but set the
                  remaining running time to BACKLIGHT_KEY so that a change in the
                  setting from ON to AUTO will not make the light flicker.*/
                hw_state_led_back = BACKLIGHT_KEY;
            }
        }
    }
}

/*activates a signal, possibly with duration and parameter.*/
void Hw_Sig_Send_Msg(enum E_HW_MSG message, uint32_t duration, enum E_HW_MSG_PARAM param)
{
    /*clip to maximum duration*/
    duration &= SIG_STATE_MAX_TIME;

    switch (message)
    {
    /*initialise the module*/
    case HW_MSG_INIT:
        hw_state_led_green = SIG_STATE_INIT;
        hw_state_led_red   = SIG_STATE_INIT;
        hw_state_led_back  = SIG_STATE_INIT;
        hw_state_beep      = SIG_STATE_INIT;
        /*green and red LED off*/
        Drv_SETRESET_BITS(LED_GPIO_BSRR, (LED_GR_ODR_OUT | LED_RD_ODR_OUT) << 16);
        /*beeper off*/
        Drv_SETRESET_BITS(BUZ_GPIO_BSRR, BUZZ_ODR_OUT << 16);
        /*display backlight off*/
        Drv_SETRESET_BITS(LIGHT_GPIO_BSRR, LIGHT_ODR_OUT << 16);
        break;

    /*switch on the green LED or extend its running time*/
    case HW_MSG_LED_GREEN_ON:
        if (hw_state_led_green < duration)
            hw_state_led_green = duration; /*to be counted down*/
        Drv_SETRESET_BITS(LED_GPIO_BSRR, LED_GR_ODR_OUT);
        break;

    /*switch on the red LED or extend its running time*/
    case HW_MSG_LED_RED_ON:
        if (hw_state_led_red < duration)
            hw_state_led_red = duration; /*to be counted down*/
        Drv_SETRESET_BITS(LED_GPIO_BSRR, LED_RD_ODR_OUT);
        break;

    /*switch on the backlight or extend its running time*/
    case HW_MSG_LED_BACK_ON:
        {
            uint32_t back_state = hw_state_led_back;
            uint64_t light_mode  = CFG_GET_OPT(CFG_LIGHT_MODE);

            if ( ((light_mode == CFG_LIGHT_ON) || (light_mode == CFG_LIGHT_AUTO) || (param == HW_MSG_PARAM_BACK_FORCE)) &&
                 (battery_status & BATTERY_HIGH) &&
                 (back_state != SIG_STATE_BACK_INHIB) )
            {
                if (back_state < duration)
                    hw_state_led_back = duration; /*to be counted down*/
                /*switch on backlight*/
                Drv_SETRESET_BITS(LIGHT_GPIO_BSRR, LIGHT_ODR_OUT);
            }
        }
        break;

    /*switch off the backlight*/
    case HW_MSG_LED_BACK_OFF:
        hw_state_led_back &= SIG_STATE_BACK_INHIB;
        Drv_SETRESET_BITS(LIGHT_GPIO_BSRR, LIGHT_ODR_OUT << 16);
        break;

    /*switch off the backlight with delay*/
    case HW_MSG_LED_BACK_FADE:
        /*when inhibited, the runtime is always 0*/
        if ((hw_state_led_back & SIG_STATE_MAX_TIME) > duration)
            hw_state_led_back = duration;
        break;

    /*inhibit switching on the backlight and switch it off*/
    case HW_MSG_LED_BACK_INHIB:
        hw_state_led_back = SIG_STATE_BACK_INHIB;
        Drv_SETRESET_BITS(LIGHT_GPIO_BSRR, LIGHT_ODR_OUT << 16);
        break;

    /*allow switching on the backlight*/
    case HW_MSG_LED_BACK_ALLOW:
        hw_state_led_back = SIG_STATE_INIT;
        break;

    /*switch on the beeper*/
    case HW_MSG_BEEP_ON:
        if  (  (CFG_HAS_OPT(CFG_SPEAKER_MODE, CFG_SPEAKER_ON)) ||
          ((CFG_HAS_OPT(CFG_SPEAKER_MODE, CFG_SPEAKER_CLICK)) && (param == HW_MSG_PARAM_CLICK)) ||
          ((CFG_HAS_OPT(CFG_SPEAKER_MODE, CFG_SPEAKER_COMP)) && (param > HW_MSG_PARAM_CLICK)) )
        {
            uint32_t new_beep_state, current_beep_state, current_duration;

            if (param == HW_MSG_PARAM_ERROR) /*error beep*/
                new_beep_state = SIG_STATE_BEEP_ERR;
            else if (param == HW_MSG_PARAM_MOVE) /*move double beep*/
                new_beep_state = SIG_STATE_BEEP_INT;
            else if ((param == HW_MSG_PARAM_CLICK) || (param == HW_MSG_PARAM_BEEP)) /*single beep*/
                new_beep_state = SIG_STATE_BEEP_ON;
            else
                break; /*unknown parameter*/

            /*override logic for current state. priorities:
              1) error beep
              2) move beep
              3) single beep
              4) click*/
            current_beep_state  = hw_state_beep;
            current_duration    = current_beep_state & SIG_STATE_MAX_TIME;
            current_beep_state &= SIG_STATE_BEEP_ANY;

            /*1) an error beep may not be interrupted*/
            if (current_beep_state == SIG_STATE_BEEP_ERR)
                break;

            /*4) a click may not extend any signal*/
            if ((current_beep_state != SIG_STATE_INIT) &&
                (param == HW_MSG_PARAM_CLICK))
                break;

            /*2) move beep may only be interrupted by an error beep or
                 another move beep. Two move beeps can happen if the user
                 presses GO rapidly several times.*/
            if (current_beep_state == SIG_STATE_BEEP_INT)
            {
                if ((new_beep_state == SIG_STATE_BEEP_ERR) ||
                    (new_beep_state == SIG_STATE_BEEP_INT))
                    new_beep_state |= duration;
                else
                    break;
            } else if (current_duration < duration)
                /*3) single beep extending a click or another single beep*/
                new_beep_state |= duration;
            else
                new_beep_state |= current_duration;

            hw_state_beep = new_beep_state;

            /*switch on beeper*/
            Drv_SETRESET_BITS(BUZ_GPIO_BSRR, BUZZ_ODR_OUT);
        }
        break;

    default:
        break;
    }
}
