/*
 * Copyright (c) 2023 NVIDIA CORPORATION.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of NVIDIA CORPORATION nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/* Compiler headers */
#include <stdbool.h>                        // for true
#include <stdint.h>                         // for uint32_t
#include <stddef.h>                         // for NULL

/* Early FSP headers */
#include <misc/ct-assert.h>                 // for CT_ASSERT
#include <soc-common/hw-const.h>            /* Must appear before any hwinc files */

/* HW headers */
#include <address_map_new.h>                // for NV_ADDRESS_MAP_UTC_...
#include <arutc.h>                          // for utc registers

/* Late FSP headers */
#include <misc/nvrm_drf.h>                  // for NV_DRF* macros
#include <error/common-errors.h>            // for E_SUCCESS, error_t, FSP_...
#include <irq/safe-irqs.h>                  // for irq_safe_enable...
#include <debug/assert.h>                   // for ASSERT
#include <misc/macros.h>                    // for UNUSED

/* Module-specific FSP headers */
#include <utc/utc-mmio.h>                   // for readl, writel_...
#include <utc/sections-utc.h>               // Immune from CT_ASSERT protection
#include <utc/utc-errors.h>                 // for E_UTC_...
#include <utc/utc.h>                        // for utc_init, utc...
#include <utc/utc-port.h>                   // for utc_port_...
#include <utc/utc-priv.h>                   // for utc_id...

/*
 * Compile-time check for FSP header files
 *   Each FSP header file contains a signature unique to that file, and the
 *   FSP project.  The CT_ASSERT macro (contained in misc/ct-assert.h) can
 *   check for this signature.  If it does not exist, then the build will
 *   abort.
 *
 *   This is a trap for projects which have their own include files of the
 *   same names, but different contents.  This trap ensures that only the
 *   files from the FSP project, are built into the FSP source code.
 */
    CT_ASSERT(FSP__SOC_COMMON__HW_CONST_H, "Header file missing or invalid.")
    CT_ASSERT(FSP__ERROR__COMMON_ERRORS_H, "Header file missing or invalid.")
    CT_ASSERT(FSP__UTC__UTC_MMIO_H, "Header file missing or invalid.")
    CT_ASSERT(FSP__IRQ__SAFE_IRQS_H, "Header file missing or invalid.")

#define UTC_COMMON_BASE         NV_ADDRESS_MAP_UTC_COMMON_32BITBASE
#define UTC_COMMON_LIMIT        NV_ADDRESS_MAP_UTC_COMMON_32BITLIMIT
#define UTC_LIMIT               NV_ADDRESS_MAP_UTC_32BITLIMIT

#define COMMON_REG(x)           (UTC_COMMON_##x##_0)
#define TX_REG(x)               (UTC_TX_CLIENT_1_##x##_0)
#define RX_REG(x)               (UTC_RX_CLIENT_1_##x##_0)

#define COMMON_DRF(r, f, v)     NV_DRF_VAL(UTC_COMMON, r, f, v)
#define TX_DRF(r, f, v)         NV_DRF_VAL(UTC_TX_CLIENT_1, r, f, v)
#define RX_DRF(r, f, v)         NV_DRF_VAL(UTC_RX_CLIENT_1, r, f, v)
#define TX_DRF_NUM(r, f, n)     NV_DRF_NUM(UTC_TX_CLIENT_1, r, f, n)
#define RX_DRF_NUM(r, f, n)     NV_DRF_NUM(UTC_RX_CLIENT_1, r, f, n)

#define UTC_CLIENT_ID_MAX       16U
#define UTC_TX_FIFO_SZ          128U

/* status flags */
static bool rx_enabled = false;
static bool utc_poll   = false;
static bool utc_inited = false;

static uintptr_t rx_queue;

static inline uint32_t
utc_common_reg(const uint32_t reg)
{
    const uint32_t addr = UTC_BASE + reg;

    ASSERT((addr >= UTC_COMMON_BASE)  && (addr < UTC_COMMON_LIMIT));

    return addr;
}

static inline uint32_t
utc_client_reg(const struct utc_client *cl, uint32_t reg)
{
    const uint32_t addr = cl->base + reg;

    ASSERT((addr >= UTC_BASE)  && (addr < UTC_LIMIT));

    return addr;
}

static inline void
utc_writel(const struct utc_client *cl, uint32_t val, uint32_t reg)
{
    const uint32_t addr = utc_client_reg(cl, reg);

    writel(val, addr);
}

static inline uint32_t
utc_readl(const struct utc_client *cl, uint32_t reg)
{
    return readl(utc_client_reg(cl, reg));
}

static void
utc_tx_init(const struct utc_client *cl)
{
    uint32_t val = 0U;

    /* perform a soft reset to start with a clean slate */
    val = TX_DRF_NUM(COMMAND, SOFT_RESET, 1U);
    utc_writel(cl, val, TX_REG(COMMAND));

    /* drain any existing interrupts */
    utc_writel(cl, 0U, TX_REG(INTR_MASK));

    /* enable TX */
    val = TX_DRF_NUM(ENABLE, ENABLE, 1U);
    utc_writel(cl, val, TX_REG(ENABLE));
}

static error_t
utc_rx_init(const struct utc_client *cl)
{
    error_t  ret = E_SUCCESS;
    uint32_t val = 0U;

    /* okay to not use rx */
    if (!cl->enable_rx) {
        goto out;
    }

    ret = utc_port_rx_queue_create(&rx_queue);
    if (ret != E_SUCCESS) {
        goto out;
    }

    /* perform a soft reset to start with a clean slate */
    val = RX_DRF_NUM(COMMAND, SOFT_RESET, 1U);
    utc_writel(cl, val, RX_REG(COMMAND));

    /* drain any existing interrupts */
    utc_writel(cl, 0U, RX_REG(INTR_MASK));

    /* Enable RX_REQ interrupts */
    if (!utc_poll) {
        val = RX_DRF_NUM(INTR_MASK, RX_REQ, 1U);
        utc_writel(cl, val, RX_REG(INTR_MASK));
    }

    /* Enable RX */
    val = RX_DRF_NUM(ENABLE, ENABLE, 1U);
    utc_writel(cl, val, RX_REG(ENABLE));

    rx_enabled = true;

out:
    return ret;
}

static error_t
utc_validate_global_cfg(const struct utc_client *cl)
{
    error_t  ret = E_SUCCESS;
    uint32_t val;

    val = readl(utc_common_reg(COMMON_REG(ENABLE)));
    val = COMMON_DRF(ENABLE, ENABLE, val);
    if (val == 0U) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

out:
    return ret;
}

static inline void
utc_write_char(const struct utc_client *cl, const char ch)
{
    uint32_t fr;

    do {
        fr = utc_readl(cl, TX_REG(FIFO_STATUS));
        fr = TX_DRF(FIFO_STATUS, FULL, fr);
    } while (fr != 0U);

    utc_writel(cl, ch, TX_REG(TX_DATA));
}

static void
utc_handle_rx_req_irq(const struct utc_client *cl, bool *isr_flag)
{
    error_t  status = E_SUCCESS;
    uint32_t occupancy;
    uint32_t c_in;
    uint32_t i;

    occupancy = utc_readl(cl, RX_REG(FIFO_OCCUPANCY)) & 0xFFU;
    for (i = 0U; i < occupancy; i += 1U) {
        c_in = utc_readl(cl, RX_REG(RX_DATA)) & 0xFFU;
        status = utc_port_rx_queue_send_from_isr(rx_queue,
                                                 (char *)&c_in,
                                                 isr_flag);
        if (status != E_SUCCESS) {
            break;
        }
    }
}

static void
utc_irq_handler(void *data)
{
    const struct utc_client *cl = (const struct utc_client *)data;
    uint32_t isr;
    uint32_t rx_req;
    bool     isr_flag;

    isr = utc_readl(cl, RX_REG(INTR_STATUS));
    do {
        rx_req = RX_DRF(INTR_STATUS, RX_REQ, isr);
        if (rx_req != 0U) {
            utc_handle_rx_req_irq(cl, &isr_flag);
        }
        utc_writel(cl, isr, RX_REG(INTR_CLEAR));
        isr = utc_readl(cl, RX_REG(INTR_STATUS));
    } while (isr != 0U);

    utc_port_isr_exit(isr_flag);
}

static inline uint64_t
utc_get_elapsed_ticks(uint64_t tstart, uint64_t timeout)
{
    uint64_t tcurr = utc_port_get_elapsed_usecs(0ULL);
    uint64_t elapsed = 0UL;

    if ((tcurr - tstart) < timeout) {
        elapsed = (timeout - (tcurr - tstart));
    }

    return elapsed;
}

SECTION_UTC_TEXT error_t
utc_is_tx_ready(const struct utc_client *cl, bool *ready)
{
    error_t  ret = E_SUCCESS;
    uint32_t fr;

    if ((cl == NULL) || (ready == NULL)) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    fr = utc_readl(cl, TX_REG(FIFO_STATUS));
    fr = TX_DRF(FIFO_STATUS, FULL, fr);
    *ready = (fr == 0U);

out:
    return ret;
}

SECTION_UTC_TEXT error_t
utc_get_tx_empty_space(const struct utc_client *cl, uint8_t *count)
{
    error_t  ret = E_SUCCESS;
    uint32_t occupancy;

    if ((cl == NULL) || (count == NULL)) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    occupancy = utc_readl(cl, TX_REG(FIFO_OCCUPANCY)) & 0xFFU;
    *count = (uint8_t)(UTC_TX_FIFO_SZ - occupancy);

out:
    return ret;
}

SECTION_UTC_TEXT error_t
utc_puts(const struct utc_client *cl, const char *s)
{
    error_t ret = E_SUCCESS;

    if ((cl == NULL) || (s == NULL)) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    while (*s != '\0') {
        utc_write_char(cl, *s++);
    }

out:
    return ret;
}

SECTION_UTC_TEXT error_t
utc_putc(const struct utc_client *cl, const char ch)
{
    error_t ret = E_SUCCESS;

    if (cl == NULL) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    utc_write_char(cl, ch);

out:
    return ret;
}

SECTION_UTC_TEXT error_t
utc_write_now(const struct utc_client *cl,
              const char *buf,
              uint32_t count)
{
    error_t ret = E_SUCCESS;
    uint32_t i;

    if ((cl == NULL) || (buf == NULL)) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    if (count == 0UL) {
        goto out;
    }

    for (i = 0U; i < count; i += 1U) {
        utc_write_char(cl, *buf);
        buf++;
    }

out:
    return ret;
}

SECTION_UTC_TEXT error_t
utc_write(const struct utc_client *cl,
          const char *buf,
          uint32_t count,
          uint32_t *written,
          uint64_t timeout_us)
{
    error_t  ret = E_SUCCESS;
    uint64_t tstart;
    uint64_t timeout_left = 0UL;
    uint32_t write_cnt = 0UL;
    uint32_t fr;

    if ((cl == NULL) || (buf == NULL) || (written == NULL)) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    if (count == 0UL) {
        goto out;
    }

    tstart = utc_port_get_elapsed_usecs(0ULL);
    do {
        fr = utc_readl(cl, TX_REG(FIFO_STATUS));
        fr = TX_DRF(FIFO_STATUS, FULL, fr);
        if (fr == 0U) {
            utc_writel(cl, *buf, TX_REG(TX_DATA));
            buf++;
            write_cnt += 1UL;
        }
        timeout_left = utc_get_elapsed_ticks(tstart, timeout_us);
    } while ((write_cnt < count) && (timeout_left != 0U));

    if (timeout_left == 0U) {
        ret = E_UTC_TIMED_OUT;
    }

    *written = write_cnt;

out:
    return ret;
}

SECTION_UTC_TEXT error_t
utc_tx_flush(const struct utc_client *cl)
{
    error_t  ret = E_SUCCESS;
    uint32_t val = 0U;

    if (cl == NULL) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    val = TX_DRF_NUM(COMMAND, FLUSH, 1U);
    utc_writel(cl, val, TX_REG(COMMAND));

out:
    return ret;
}

SECTION_UTC_TEXT error_t
utc_getc(const struct utc_client *cl, char *ch)
{
    error_t  ret = E_SUCCESS;
    uint32_t fr;

    if ((cl == NULL) || (ch == NULL)) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    if (!rx_enabled) {
        ret = E_UTC_RX_DISABLED;
        goto out;
    }

    do {
        fr = utc_readl(cl, RX_REG(FIFO_STATUS));
        fr = RX_DRF(FIFO_STATUS, EMPTY, fr);
    } while (fr != 0U);

    *ch = utc_readl(cl, RX_REG(RX_DATA)) & 0xFFU;

out:
    return ret;
}

SECTION_UTC_TEXT error_t
utc_read(const struct utc_client *cl,
         char *buf,
         uint32_t count,
         uint32_t *read,
         uint64_t timeout_us)
{
    error_t  ret = E_SUCCESS;
    uint64_t tstart;
    uint64_t timeout_left = 0;
    uint32_t read_cnt = 0UL;

    if ((cl == NULL) || (buf == NULL)) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if (!utc_inited) {
        ret = E_UTC_NOT_INITED;
        goto out;
    }

    if (!rx_enabled) {
        ret = E_UTC_RX_DISABLED;
        goto out;
    }

    if (count == 0UL) {
        goto out;
    }

    tstart = utc_port_get_elapsed_usecs(0ULL);
    timeout_left = timeout_us;
    for (read_cnt = 0UL; read_cnt < count; read_cnt += 1UL) {
        ret = utc_port_rx_queue_receive(rx_queue,
                                        buf,
                                        timeout_left);
        if (ret != E_SUCCESS) {
            break;
        }
        buf++;
        timeout_left = utc_get_elapsed_ticks(tstart, timeout_us);
    }

out:
    *read = read_cnt;
    return ret;
}

SECTION_UTC_INIT_TEXT error_t
utc_client_init(const struct utc_client *cl)
{
    error_t ret = E_SUCCESS;
    uint32_t cl_base = 0U;

    if (cl == NULL) {
        ret = E_UTC_NULL_PTR;
        goto out;
    }

    if ((cl->id == 0U) || (cl->id > UTC_CLIENT_ID_MAX)) {
        ret = E_UTC_INVALID_PARAM;
        goto out;
    }

    cl_base = CL_BASE(cl->id);
    ASSERT((cl_base >= UTC_BASE) && (cl_base < UTC_LIMIT));
    ASSERT(cl_base == cl->base);

    ret = utc_validate_global_cfg(cl);
    if (ret != E_SUCCESS) {
        goto out;
    }

    if (cl->irq == UINT32_MAX) {
        utc_poll = true;
    }

    utc_tx_init(cl);
    ret = utc_rx_init(cl);
    if (ret != E_SUCCESS) {
        goto out;
    }

    if (!utc_poll) {
        ret = irq_safe_set_handler(cl->irq, utc_irq_handler, (void*)cl);
        if (ret == E_SUCCESS) {
            ret = irq_safe_enable(cl->irq);
        }
    }

    if (ret == E_SUCCESS) {
        utc_inited = true;
    }

out:
    return ret;
}
