#ifndef INCLUDE_ITKPIX_EFUSE_CODEC_H
#define INCLUDE_ITKPIX_EFUSE_CODEC_H

// std/stl
#include <string>
#include <sstream> // stringstream
#include <iomanip> // setw, setfill
#include <map>

// hamming-codec
#include "hamming_codec.h"

namespace itkpix_efuse_codec {

    namespace internal {
        union EfuseInput {
            EfuseInput() : raw(0){}
            uint32_t raw;
            struct {
                unsigned int chip_serial_number : 20;
                unsigned int probe_location_id : 4;
            } fields;
        };
        static const unsigned N_BITS_EFUSE_MESSAGE{24};
    } // namespace internal

    static const std::map<std::string, uint8_t> ProbeLocationMap {
        {"UNPROBED", 0}
       ,{"BONN", 1}
       ,{"GLASGOW", 2}
       ,{"PARIS", 3}
    };

    class EfuseData {
        public :
            EfuseData(const uint32_t& efuse_data) :
                _message(efuse_data) {}
            EfuseData(const std::string& binary_str) :
                _message(0)
            {
                try {
                    _message = std::stoul(binary_str, 0, 2);
                } catch(std::exception& e) {
                    throw std::runtime_error("Invalid binary string provided to EfuseData: \"" + binary_str + "\"");
                }
            }

            const uint32_t raw() { return _message._raw; }
            const uint32_t chip_sn() { return _message.fields.chip_serial_number; }
            const uint32_t probe_location_id() { return _message.fields.probe_location_id; }
            const std::string probe_location_name() {
                for(const auto& [k, v] : ProbeLocationMap) {
                    if(v == _message.fields.probe_location_id)
                        return k;
                }
                throw std::runtime_error("Could not get probe location name from stored data");
            }

        private :
            union _efuse_layout {
                _efuse_layout(uint32_t data) : _raw(data){}
                uint32_t _raw;
                struct {
                    unsigned int chip_serial_number : 20;
                    unsigned int probe_location_id : 4;
                } fields;
            };
            _efuse_layout _message;
    };

    // decode ITkPix e-fuse data readback from a chip
    std::string decode(const uint32_t& efuse_data) {

        // expect LSB 8-bits to hold the 5 parity bits
        uint32_t parity = (0x1f & efuse_data);
        uint32_t without_parity = 0xffffff & (efuse_data >> 8);
        uint32_t efuse_data_formatted = (without_parity << 5) | parity;
        std::string decoded_binary_str = hamming_codec::decode(efuse_data_formatted
                                                            ,29
                                                            ,hamming_codec::ParityLocation::LSB
                                                            ,5);
        return decoded_binary_str;
    }

    std::string encode(const std::string& probe_location_name, const uint32_t& chip_serial_number) {

        // check probe location
        if(ProbeLocationMap.count(probe_location_name) == 0) {
            throw std::runtime_error("Invalid probe location \"" + probe_location_name + "\"");
        }
        uint32_t probe_location_id = static_cast<uint32_t>(ProbeLocationMap.at(probe_location_name));

        uint32_t efuse_input{0x0};
        efuse_input |= (0xf & probe_location_id) << 20;
        efuse_input |= (0xfffff & chip_serial_number);

        std::string encoded_binary_string = hamming_codec::encode((0xffffff & efuse_input)
                                                                    ,internal::N_BITS_EFUSE_MESSAGE
                                                                    ,hamming_codec::ParityLocation::LSB);

        int n_parity_bits = encoded_binary_string.length() - internal::N_BITS_EFUSE_MESSAGE;
        if(n_parity_bits != 5) {
            throw std::runtime_error("Invalid number of parity bits (expect: 5, got: " + std::to_string(n_parity_bits) + ")");
        }

        std::string encoding_without_parity = encoded_binary_string.substr(0, encoded_binary_string.length() - n_parity_bits);
        std::string parity_bits_str = encoded_binary_string.substr(encoded_binary_string.length() - n_parity_bits, encoded_binary_string.length());

        std::stringstream expected;
        expected << std::hex << efuse_input;
        std::stringstream received;
        received << std::hex << std::stoul(encoding_without_parity, 0, 2);
        bool equal_as_expected = (expected.str() == received.str());

        // itkpix efuses store the parity bits as 8-bit block
        std::stringstream parity_bit_block;
        parity_bit_block << std::setw(8) << std::setfill('0') << parity_bits_str;

        std::stringstream itkpix_encoded_efuse;
        itkpix_encoded_efuse << encoding_without_parity << parity_bit_block.str();

        if(itkpix_encoded_efuse.str().length() != 32) {
            throw std::runtime_error("Encoded efuse data length != 32 bits");
        }
        return itkpix_encoded_efuse.str();
    }
} // namespace itkpix_efuse_codec


#endif
