/*
 * This file is part of the MicroPython project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2021 Kentaro Sekimoto
 *
 * 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.
 */

// The following routines come from https://github.com/Seeed-Studio/Seeed_RGB_LED_Matrix

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include "hardware/gpio.h"
#include "hardware/i2c.h"
#include "pico/binary_info.h"
#include "pico/time.h"
#include "rgblm.h"

#if defined(__GNUC__)
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wconversion"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#endif

// --- I2C ---
// #ifndef PICO_DEFAULT_I2C
// #define PICO_DEFAULT_I2C 1
// #endif
#ifndef PICO_DEFAULT_I2C_SDA_PIN
#define PICO_DEFAULT_I2C_SDA_PIN 6
#endif
#ifndef PICO_DEFAULT_I2C_SCL_PIN
#define PICO_DEFAULT_I2C_SCL_PIN 7
#endif
#ifdef DEFAULT_I2C_FREQ
#undef DEFAULT_I2C_FREQ
#endif
#define DEFAULT_I2C_FREQ (100000)

static uint8_t currentDeviceAddress;
static uint8_t baseAddress;
static uint8_t offsetAddress;
static uint8_t deviceId[12];

static inline void delay(int ms) {
    sleep_ms(ms);
}
static void i2cInit(void) {
    // This example will use i2c1 on the default SDA and SCL pins (4, 5 on a Pico)
    i2c_init(i2c1, DEFAULT_I2C_FREQ);
    gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
    gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
    gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);
    gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);
    // Make the I2C pins available to picotool
    bi_decl(bi_2pins_with_func(PICO_DEFAULT_I2C_SDA_PIN, PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C));
}

static void i2cSendByte(uint8_t address, uint8_t data) {
    i2c_write_blocking(i2c1, address, (const uint8_t *)&data, (size_t)1, false);
}

static void i2cSendBytes(uint8_t address, uint8_t *data, uint8_t len) {
    i2c_write_blocking(i2c1, address, (const uint8_t *)data, (size_t)len, false);
}

static void i2cSendContinueBytes(uint8_t address, uint8_t *data, uint8_t len) {
#define I2C_BUF_MAX 64
    uint8_t i2c_buf[I2C_BUF_MAX];
    uint8_t size = len + 1;
    uint8_t *p = (uint8_t *)&i2c_buf;
    *p++ = I2C_CMD_CONTINUE_DATA;
    if (len > I2C_BUF_MAX) {
        size = I2C_BUF_MAX;
    }
    for (int i = 0; i < size - 1; i++) {
        *p++ = *data++;
    }
    i2c_write_blocking(i2c1, address, (const uint8_t *)&i2c_buf, (size_t)size, false);
}

static void i2cReceiveBytes(uint8_t address, uint8_t *data, uint8_t len) {
    i2c_read_blocking(i2c1, address, (uint8_t *)data, (size_t)len, false);
}

uint16_t rgblm_getDeviceVID(void) {
    uint8_t data[4] = {
        0,
    };

    i2cSendByte(currentDeviceAddress, I2C_CMD_GET_DEV_ID);
    i2cReceiveBytes(currentDeviceAddress, data, 4);

    return (uint16_t)(data[0] + data[1] * 256);
}

uint16_t rgblm_getDevicePID(void) {
    uint8_t data[4] = {
        0,
    };

    i2cSendByte(currentDeviceAddress, I2C_CMD_GET_DEV_ID);
    i2cReceiveBytes(currentDeviceAddress, data, 4);

    return (uint16_t)(data[2] + data[3] * 256);
}

void rgblm_changeDeviceBaseAddress(uint8_t newAddress) {
    uint8_t data[2] = {
        0,
    };

    if (!(newAddress >= 0x10 && newAddress <= 0x70)) {
        newAddress = GROVE_TWO_RGB_LED_MATRIX_DEF_I2C_ADDR;
    }

    data[0] = I2C_CMD_SET_ADDR;
    data[1] = newAddress;
    baseAddress = newAddress;

    i2cSendBytes(currentDeviceAddress, data, 2);

    currentDeviceAddress = baseAddress + offsetAddress;
    delay(200);
}

void rgblm_defaultDeviceAddress(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_RST_ADDR);

    baseAddress = GROVE_TWO_RGB_LED_MATRIX_DEF_I2C_ADDR;
    currentDeviceAddress = baseAddress + offsetAddress;
    delay(200);
}

void rgblm_turnOnLedFlash(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_LED_ON);
}

void rgblm_turnOffLedFlash(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_LED_OFF);
}

void rgblm_enableAutoSleep(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_AUTO_SLEEP_ON);
}

void rgblm_wakeDevice(void) {
    delay(200);
}

void rgblm_disableAutoSleep(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_AUTO_SLEEP_OFF);
}

void rgblm_setDisplayOrientation(int orientation) {
    uint8_t data[2] = {
        0,
    };

    data[0] = I2C_CMD_DISP_ROTATE;
    data[1] = (uint8_t)orientation;

    i2cSendBytes(currentDeviceAddress, data, 2);
}

void rgblm_setDisplayOffset(int offset_x, int offset_y) {
    // convert to positive
    offset_x += 8;
    offset_y += 8;
    if (offset_x < 0) {
        offset_x = 0;
    } else if (offset_x > 16) {
        offset_x = 16;
    }
    if (offset_y < 0) {
        offset_y = 0;
    } else if (offset_y > 16) {
        offset_y = 16;
    }

    uint8_t data[3] = {
        0,
    };

    data[0] = I2C_CMD_DISP_OFFSET;
    data[1] = (uint8_t)offset_x;
    data[2] = (uint8_t)offset_y;

    i2cSendBytes(currentDeviceAddress, data, 3);
}

void rgblm_displayBar(uint8_t bar, uint16_t duration_time, bool forever_flag, uint8_t color) {
    uint8_t data[6] = {
        0,
    };

    data[0] = I2C_CMD_DISP_BAR;

    if (bar > 32) {
        bar = 32;
    }
    data[1] = bar;
    data[2] = (uint8_t)(duration_time & 0xff);
    data[3] = (uint8_t)((duration_time >> 8) & 0xff);
    data[4] = forever_flag;
    data[5] = color;

    i2cSendBytes(currentDeviceAddress, data, 6);
}

void rgblm_displayEmoji(uint8_t emoji, uint16_t duration_time, bool forever_flag) {
    uint8_t data[5] = {
        0,
    };

    data[0] = I2C_CMD_DISP_EMOJI;
    data[1] = emoji;
    data[2] = (uint8_t)(duration_time & 0xff);
    data[3] = (uint8_t)((duration_time >> 8) & 0xff);
    data[4] = forever_flag;

    i2cSendBytes(currentDeviceAddress, data, 5);
}

void rgblm_displayNumber(int16_t number, uint16_t duration_time, bool forever_flag,
    uint8_t color) {
    uint8_t data[7] = {
        0,
    };
    data[0] = I2C_CMD_DISP_NUM;
    data[1] = (uint8_t)((uint16_t)number & 0xff);
    data[2] = (uint8_t)(((uint16_t)number >> 8) & 0xff);
    data[3] = (uint8_t)(duration_time & 0xff);
    data[4] = (uint8_t)((duration_time >> 8) & 0xff);
    data[5] = forever_flag;
    data[6] = color;

    i2cSendBytes(currentDeviceAddress, data, 7);
}

void rgblm_displayString(char *str, uint16_t duration_time, bool forever_flag, uint8_t color) {
    uint8_t data[36] = {
        0,
    },
            len = strlen(str);

    if (len >= 28) {
        len = 28;
    }

    for (uint8_t i = 0; i < len; i++) {
        data[i + 6] = str[i];
    }

    data[0] = I2C_CMD_DISP_STR;
    data[1] = forever_flag;
    data[2] = (uint8_t)(duration_time & 0xff);
    data[3] = (uint8_t)((duration_time >> 8) & 0xff);
    data[4] = len;
    data[5] = color;

    if (len > 25) {
        i2cSendBytes(currentDeviceAddress, data, 31);
        delay(1);
        i2cSendContinueBytes(currentDeviceAddress, data + 31, (len - 25));
    } else {
        i2cSendBytes(currentDeviceAddress, data, (len + 6));
    }

}

void rgblm_displayFrames(uint8_t *buffer, uint16_t duration_time, bool forever_flag, uint8_t frames_number) {
    uint8_t data[72] = {
        0,
    };
    // max 5 frames in storage
    if (frames_number > 5) {
        frames_number = 5;
    } else if (frames_number == 0) {
        return;
    }

    data[0] = I2C_CMD_DISP_CUSTOM;
    data[1] = 0x0;
    data[2] = 0x0;
    data[3] = 0x0;
    data[4] = frames_number;

    for (int i = frames_number - 1; i >= 0; i--) {
        data[5] = i;
        for (int j = 0; j < 64; j++) {
            data[8 + j] = buffer[j + i * 64];
        }
        if (i == 0) {
            // display when everything is finished.
            data[1] = (uint8_t)(duration_time & 0xff);
            data[2] = (uint8_t)((duration_time >> 8) & 0xff);
            data[3] = forever_flag;
        }
        i2cSendBytes(currentDeviceAddress, data, 24);
        delay(1);
        i2cSendContinueBytes(currentDeviceAddress, data + 24, 24);
        delay(1);
        i2cSendContinueBytes(currentDeviceAddress, data + 48, 24);
    }
}

void rgblm_displayFrames_64(uint64_t *buffer, uint16_t duration_time, bool forever_flag, uint8_t frames_number) {
    uint8_t data[72] = {
        0,
    };
    // max 5 frames in storage
    if (frames_number > 5) {
        frames_number = 5;
    } else if (frames_number == 0) {
        return;
    }

    data[0] = I2C_CMD_DISP_CUSTOM;
    data[1] = 0x0;
    data[2] = 0x0;
    data[3] = 0x0;
    data[4] = frames_number;

    for (int i = frames_number - 1; i >= 0; i--) {
        data[5] = i;
        // different from uint8_t buffer
        for (int j = 0; j < 8; j++) {
            for (int k = 7; k >= 0; k--) {
                data[8 + j * 8 + (7 - k)] = ((uint8_t *)buffer)[j * 8 + k + i * 64];
            }
        }

        if (i == 0) {
            // display when everything is finished.
            data[1] = (uint8_t)(duration_time & 0xff);
            data[2] = (uint8_t)((duration_time >> 8) & 0xff);
            data[3] = forever_flag;
        }
        i2cSendBytes(currentDeviceAddress, data, 24);
        delay(1);
        i2cSendContinueBytes(currentDeviceAddress, data + 24, 24);
        delay(1);
        i2cSendContinueBytes(currentDeviceAddress, data + 48, 24);
    }
}

void rgblm_stopDisplay(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_DISP_OFF);
}

void rgblm_storeFrames(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_STORE_FLASH);
    delay(200);
}

void rgblm_deleteFrames(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_DELETE_FLASH);
    delay(200);
}

void rgblm_displayFramesFromFlash(uint16_t duration_time, bool forever_flag, uint8_t from, uint8_t to) {
    uint8_t data[6] = {
        0,
    };

    uint8_t temp = 0;
    // 1 <= from <= to <= 5
    if (from < 1) {
        from = 1;
    } else if (from > 5) {
        from = 5;
    }

    if (to < 1) {
        to = 1;
    } else if (to > 5) {
        to = 5;
    }

    if (from > to) {
        temp = from;
        from = to;
        to = temp;
    }

    data[0] = I2C_CMD_DISP_FLASH;
    data[1] = (uint8_t)(duration_time & 0xff);
    data[2] = (uint8_t)((duration_time >> 8) & 0xff);
    data[3] = forever_flag;
    data[4] = from - 1;
    data[5] = to - 1;

    i2cSendBytes(currentDeviceAddress, data, 6);
    delay(200);
}

void rgblm_displayColorBlock(uint32_t rgb, uint16_t duration_time, bool forever_flag) {
    uint8_t data[7] = {
        0,
    };
    data[0] = I2C_CMD_DISP_COLOR_BLOCK;
    // red green blue
    data[1] = (uint8_t)((rgb >> 16) & 0xff);
    data[2] = (uint8_t)((rgb >> 8) & 0xff);
    data[3] = (uint8_t)(rgb & 0xff);

    data[4] = (uint8_t)(duration_time & 0xff);
    data[5] = (uint8_t)((duration_time >> 8) & 0xff);
    data[6] = forever_flag;

    i2cSendBytes(currentDeviceAddress, data, 7);
}

void rgblm_displayColorBar(uint8_t bar, uint16_t duration_time, bool forever_flag) {
    uint8_t data[5] = {
        0,
    };
    data[0] = I2C_CMD_DISP_COLOR_BAR;

    if (bar > 32) {
        bar = 32;
    }
    data[1] = bar;
    data[2] = (uint8_t)(duration_time & 0xff);
    data[3] = (uint8_t)((duration_time >> 8) & 0xff);
    data[4] = forever_flag;

    i2cSendBytes(currentDeviceAddress, data, 5);
}

void rgblm_displayColorWave(uint8_t color, uint16_t duration_time, bool forever_flag) {
    uint8_t data[5] = {
        0,
    };
    data[0] = I2C_CMD_DISP_COLOR_WAVE;
    data[1] = color;
    data[2] = (uint8_t)(duration_time & 0xff);
    data[3] = (uint8_t)((duration_time >> 8) & 0xff);
    data[4] = forever_flag;

    i2cSendBytes(currentDeviceAddress, data, 5);
}

void rgblm_displayClockwise(bool is_cw, bool is_big, uint16_t duration_time, bool forever_flag) {
    uint8_t data[6] = {
        0,
    };
    data[0] = I2C_CMD_DISP_COLOR_CLOCKWISE;
    data[1] = is_cw;
    data[2] = is_big;
    data[3] = (uint8_t)(duration_time & 0xff);
    data[4] = (uint8_t)((duration_time >> 8) & 0xff);
    data[5] = forever_flag;

    i2cSendBytes(currentDeviceAddress, data, 6);
}
void rgblm_displayColorAnimation(uint8_t index, uint16_t duration_time, bool forever_flag) {
    uint8_t data[6] = {
        0,
    };
    uint8_t from, to;
    data[0] = I2C_CMD_DISP_COLOR_ANIMATION;
    switch (index) {
        case 0:
            from = 0;
            to = 28;
            break;

        case 1:
            from = 29;
            to = 41;
            break;

        case 2:  // rainbow cycle
            from = 255;
            to = 255;
            break;

        case 3:  // fire
            from = 254;
            to = 254;
            break;

        case 4:  // walking
            from = 42;
            to = 43;
            break;

        case 5:  // broken heart
            from = 44;
            to = 52;
            break;

        default:
            from = 0;
            to = 0;
            break;
    }

    data[1] = from;
    data[2] = to;
    data[3] = (uint8_t)(duration_time & 0xff);
    data[4] = (uint8_t)((duration_time >> 8) & 0xff);
    data[5] = forever_flag;

    i2cSendBytes(currentDeviceAddress, data, 6);
}

void rgblm_enableTestMode(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_TEST_TX_RX_ON);
}

void rgblm_disableTestMode(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_TEST_TX_RX_OFF);
}

uint32_t rgblm_getTestVersion(void) {
    uint8_t data[3] = {
        0,
    };

    i2cSendByte(currentDeviceAddress, I2C_CMD_TEST_GET_VER);
    i2cReceiveBytes(currentDeviceAddress, data, 3);
    return (uint32_t)(data[2] + (uint32_t)data[1] * 256 + (uint32_t)data[0] * 256 * 256);
}

void rgblm_resetDevice(void) {
}

void rgblm_getDeviceId(void) {
    i2cSendByte(currentDeviceAddress, I2C_CMD_GET_DEVICE_UID);
    i2cReceiveBytes(currentDeviceAddress, (uint8_t *)&deviceId, 12);
}

void rgblm_init(void) {
    currentDeviceAddress = GROVE_TWO_RGB_LED_MATRIX_DEF_I2C_ADDR;
    i2cInit();
}
