/*!A cross-platform build utility based on Lua
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Copyright (C) 2015-present, Xmake Open Source Community.
 *
 * @author      ruki
 * @file        bin2c.c
 *
 */

/* //////////////////////////////////////////////////////////////////////////////////////
 * trace
 */
#define TB_TRACE_MODULE_NAME "bin2c"
#define TB_TRACE_MODULE_DEBUG (0)

/* //////////////////////////////////////////////////////////////////////////////////////
 * includes
 */
#include "prefix.h"

/* //////////////////////////////////////////////////////////////////////////////////////
 * macros
 */

#define XM_BIN2C_DATA_SIZE      (8 * 1024)
#define XM_BIN2C_LINE_SIZE      (4 * 1024)
#define XM_BIN2C_LINEWIDTH_MAX  ((XM_BIN2C_LINE_SIZE - 2) / 6)

/* //////////////////////////////////////////////////////////////////////////////////////
 * private implementation
 */

// optimized hex conversion table
static tb_char_t const *xm_binutils_bin2c_digits = "0123456789ABCDEF";

// inline hex conversion for better performance
static __tb_inline__ tb_void_t xm_binutils_bin2c_write_hex(tb_char_t *str, tb_byte_t value) {
    str[0] = ' ';
    str[1] = '0';
    str[2] = 'x';
    str[3] = xm_binutils_bin2c_digits[(value >> 4) & 15];
    str[4] = xm_binutils_bin2c_digits[value & 15];
}

static tb_bool_t xm_binutils_bin2c_dump(tb_stream_ref_t istream,
                                        tb_stream_ref_t ostream,
                                        tb_int_t        linewidth,
                                        tb_bool_t       nozeroend) {

    tb_bool_t first = tb_true;
    tb_bool_t zero_pending = tb_false;
    tb_byte_t data[XM_BIN2C_DATA_SIZE];
    tb_char_t line[XM_BIN2C_LINE_SIZE];
    tb_size_t linesize = 0;
    tb_size_t bytes_in_line = 0;
    tb_size_t data_pos = 0;
    tb_size_t data_size = 0;
    tb_assert_and_check_return_val(linewidth > 0 && linewidth <= XM_BIN2C_LINEWIDTH_MAX, tb_false);

    while (!tb_stream_beof(istream) || data_pos < data_size || zero_pending) {
        // read a large chunk of data if buffer is empty
        if (data_pos >= data_size) {
            // handle pending zero terminator
            if (zero_pending) {
                data[0] = '\0';
                data_size = 1;
                data_pos = 0;
                zero_pending = tb_false;
            } else {
                tb_hong_t left = tb_stream_left(istream);
                tb_size_t to_read = (tb_size_t)tb_min(left, (tb_hong_t)XM_BIN2C_DATA_SIZE);
                tb_check_break(to_read);

                if (!tb_stream_bread(istream, data, to_read)) {
                    break;
                }
                data_size = to_read;
                data_pos = 0;

                // check if we need to add zero terminator at the end
                if (!nozeroend && tb_stream_beof(istream)) {
                    if (data_size < XM_BIN2C_DATA_SIZE) {
                        // can add directly to current buffer
                        data[data_size++] = '\0';
                    } else {
                        // buffer is full, need to add zero in next iteration
                        zero_pending = tb_true;
                    }
                }
            }
        }

        // process bytes from buffer
        while (data_pos < data_size) {
            // check if we need a new line
            if (bytes_in_line >= (tb_size_t)linewidth) {
                // write line (tb_stream_bwrit_line will add newline automatically)
                if (tb_stream_bwrit_line(ostream, line, linesize) < 0) {
                    return tb_false;
                }

                linesize = 0;
                bytes_in_line = 0;
                first = tb_false;
            }

            // ensure we have enough space in line buffer (6 chars per byte: ", 0xXX")
            if (linesize + 6 > sizeof(line)) {
                // flush partial line if buffer is full
                if (linesize > 0) {
                    if (!tb_stream_bwrit(ostream, (tb_byte_t *)line, linesize)) {
                        return tb_false;
                    }
                    linesize = 0;
                }
            }

            // add separator
            if (bytes_in_line == 0 && first) {
                line[linesize++] = ' ';
                first = tb_false;
            } else {
                line[linesize++] = ',';
            }

            // write hex value (inline for performance)
            xm_binutils_bin2c_write_hex(line + linesize, data[data_pos]);
            linesize += 5;
            bytes_in_line++;
            data_pos++;
        }
    }

    // flush remaining line
    if (linesize > 0) {
        // write line (tb_stream_bwrit_line will add newline automatically)
        if (tb_stream_bwrit_line(ostream, line, linesize) < 0) {
            return tb_false;
        }
    }

    return tb_stream_beof(istream);
}

/* //////////////////////////////////////////////////////////////////////////////////////
 * implementation
 */

/* generate c/c++ code from the binary file
 *
 * local ok, errors = binutils.bin2c(binaryfile, outputfile, linewidth, nozeroend)
 */
tb_int_t xm_binutils_bin2c(lua_State *lua) {
    tb_assert_and_check_return_val(lua, 0);

    // get the binaryfile
    tb_char_t const *binaryfile = luaL_checkstring(lua, 1);
    tb_check_return_val(binaryfile, 0);

    // get the outputfile
    tb_char_t const *outputfile = luaL_checkstring(lua, 2);
    tb_check_return_val(outputfile, 0);

    // get line width
    tb_int_t linewidth = (tb_int_t)lua_tointeger(lua, 3);

    // no zero end?
    tb_bool_t nozeroend = (tb_bool_t)lua_toboolean(lua, 4);

    // do dump
    tb_bool_t ok = tb_false;
    tb_stream_ref_t istream = tb_stream_init_from_file(binaryfile, TB_FILE_MODE_RO);
    tb_stream_ref_t ostream = tb_stream_init_from_file(outputfile,
                                                       TB_FILE_MODE_RW | TB_FILE_MODE_CREAT | TB_FILE_MODE_TRUNC);
    do {
        if (!tb_stream_open(istream)) {
            lua_pushboolean(lua, tb_false);
            lua_pushfstring(lua, "bin2c: open %s failed", binaryfile);
            break;
        }

        if (!tb_stream_open(ostream)) {
            lua_pushboolean(lua, tb_false);
            lua_pushfstring(lua, "bin2c: open %s failed", outputfile);
            break;
        }

        if (!xm_binutils_bin2c_dump(istream, ostream, linewidth, nozeroend)) {
            lua_pushboolean(lua, tb_false);
            lua_pushfstring(lua, "bin2c: dump data failed");
            break;
        }

        ok = tb_true;
        lua_pushboolean(lua, ok);

    } while (0);

    if (istream) {
        tb_stream_exit(istream);
    }
    istream = tb_null;

    if (ostream) {
        tb_stream_exit(ostream);
    }
    ostream = tb_null;

    return ok ? 1 : 2;
}
