/*
 * LEIGH:plc
 * Copyright 2022 Alex Leigh
 * Copyright 2022 Brian Hair
 *
 * 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 <esp_log.h>
#include <esp_netif.h>
#include <cJSON.h>
#include <sys/time.h>
#include <lwip/sockets.h>

#include "dmx.h"
#include "ele.h"
#include "mecha.h"
#include "pwm.h"
#include "sacn.h"
#include "lcd.h"
#include "json.h"

#define UNIVERSE_PORT1_DISPLAY_NAME "Univ A:"
#define MULTICAST_IPV4_ADDR "239.255.0.1"
#define MULTICAST_TTL 255
#define UDP_PORT 5568

static cJSON *sacnG_field_port1_universe;
static cJSON *sacnG_field_port1_sacn_source;
struct timeval sacnG_last_sacn_packet_time;
struct timeval sacnG_packet_timeout_threshold = {
    .tv_sec = 2,   // whole seconds
    .tv_usec = 500 // microseconds
};
static int sacnGoutc = 0;
static const char *TAG = "sacn";

static void sacnF_pkt_handler(const char *buf, size_t buf_len, const char *source_ip);
static void sacnF_dmx_handler(const sacnTe131_packet *pkt);
static void sacnF_pwm_handler(const sacnTe131_packet *pkt);

/**
 * @brief Subtract the 'struct timeval' values X and Y, storing the result in RESULT.
 *
 * @param result
 * @param x
 * @param y
 */
int timeval_subtract(struct timeval *result, struct timeval *x, struct timeval *y)
{
    struct timeval xx = *x;
    struct timeval yy = *y;
    x = &xx;
    y = &yy;

    if (x->tv_usec > 999999)
    {
        x->tv_sec += x->tv_usec / 1000000;
        x->tv_usec %= 1000000;
    }

    if (y->tv_usec > 999999)
    {
        y->tv_sec += y->tv_usec / 1000000;
        y->tv_usec %= 1000000;
    }

    result->tv_sec = x->tv_sec - y->tv_sec;

    if ((result->tv_usec = x->tv_usec - y->tv_usec) < 0)
    {
        result->tv_usec += 1000000;
        result->tv_sec--; // borrow
    }

    return result->tv_sec < 0;
}

/**
 * @brief The main sACN processing task
 *
 * This task listens for sACN packets and triggers various sACN processing logic.
 *
 */
void sacnFtask()
{
    // set destination multicast addresses for sending from these sockets
    struct sockaddr_in sdestv4 = {
        .sin_family = PF_INET,
        .sin_port = htons(UDP_PORT),
    };
    // We know this inet_aton will pass because we did it above already
    inet_aton(MULTICAST_IPV4_ADDR, &sdestv4.sin_addr.s_addr);

    pwmFinit();

    netFmc_listener(sacnF_pkt_handler, &sdestv4);
}

/**
 * @brief The sACN watchdog task
 *
 * This watches to see if sACN sources have gone stale.
 *
 */
void sacnF_watchdog_task()
{
    while (1)
    {
        struct timeval current_time, time_difference;
        gettimeofday(&current_time, NULL);
        gettimeofday(&time_difference, NULL);
        // Do inital subtration between now and last marked time from an sACN packet.
        timeval_subtract(&time_difference, &current_time, &sacnG_last_sacn_packet_time);
        // We check to see if the found time difference is greater than our threshold based on
        // if our subtraction against the threshold returns a 1 for negative.
        if (timeval_subtract(&time_difference, &sacnG_packet_timeout_threshold, &time_difference))
        {
            // We've passed the defined timeout period for sACN.
            jsonFset(sacnG_field_port1_universe, ELE_FIELD_VALUE, UNIVERSE_PORT1_DISPLAY_NAME);
            jsonFset(sacnG_field_port1_sacn_source, ELE_FIELD_VALUE, "  No sACN SRC");
        }

        vTaskDelay(500 / portTICK_PERIOD_MS); // millisecond wait time
    }
}

/**
 * @brief Processes a received sACN packet
 *
 * @param buf
 * @param buf_len
 * @param source_ip
 */
static void sacnF_pkt_handler(const char *buf, size_t buf_len, const char *source_ip)
{
    // Not actually required but makes for simpler coding
    // bhair asserts this is required because it makes for simpler coding.
    const sacnTe131_packet *pkt = (const sacnTe131_packet *)buf;

    gettimeofday(&sacnG_last_sacn_packet_time, NULL);

    sacnF_dmx_handler(pkt);
    sacnF_pwm_handler(pkt);

    // Push new values to screen info panel.
    char buffer[16];
    snprintf(buffer, sizeof(buffer), UNIVERSE_PORT1_DISPLAY_NAME " %5hu", ntohs(pkt->frame.universe));
    jsonFset(sacnG_field_port1_universe, ELE_FIELD_VALUE, buffer);
    snprintf(buffer, sizeof(buffer), " %s", source_ip);
    jsonFset(sacnG_field_port1_sacn_source, ELE_FIELD_VALUE, buffer);
}

/**
 * @brief Processes a received sACN packet and emits to DMX
 *
 * @param pkt
 */
static void sacnF_dmx_handler(const sacnTe131_packet *pkt)
{
    // Emit our sACN packet to DMX.
    dmxFwrite_all(0, pkt->dmp.prop_val, 1, pkt->dmp.prop_val_cnt);
}

/**
 * @brief Processes a received sACN packet and emits to PWM
 *
 * @param pkt
 */
static void sacnF_pwm_handler(const sacnTe131_packet *pkt)
{
    // Derate the output - it's too fast.
    if (sacnGoutc >= 100)
    {
        ESP_LOGI(TAG, "sACN packet received. [universe: %u] [priority: %u] [src: %s]",
                 ntohs(pkt->frame.universe), pkt->frame.priority, pkt->frame.source_name);
        ESP_LOGI(TAG, "Channels 1-4: %u %u %u %u",
                 pkt->dmp.prop_val[1],
                 pkt->dmp.prop_val[2],
                 pkt->dmp.prop_val[3],
                 pkt->dmp.prop_val[4]);
        sacnGoutc = 0;
    }
    sacnGoutc++;

    pwmFset(pkt->dmp.prop_val[1], pkt->dmp.prop_val[2], pkt->dmp.prop_val[3], pkt->dmp.prop_val[4]);
}

void sacnFinit()
{
    ESP_LOGI(TAG, "sacnFinit(): Initializing sACN...");

    // Initialize last sacn packet time with something real and non-zero
    gettimeofday(&sacnG_last_sacn_packet_time, NULL);

    cJSON *sacnpanel = cJSON_CreateObject();
    cJSON *fields = cJSON_CreateArray();

    cJSON_AddStringToObject(sacnpanel, ELE_PANEL_TITLE, "sACN Universe(s)");
    cJSON_AddItemToObject(sacnpanel, ELE_PANEL_CONTENT, fields);

    sacnG_field_port1_universe = cJSON_CreateObject();
    cJSON_AddStringToObject(sacnG_field_port1_universe, ELE_FIELD_TITLE, "universe_port1");
    cJSON_AddStringToObject(sacnG_field_port1_universe, ELE_FIELD_VALUE, UNIVERSE_PORT1_DISPLAY_NAME);
    cJSON_AddItemToArray(fields, sacnG_field_port1_universe);

    sacnG_field_port1_sacn_source = cJSON_CreateObject();
    cJSON_AddStringToObject(sacnG_field_port1_sacn_source, ELE_FIELD_TITLE, "sacn_source");
    cJSON_AddStringToObject(sacnG_field_port1_sacn_source, ELE_FIELD_VALUE, "  No sACN SRC");
    cJSON_AddItemToArray(fields, sacnG_field_port1_sacn_source);

    int ret = lcdFlock_screen();
    assert(ret == 1);
    cJSON *screen = lcdFget_screen();
    assert(screen != NULL);
    cJSON *panels = cJSON_GetObjectItem(screen, ELE_SCREEN_CONTENT);
    assert(panels != NULL);
    cJSON_AddItemToArray(panels, sacnpanel);
    lcdFunlock_screen();
}