/*
 * This file is part of the MicroPython project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2017-2021 Scott Shawcroft for Adafruit Industries
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "supervisor/shared/status_leds.h"

#include <string.h>

#include "mphalport.h"
#include "shared-bindings/microcontroller/Pin.h"
#include "supervisor/shared/tick.h"
#include "py/obj.h"


#ifdef CIRCUITPY_STATUS_LED_POWER
#include "shared-bindings/digitalio/DigitalInOut.h"
static digitalio_digitalinout_obj_t _status_power;
#endif

#ifdef MICROPY_HW_NEOPIXEL
uint8_t rgb_status_brightness = 63;
#include "shared-bindings/digitalio/DigitalInOut.h"
#include "shared-bindings/neopixel_write/__init__.h"

#ifndef MICROPY_HW_NEOPIXEL_COUNT
#define MICROPY_HW_NEOPIXEL_COUNT (1)
#endif

static uint64_t next_start_raw_ticks;
static uint8_t status_neopixel_color[3 * MICROPY_HW_NEOPIXEL_COUNT];
static digitalio_digitalinout_obj_t status_neopixel;

#elif defined(MICROPY_HW_APA102_MOSI) && defined(MICROPY_HW_APA102_SCK)
uint8_t rgb_status_brightness = 50;

#ifndef MICROPY_HW_APA102_COUNT
#define MICROPY_HW_APA102_COUNT (1)
#endif

    #define APA102_BUFFER_LENGTH (4 + 4 * MICROPY_HW_APA102_COUNT + 4)
static uint8_t status_apa102_color[APA102_BUFFER_LENGTH];

    #if CIRCUITPY_BITBANG_APA102
    #include "shared-bindings/bitbangio/SPI.h"
static bitbangio_spi_obj_t status_apa102 = {
    .base = {
        .type = &bitbangio_spi_type,
    },
};
    #else
    #include "shared-bindings/busio/SPI.h"
busio_spi_obj_t status_apa102 = {
    .base = {
        .type = &busio_spi_type,
    },
};
    #endif

#elif CIRCUITPY_PWM_RGB_LED
    #include "shared-bindings/pwmio/PWMOut.h"
    #include "shared-bindings/microcontroller/Pin.h"

pwmio_pwmout_obj_t rgb_status_r = {
    .base = {
        .type = &pwmio_pwmout_type,
    },
};
pwmio_pwmout_obj_t rgb_status_g = {
    .base = {
        .type = &pwmio_pwmout_type,
    },
};
pwmio_pwmout_obj_t rgb_status_b = {
    .base = {
        .type = &pwmio_pwmout_type,
    },
};

uint8_t rgb_status_brightness = 0xFF;

uint16_t status_rgb_color[3] = {
    0 /* red */, 0 /* green */, 0     /* blue */
};
#elif CIRCUITPY_DIGITALIO && defined(MICROPY_HW_LED_STATUS)
#include "shared-bindings/digitalio/DigitalInOut.h"
digitalio_digitalinout_obj_t single_color_led;

uint8_t rgb_status_brightness = 0xff;

#ifndef MICROPY_HW_LED_STATUS_INVERTED
#define MICROPY_HW_LED_STATUS_INVERTED (0)
#endif

#endif

#if CIRCUITPY_DIGITALIO && (defined(MICROPY_HW_LED_RX) || defined(MICROPY_HW_LED_TX))
#include "shared-bindings/digitalio/DigitalInOut.h"

#ifdef MICROPY_HW_LED_RX
digitalio_digitalinout_obj_t rx_led;
#endif

#ifdef MICROPY_HW_LED_TX
digitalio_digitalinout_obj_t tx_led;
#endif
#endif

#if CIRCUITPY_STATUS_LED
static uint32_t current_status_color = 0;
#endif

static bool status_led_init_in_progress = false;
void status_led_init() {
    if (status_led_init_in_progress) {
        // Avoid recursion.
        return;
    }
    status_led_init_in_progress = true;

    #ifdef CIRCUITPY_STATUS_LED_POWER
    common_hal_digitalio_digitalinout_construct(&_status_power, CIRCUITPY_STATUS_LED_POWER);
    common_hal_digitalio_digitalinout_switch_to_output(&_status_power,
        CIRCUITPY_STATUS_LED_POWER_INVERTED == 0, DRIVE_MODE_PUSH_PULL);
    #endif

    #ifdef MICROPY_HW_NEOPIXEL
    common_hal_digitalio_digitalinout_construct(&status_neopixel, MICROPY_HW_NEOPIXEL);
    common_hal_digitalio_digitalinout_switch_to_output(&status_neopixel, false, DRIVE_MODE_PUSH_PULL);
    #elif defined(MICROPY_HW_APA102_MOSI) && defined(MICROPY_HW_APA102_SCK)
    // Set every byte to 0xff except the start 4 bytes that make up the header.
    memset(status_apa102_color + 4, 0xff, APA102_BUFFER_LENGTH - 4);
    #if CIRCUITPY_BITBANG_APA102
    shared_module_bitbangio_spi_construct(&status_apa102,
        MICROPY_HW_APA102_SCK,
        MICROPY_HW_APA102_MOSI,
        NULL);
    #else
    if (!common_hal_busio_spi_deinited(&status_apa102)) {
        common_hal_busio_spi_deinit(&status_apa102);
    }
    common_hal_busio_spi_construct(&status_apa102,
        MICROPY_HW_APA102_SCK,
        MICROPY_HW_APA102_MOSI,
        NULL,
        false);
    #endif
    #if CIRCUITPY_BITBANG_APA102
    shared_module_bitbangio_spi_try_lock(&status_apa102);
    // Use 1MHz for clock rate. Some APA102's are spec'd 800kHz-1200kHz,
    // though many can run much faster. bitbang will probably run slower.
    shared_module_bitbangio_spi_configure(&status_apa102, 1000000, 0, 0, 8);
    #else
    common_hal_busio_spi_try_lock(&status_apa102);
    common_hal_busio_spi_configure(&status_apa102, 1000000, 0, 0, 8);
    #endif


    #elif CIRCUITPY_PWM_RGB_LED
    if (common_hal_mcu_pin_is_free(CIRCUITPY_RGB_STATUS_R)) {
        common_hal_pwmio_pwmout_construct(&rgb_status_r, CIRCUITPY_RGB_STATUS_R, 0, 50000, false);
    }

    if (common_hal_mcu_pin_is_free(CIRCUITPY_RGB_STATUS_G)) {
        common_hal_pwmio_pwmout_construct(&rgb_status_g, CIRCUITPY_RGB_STATUS_G, 0, 50000, false);
    }

    if (common_hal_mcu_pin_is_free(CIRCUITPY_RGB_STATUS_B)) {
        common_hal_pwmio_pwmout_construct(&rgb_status_b, CIRCUITPY_RGB_STATUS_B, 0, 50000, false);
    }

    #elif defined(MICROPY_HW_LED_STATUS)
    common_hal_digitalio_digitalinout_construct(&single_color_led, MICROPY_HW_LED_STATUS);
    common_hal_digitalio_digitalinout_switch_to_output(
        &single_color_led, MICROPY_HW_LED_STATUS_INVERTED == 0, DRIVE_MODE_PUSH_PULL);
    #endif

    #if CIRCUITPY_DIGITALIO && CIRCUITPY_STATUS_LED
    // Force a write of the current status color.
    uint32_t rgb = current_status_color;
    current_status_color = 0x1000000; // Not a valid color
    new_status_color(rgb);
    #endif

    status_led_init_in_progress = false;
}

void status_led_deinit() {
    #ifdef MICROPY_HW_NEOPIXEL
    // Make sure the pin stays low for the reset period. The pin reset may pull
    // it up and stop the reset period.
    while (port_get_raw_ticks(NULL) < next_start_raw_ticks) {
    }
    common_hal_reset_pin(MICROPY_HW_NEOPIXEL);

    #elif defined(MICROPY_HW_APA102_MOSI) && defined(MICROPY_HW_APA102_SCK)
    #if CIRCUITPY_BITBANG_APA102
    shared_module_bitbangio_spi_deinit(&status_apa102);
    #else
    common_hal_busio_spi_deinit(&status_apa102);
    #endif

    #elif CIRCUITPY_PWM_RGB_LED
    if (!common_hal_mcu_pin_is_free(CIRCUITPY_RGB_STATUS_R)) {
        common_hal_pwmio_pwmout_deinit(&rgb_status_r);
    }

    if (!common_hal_mcu_pin_is_free(CIRCUITPY_RGB_STATUS_G)) {
        common_hal_pwmio_pwmout_deinit(&rgb_status_g);
    }

    if (!common_hal_mcu_pin_is_free(CIRCUITPY_RGB_STATUS_B)) {
        common_hal_pwmio_pwmout_deinit(&rgb_status_b);
    }

    #elif defined(MICROPY_HW_LED_STATUS)
    common_hal_digitalio_digitalinout_deinit(&single_color_led);
    #endif

    #ifdef CIRCUITPY_STATUS_LED_POWER
    common_hal_digitalio_digitalinout_deinit(&_status_power);
    #endif
}

void new_status_color(uint32_t rgb) {
    #if CIRCUITPY_STATUS_LED
    if (current_status_color == rgb) {
        return;
    }
    uint32_t rgb_adjusted = color_brightness(rgb, rgb_status_brightness);
    current_status_color = rgb;
    #endif

    #ifdef MICROPY_HW_NEOPIXEL
    for (size_t i = 0; i < MICROPY_HW_NEOPIXEL_COUNT; i++) {
        status_neopixel_color[3 * i + 0] = (rgb_adjusted >> 8) & 0xff;
        status_neopixel_color[3 * i + 1] = (rgb_adjusted >> 16) & 0xff;
        status_neopixel_color[3 * i + 2] = rgb_adjusted & 0xff;
    }
    common_hal_neopixel_write(&status_neopixel, status_neopixel_color, 3 * MICROPY_HW_NEOPIXEL_COUNT);
    next_start_raw_ticks = port_get_raw_ticks(NULL) + 2;
    #elif defined(MICROPY_HW_APA102_MOSI) && defined(MICROPY_HW_APA102_SCK)
    for (size_t i = 0; i < MICROPY_HW_APA102_COUNT; i++) {
        // Skip 4 + offset to skip the header bytes too.
        status_apa102_color[4 * i + 4 + 1] = rgb_adjusted & 0xff;
        status_apa102_color[4 * i + 4 + 2] = (rgb_adjusted >> 8) & 0xff;
        status_apa102_color[4 * i + 4 + 3] = (rgb_adjusted >> 16) & 0xff;
    }

    #if CIRCUITPY_BITBANG_APA102
    shared_module_bitbangio_spi_write(&status_apa102, status_apa102_color, APA102_BUFFER_LENGTH);
    #else
    common_hal_busio_spi_write(&status_apa102, status_apa102_color, APA102_BUFFER_LENGTH);
    #endif

    #elif CIRCUITPY_PWM_RGB_LED
    uint8_t red_u8 = (rgb_adjusted >> 16) & 0xFF;
    uint8_t green_u8 = (rgb_adjusted >> 8) & 0xFF;
    uint8_t blue_u8 = rgb_adjusted & 0xFF;

    #ifdef CIRCUITPY_RGB_STATUS_INVERTED_PWM
    status_rgb_color[0] = (1 << 16) - 1 - ((uint16_t)(red_u8 << 8) + red_u8);
    status_rgb_color[1] = (1 << 16) - 1 - ((uint16_t)(green_u8 << 8) + green_u8);
    status_rgb_color[2] = (1 << 16) - 1 - ((uint16_t)(blue_u8 << 8) + blue_u8);
    #else
    status_rgb_color[0] = (uint16_t)(red_u8 << 8) + red_u8;
    status_rgb_color[1] = (uint16_t)(green_u8 << 8) + green_u8;
    status_rgb_color[2] = (uint16_t)(blue_u8 << 8) + blue_u8;
    #endif

    common_hal_pwmio_pwmout_set_duty_cycle(&rgb_status_r, status_rgb_color[0]);
    common_hal_pwmio_pwmout_set_duty_cycle(&rgb_status_g, status_rgb_color[1]);
    common_hal_pwmio_pwmout_set_duty_cycle(&rgb_status_b, status_rgb_color[2]);
    #elif CIRCUITPY_DIGITALIO && defined(MICROPY_HW_LED_STATUS)
    common_hal_digitalio_digitalinout_set_value(
        &single_color_led, (rgb_adjusted > 0) ^ MICROPY_HW_LED_STATUS_INVERTED);
    #endif
}

uint32_t color_brightness(uint32_t color, uint8_t brightness) {
    #if CIRCUITPY_STATUS_LED
    uint32_t result = ((color & 0xff0000) * brightness / 255) & 0xff0000;
    result += ((color & 0xff00) * brightness / 255) & 0xff00;
    result += ((color & 0xff) * brightness / 255) & 0xff;
    return result;
    #else
    return color;
    #endif
}

void set_status_brightness(uint8_t level) {
    #if CIRCUITPY_STATUS_LED
    rgb_status_brightness = level;
    // This is only called by user code and we're never controlling the status
    // LED when user code is running. So, we don't need to update the current
    // state (there is none.)
    #endif
}

uint8_t get_status_brightness(void) {
    #if CIRCUITPY_STATUS_LED
    return rgb_status_brightness;
    #else
    return 0;
    #endif
}

void init_rxtx_leds(void) {
    #if CIRCUITPY_DIGITALIO && defined(MICROPY_HW_LED_RX)
    common_hal_digitalio_digitalinout_construct(&rx_led, MICROPY_HW_LED_RX);
    common_hal_digitalio_digitalinout_switch_to_output(&rx_led, true, DRIVE_MODE_PUSH_PULL);
    common_hal_digitalio_digitalinout_never_reset(&rx_led);
    #endif
    #if CIRCUITPY_DIGITALIO && defined(MICROPY_HW_LED_TX)
    common_hal_digitalio_digitalinout_construct(&tx_led, MICROPY_HW_LED_TX);
    common_hal_digitalio_digitalinout_switch_to_output(&tx_led, true, DRIVE_MODE_PUSH_PULL);
    common_hal_digitalio_digitalinout_never_reset(&tx_led);
    #endif
}

void deinit_rxtx_leds(void) {
    #if CIRCUITPY_DIGITALIO && defined(MICROPY_HW_LED_RX)
    common_hal_digitalio_digitalinout_deinit(&rx_led);
    #endif
    #if CIRCUITPY_DIGITALIO && defined(MICROPY_HW_LED_TX)
    common_hal_digitalio_digitalinout_deinit(&tx_led);
    #endif
}

void toggle_rx_led(void) {
    #if CIRCUITPY_DIGITALIO && defined(MICROPY_HW_LED_RX)
    common_hal_digitalio_digitalinout_set_value(&rx_led, !common_hal_digitalio_digitalinout_get_value(&rx_led));
    #endif
}


void toggle_tx_led(void) {
    #if CIRCUITPY_DIGITALIO && defined(MICROPY_HW_LED_TX)
    common_hal_digitalio_digitalinout_set_value(&tx_led, !common_hal_digitalio_digitalinout_get_value(&tx_led));
    #endif
}
