/*
 * Copyright (c) 2013-2021 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 *
 * This software is available to you under a choice of one of two
 * licenses.  You may choose to be licensed under the terms of the GNU
 * General Public License (GPL) Version 2, available from the file
 * COPYING in the main directory of this source tree, or the
 * OpenIB.org BSD license below:
 *
 *     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.
 *
 * 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 <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <bit_slice.h>
#include <common/tools_utils.h>

#include "mtcr_tools_cif.h"

#ifdef MST_UL
#include <mtcr_int_defs.h>
#include <mtcr_mf.h>
#endif

#define TOOLS_HCR_ADDR      0x80780
#define CR_MBOX_ADDR        0xe0000
#define CMD_IF_SIZE         28
#define CMD_IF_WAIT_GO      2000

#define TOOLS_HCR_SEM       0xf03bc // sem47
#define TOOLS_SEM_TRIES     1024     // Number of tries to obtain a TOOLS sem.

#define MBOX_WRITE_OP       0x70
#define MBOX_READ_OP        0x71
#define REG_ACCESS_OP       0x3b

#define MREAD4_ADV(offs, val_ptr, action_on_fail)   do { \
        if (mread4(mf, offs, val_ptr) != 4) { \
            /*fprintf(stderr, "-E- Cr read (0x%08x) failed: %s(%d)\n", (u_int32_t)(offs), strerror(errno), (u_int32_t)errno);*/ \
            action_on_fail; \
        } \
        /*printf("-D- %s:%d mread4: offs = %#x, val = %#x\n", __FUNCTION__, __LINE__, offs, val);*/ \
} while (0)

#define MWRITE4_ADV(offs, val, action_on_fail)  do { \
        if (mwrite4(mf, offs, val) != 4) { \
            /*fprintf(stderr, "-E- Cr write (0x%08x, 0x%08x) failed: %s(%d)\n", (u_int32_t)(offs), (u_int32_t)(val), strerror(errno), (u_int32_t)errno);*/ \
            action_on_fail; \
        } \
        /*printf("-D- %s:%d mwrite4: offs = %#x, val = %#x\n",   __FUNCTION__, __LINE__, offs, val);*/ \
} while (0)
#define MREAD4(offs, val_ptr) MREAD4_ADV(offs, val_ptr, return ME_CR_ERROR)
#define MWRITE4(offs, val) MWRITE4_ADV(offs, val, return ME_CR_ERROR)


#define TOCPUn(s, n) do {                                          \
        u_int32_t ii;                                                   \
        u_int32_t *p = (u_int32_t*)(s);                               \
        for (ii = 0; ii < (n); ii++, p++)                         \
            *p = __be32_to_cpu(*p);                                    \
} while (0)

#if __BYTE_ORDER == __BIG_ENDIAN

#define SWAP_DW_BE(uint64_num) \
    (((uint64_num) & 0xffffffffULL) << 32) | (((uint64_num) >> 32) & 0xffffffffULL)

#else
#define SWAP_DW_BE(uint64_num) (uint64_num)
#endif

typedef struct tools_cmdif_t {
    u_int32_t in_param_h;
    u_int32_t in_param_l;
    u_int32_t out_param_h;
    u_int32_t out_param_l;
    u_int32_t input_modifier;
    u_int16_t token;
    u_int16_t opcode;
    u_int8_t opcode_modifier;
    u_int8_t t;
    u_int8_t e;
    u_int8_t go;
    u_int8_t status;
} tools_cmdif;


static int translate_status(int status)
{
    switch (status) {
    case 0x0:
        return ME_OK;

    case 0x1:
        return ME_CMDIF_BUSY;

    case 0x2:
        return ME_CMDIF_BAD_OP;

    case 0x3:
        return ME_CMDIF_UNKN_TLV;

    case 0x4:
        return ME_CMDIF_BAD_SYS;

    case 0x9:
        return ME_CMDIF_RES_STATE;

    default:
        //printf("-D- Unknown status: 0x%x\n", status);
        return ME_CMDIF_UNKN_STATUS;
    }
}

extern void mpci_change(mfile *mf);

static void tools_cmdif_pack(tools_cmdif *cmd, u_int32_t *buf)
{
    memset((char*)buf, 0, CMD_IF_SIZE);
    buf[0] = cmd->in_param_h;
    buf[1] = cmd->in_param_l;
    buf[2] = cmd->input_modifier;
    // out h
    // out l
    buf[5] = MERGE(buf[5], cmd->token,               16, 16);
    buf[6] = MERGE(buf[6], cmd->opcode,               0, 12);
    buf[6] = MERGE(buf[6], cmd->opcode_modifier,     12,  4);
    buf[6] = MERGE(buf[6], cmd->e,                   22,  1);
}

static void tools_cmdif_unpack(tools_cmdif *cmd, u_int32_t *buf)
{
    memset(cmd, 0, sizeof(tools_cmdif));

    cmd->in_param_h       = buf[0];
    cmd->in_param_l       = buf[1];

    cmd->input_modifier = buf[2];
    cmd->out_param_h      = buf[3];
    cmd->out_param_l      = buf[4];
    cmd->opcode         = EXTRACT(buf[6], 0, 12);
    cmd->opcode_modifier = EXTRACT(buf[6], 12, 4);
    cmd->status         = EXTRACT(buf[6], 24, 8);
}

static int tools_cmdif_flash_lock(mfile *mf, int lock_state)
{
    // Obtain Semaphore
    u_int32_t cnt = 0;
    u_int32_t word = 0;
    if (lock_state) {
        do {
            if (++cnt > TOOLS_SEM_TRIES) {
                //printf("-E- Can not obtain Flash semaphore");
                return ME_SEM_LOCKED;
            }
            MREAD4(TOOLS_HCR_SEM, &word);
            if (word) {
                msleep(rand() % 5);
            }
        } while (word);
    } else {
        MWRITE4(TOOLS_HCR_SEM, 0);
    }

    return ME_OK;
}

int tools_cmdif_unlock_semaphore(mfile *mf)
{
    return tools_cmdif_flash_lock(mf, 0);
}

static int tools_cmdif_wait_go(mfile *mf, int *retries)
{
    int i;
    u_int8_t go_bit;
    int wait = 1;

    for (i = 0; i < CMD_IF_WAIT_GO; i++) {
        u_int32_t word = 0;
        MREAD4(TOOLS_HCR_ADDR + CMD_IF_SIZE - 4, &word);
        go_bit = EXTRACT(word, 23, 1);
        if (!go_bit) {
            if (retries) {
                *retries = i;
            }
            return 0;
        }
        if (i > 5) { // after some iteration put sleeps bwtween busy-wait
            msleep(wait);  // don't hog the cpu with busy-wait
            if (wait < 8) {
                wait *= 2;    // exponential backoff - up-to 8ms between polls
            }
        }
    }
    return ME_CMDIF_BUSY;
}

static int tools_cmdif_send_cmd_int(mfile *mf, tools_cmdif *cmd)
{
    u_int32_t raw_cmd[CMD_IF_SIZE / 4];
    int act_retries;
    int rc;

    // Check if the go BIT is ready
    rc = tools_cmdif_wait_go(mf, NULL);
    if (rc) {
        //printf("tools_cmdif_send: GO bit set before command issued (rc=%d)\n", rc);
        return ME_CMDIF_BUSY;
    }
    // Prepare the date of the command we're gonna execute
    tools_cmdif_pack(cmd, raw_cmd);
    if (mwrite4_block(mf, TOOLS_HCR_ADDR, raw_cmd, CMD_IF_SIZE) != CMD_IF_SIZE) {
        return ME_CR_ERROR;
    }

    raw_cmd[6] = MERGE(raw_cmd[6],      1,                   23,  1); // go
    MWRITE4(TOOLS_HCR_ADDR + 24, raw_cmd[6]);

    rc = tools_cmdif_wait_go(mf, &act_retries);

    if (rc) {
        return ME_CMDIF_TOUT;
    }

    if (mread4_block(mf, TOOLS_HCR_ADDR, raw_cmd, CMD_IF_SIZE) != CMD_IF_SIZE) {
        return ME_CR_ERROR;
    }

    tools_cmdif_unpack(cmd, raw_cmd);

    if (cmd->status) {
        //printf("-E- CMD IF Bad status. Op: 0x%x Status: 0x%x\n", cmd->opcode, cmd->status);
        return ME_CMDIF_BAD_STATUS; // TODO - Special return code here - needs a specific attention
    }
    return ME_OK;
}

int tools_cmdif_send_inline_cmd_int(mfile *mf,
                                    u_int32_t in_param[2],
                                    u_int32_t out_param[2],
                                    u_int32_t input_modifier,
                                    u_int16_t opcode,
                                    u_int8_t opcode_modifier)
{
    if (!mf) {
        return ME_BAD_PARAMS;
    }
    tools_cmdif cmdif;
    memset(&cmdif, 0, sizeof(tools_cmdif));
    cmdif.in_param_l = in_param[0];
    cmdif.in_param_h = in_param[1];
    cmdif.input_modifier = input_modifier;
    cmdif.opcode = opcode;
    cmdif.opcode_modifier = opcode_modifier;

    //take semaphore
    mpci_change(mf);
    if (tools_cmdif_flash_lock(mf, 1)) {
        mpci_change(mf);
        return ME_SEM_LOCKED;
    }
    int rc = tools_cmdif_send_cmd_int(mf, &cmdif);
    // release it
    tools_cmdif_flash_lock(mf, 0);
    mpci_change(mf);
    if (out_param) {
        out_param[0] = cmdif.out_param_l;
        out_param[1] = cmdif.out_param_h;
    }
    if (rc || cmdif.status) {
        return (rc != ME_CMDIF_BAD_STATUS) ? rc : translate_status(cmdif.status);
    }
    return rc;
}

int tools_cmdif_send_inline_cmd(mfile *mf,
                                u_int64_t in_param,
                                u_int64_t *out_param,
                                u_int32_t input_modifier,
                                u_int16_t opcode,
                                u_int8_t opcode_modifier)
{
    int rc;
    in_param = SWAP_DW_BE(in_param);
    rc = tools_cmdif_send_inline_cmd_int(mf, (u_int32_t*)((u_int8_t*)&in_param), (u_int32_t*)out_param, input_modifier, opcode, opcode_modifier);
    if (out_param) {
        u_int64_t swapped_out_param = SWAP_DW_BE(*out_param);
        *out_param = swapped_out_param;
    }
    return rc;
}

// read from mailbox method. output[2] is in BigEndian
static int tools_cmdif_mbox_read(mfile *mf, u_int32_t offset, u_int32_t output[2])
{
    if (!mf || (offset & 0x1) != 0 || !output) {// offset should be quad word aligned (i.e only even dwords)
        return ME_BAD_PARAMS;
    }
    tools_cmdif cmdif;
    memset(&cmdif, 0, sizeof(tools_cmdif));
    //printf("-D- offset: 0x%x\n", offset>>2);
    cmdif.input_modifier = offset; // offset is in dwords
    cmdif.opcode = MBOX_READ_OP;

    int rc = tools_cmdif_send_cmd_int(mf, &cmdif);
    // swap endianess because tools_cmdif_send_cmd_int returns output in CPU endianess
    output[0] = __be32_to_cpu(cmdif.out_param_l);
    output[1] = __be32_to_cpu(cmdif.out_param_h);
    if (rc || cmdif.status) {
        return (rc != ME_CMDIF_BAD_STATUS) ? rc : translate_status(cmdif.status);
    }
    //printf("-D- mbox read OK\n");
    return ME_OK;
}

// write to mailbox method. input[2] is in BigEndian
static int tools_cmdif_mbox_write(mfile *mf, u_int32_t offset, u_int32_t input[2])
{
    if (!mf || (offset & 0x1) != 0) {// offset should be quad word aligned (i.e only even dwords)
        return ME_BAD_PARAMS;
    }
    tools_cmdif cmdif;
    memset(&cmdif, 0, sizeof(tools_cmdif));
    // swap endianess because tools_cmdif_send_cmd_int assumes input in CPU endianess
    cmdif.in_param_l = __be32_to_cpu(input[0]);
    cmdif.in_param_h = __be32_to_cpu(input[1]);
    cmdif.input_modifier = offset; // offset is in dwords
    cmdif.opcode = MBOX_WRITE_OP;
    int rc = tools_cmdif_send_cmd_int(mf, &cmdif);

    if (rc || cmdif.status) {
        return (rc != ME_CMDIF_BAD_STATUS) ? rc : translate_status(cmdif.status);
    }
    return ME_OK;
}

static int tools_cmdif_cr_mbox_write(mfile *mf, u_int32_t offset, u_int8_t *data, int size)
{
    if (!mf || !data || (offset + size > TOOLS_HCR_MAX_MBOX)) {
        return ME_BAD_PARAMS;
    }
    if (mwrite_buffer(mf, CR_MBOX_ADDR + offset, data, size) != size) {
        return ME_CR_ERROR;
    }
    return ME_OK;
}

static int tools_cmdif_cr_mbox_read(mfile *mf, u_int32_t offset, u_int8_t *data, int size)
{
    if (!mf || !data || (offset + size > TOOLS_HCR_MAX_MBOX)) {
        return ME_BAD_PARAMS;
    }
    if (mread_buffer(mf, CR_MBOX_ADDR + offset, data, size) != size) {
        return ME_CR_ERROR;
    }
    return ME_OK;
}

int tools_cmdif_is_supported(mfile *mf)
{
    int rc = ME_OK;
    // run mailbox write cmd (read command fails after driver restart or internal reset)
    u_int32_t writebuf[2] = {0};
    if (!mf) {
        return ME_BAD_PARAMS;
    }
    mpci_change(mf);
    // take semaphore
    if (tools_cmdif_flash_lock(mf, 1)) {
        rc = ME_SEM_LOCKED;
        goto cleanup;
    }
    rc = tools_cmdif_mbox_write(mf, 0, writebuf);
    if (rc) {
        tools_cmdif_flash_lock(mf, 0);
        goto cleanup;
    }
    tools_cmdif_flash_lock(mf, 0);
cleanup:
    mpci_change(mf);
    return rc;
}

#define MAGIC 0xbadb00f
int tools_cmdif_is_cr_mbox_supported(mfile *mf)
{
    int rc;
    u_int32_t val = 0;
    mpci_change(mf);
    if ((rc = tools_cmdif_flash_lock(mf, 1))) {
        goto cleanup_no_sem;
    }
    // attempt to write/read from cr-mbox
    MWRITE4_ADV(CR_MBOX_ADDR, MAGIC, rc = ME_CR_ERROR; goto cleanup);
    MREAD4_ADV(CR_MBOX_ADDR, &val, rc = ME_CR_ERROR; goto cleanup);
cleanup:
    tools_cmdif_flash_lock(mf, 0);
cleanup_no_sem:
    mpci_change(mf);
    if (rc) {
        return rc;
    }
    return val == MAGIC ? ME_OK : ME_CMDIF_NOT_SUPP;
}

#define COMPLEMENT_TO_QUAD_ALIGNED(byte_sz) \
    ((byte_sz) + ((8 - ((byte_sz) & 7) == 8) ? 0 : (8 - ((byte_sz) & 7))))

int tools_cmdif_send_mbox_command_int(mfile *mf,
                                      u_int32_t input_modifier,
                                      u_int16_t opcode,
                                      u_int8_t opcode_modifier,
                                      int data_offs_in_mbox,
                                      void *data,
                                      int write_data_size,
                                      int read_data_size,
                                      int skip_write,
                                      int use_cr_mbox)
{
    int read_data_size_quad_aligned =  COMPLEMENT_TO_QUAD_ALIGNED(read_data_size);
    int write_data_size_quad_aligned =  COMPLEMENT_TO_QUAD_ALIGNED(write_data_size);
    int rc,
        i;
    /*printf("-D- opcode: 0x%x, opcode mod: 0x%x, data_offs_in_mbox: 0x%x, write_data_size: 0x%x(0x%x), read_data_size 0x%x(0x%x), skip_write: %d\n",\
             opcode, opcode_modifier, data_offs_in_mbox, write_data_size, write_data_size_quad_aligned, read_data_size, read_data_size_quad_aligned, skip_write);*/
    // check params
    if (!mf || !data || data_offs_in_mbox < 0 || (data_offs_in_mbox & 7) != 0 || \
        data_offs_in_mbox + read_data_size_quad_aligned > TOOLS_HCR_MAX_MBOX || \
        data_offs_in_mbox + write_data_size_quad_aligned > TOOLS_HCR_MAX_MBOX) {
        return ME_BAD_PARAMS;
    }
    mpci_change(mf);
    //take semaphore
    if (tools_cmdif_flash_lock(mf, 1)) {
        mpci_change(mf);
        return ME_SEM_LOCKED;
    }

    // write to mailbox if needed
    u_int8_t mailbox[TOOLS_HCR_MAX_MBOX] = {0};
    if (!skip_write) {
        // place the data inside 256 bytes buffer (init with zeroes) and write the entire buffer to mbox
        // it is required in the TOOLS_HCR HLD to write the ENTIRE MBOX

        memcpy(&(mailbox[data_offs_in_mbox]), data, write_data_size);

        if (use_cr_mbox == 1) {
            //write mbox to virtual cr-space
            rc = tools_cmdif_cr_mbox_write(mf, 0x0, mailbox, TOOLS_HCR_MAX_MBOX);
            if (rc) {
                goto cleanup;
            }
        } else {
            int i;
            for (i = 0; i < TOOLS_HCR_MAX_MBOX; i += 8) { // it is required to write in quad word chunks
                rc = tools_cmdif_mbox_write(mf, i / 4, (u_int32_t*)&mailbox[i]);
                if (rc) {
                    goto cleanup;
                }
            }
        }
    }
    // send cmd
    tools_cmdif cmdif;
    memset(&cmdif, 0, sizeof(tools_cmdif));
    cmdif.opcode = opcode;
    cmdif.opcode_modifier = opcode_modifier;
    cmdif.input_modifier = input_modifier;
    rc = tools_cmdif_send_cmd_int(mf, &cmdif);

    if (rc || cmdif.status) {
        if (rc == ME_CMDIF_BAD_STATUS) {
            rc = translate_status(cmdif.status); // means that driver is down or we dont support the extended version of tools hcr.
        }
        goto cleanup;
    }
    // read from mbox
    // read read_data_size bytes from mbox and update our data
    if (use_cr_mbox == 1) {
        // read mailbox from virtual CR-space
        rc = tools_cmdif_cr_mbox_read(mf, data_offs_in_mbox, &mailbox[data_offs_in_mbox], read_data_size_quad_aligned);
        if (rc) {
            goto cleanup;
        }
    } else {
        for (i = data_offs_in_mbox; i < (data_offs_in_mbox + read_data_size_quad_aligned); i += 8) { // it is required to write in quad word chunks (64bits each time)
            rc = tools_cmdif_mbox_read(mf, i / 4, (u_int32_t*)&(mailbox[i]));
            if (rc) {
                goto cleanup;
            }
        }
    }
    // copy data back to user
    memcpy(data, &(mailbox[data_offs_in_mbox]), read_data_size);
    rc = ME_OK;
cleanup:
    tools_cmdif_flash_lock(mf, 0);
    mpci_change(mf);
    return rc;
}


int tools_cmdif_reg_access(mfile *mf, void *data, int write_data_size, int read_data_size)
{
    int rc;
    if (mf->hcr_params.supp_cr_mbox == 0) {
        rc = tools_cmdif_is_cr_mbox_supported(mf);
        if (rc == ME_OK) {
            mf->hcr_params.supp_cr_mbox = 1;
        } else if (rc == ME_CMDIF_NOT_SUPP) {
            mf->hcr_params.supp_cr_mbox = -1;
        } else {
            // CR error or Semaphore locked
            return rc;
        }
    }
    if (mf->hcr_params.supp_cr_mbox == 1) {
        rc = tools_cmdif_send_mbox_command_int(mf, 1, REG_ACCESS_OP, 0, \
                                               0, data, write_data_size, read_data_size, 0, 1);
    } else {
        rc = tools_cmdif_send_mbox_command_int(mf, 0, REG_ACCESS_OP, 0, \
                                               0, data, write_data_size, read_data_size, 0, 0);
    }
    return rc;
}


int tools_cmdif_send_mbox_command(mfile *mf,
                                  u_int32_t input_modifier,
                                  u_int16_t opcode,
                                  u_int8_t opcode_modifier,
                                  int data_offs_in_mbox,
                                  void *data,
                                  int data_size,
                                  int skip_write)
{

    return tools_cmdif_send_mbox_command_int(mf, input_modifier, opcode, opcode_modifier, \
                                             data_offs_in_mbox, data, data_size, \
                                             data_size, skip_write, 0);
}

