//
//  byte_vector.hpp
//  eagle0
//
//  Created by Dan Crosby on 7/23/17.
//  Copyright © 2017 none. All rights reserved.
//

#ifndef byte_vector_h
#define byte_vector_h

#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>

using std::string;
using std::vector;

class byte_vector : public vector<uint8_t> {
public:
    class HexException : public std::exception {
        const std::string description;

    public:
        explicit HexException(const char n)
            : description(std::string("Unknown hex character ") + std::to_string(n)) {}

        [[nodiscard]] auto what() const noexcept -> const char* override {
            return description.c_str();
        }
    };

private:
    static auto HexNibbleToValue(const char nibble) -> uint8_t {
        if (nibble >= '0' && nibble <= '9') {
            return uint8_t(nibble - '0');
        } else if (nibble >= 'A' && nibble <= 'F') {
            return uint8_t(nibble - 'A' + 10);
        } else if (nibble >= 'a' && nibble <= 'f') {
            return uint8_t(nibble - 'a' + 10);
        }
        throw HexException(nibble);
    }

    static auto HexCharsToByte(std::istringstream& str) -> uint8_t {
        uint8_t b = 0;
        char c = 0;
        str >> c;
        b |= (HexNibbleToValue(c) << 4u);
        str >> c;
        b |= HexNibbleToValue(c);
        return b;
    }

public:
    byte_vector() = default;
    byte_vector(const std::initializer_list<uint8_t> list) : vector<uint8_t>(list) {}
    template<class InputIt>
    byte_vector(InputIt begin, InputIt end) : vector<uint8_t>(begin, end) {}
    byte_vector(const byte_vector& rhs) = default;
    explicit byte_vector(const std::string& s) : byte_vector(s.begin(), s.end()) {}
    explicit byte_vector(const size_t size) : vector<uint8_t>(size) {}
    explicit byte_vector(void* data, const size_t size) : vector<uint8_t>(size) {
        this->assign((uint8_t*)data, (uint8_t*)data + size);
    }

    auto operator==(const vector<uint8_t>& vec) -> bool {
        if (this->size() != vec.size()) return false;
        for (size_t i = 0; i < size(); i++) {
            if ((*this)[i] != vec[i]) return false;
        }
        return true;
    }

    auto append(const char* bytes, const size_t length) {
        const auto previousSize = this->size();
        this->resize(previousSize + length);
        std::memcpy(this->data() + previousSize, bytes, length);
    }

    template<class T>
    auto append(const T* elts, const size_t count) {
        append((char*)elts, count * sizeof(T));
    }

    template<class T>
    auto append(const vector<T> elts) {
        append(elts.data(), elts.size());
    }

    auto operator+=(const char* str) -> byte_vector& {
        this->operator+=(string(str));
        return *this;
    }

    auto operator+=(const uint8_t c) -> byte_vector& {
        this->insert(end(), c);
        return *this;
    }

    template<class Addable>
    auto operator+(const Addable& rhs) -> byte_vector {
        byte_vector sum = *this;
        sum += rhs;
        return sum;
    }

    template<class Iterable>
    auto operator+=(const Iterable& rhs) -> byte_vector& {
        this->insert(end(), rhs.cbegin(), rhs.cend());
        return *this;
    }

    [[nodiscard]] auto hash() const -> uint64_t {
        if (size() < 8) {
            uint64_t total = 0;
            for (size_t i = 0; i < size(); i++) {
                total <<= 8u;
                total += (*this)[i];
            }
            return total;
        }

        uint64_t firstBytes = 0;
        uint64_t lastBytes = 0;
        for (unsigned i = 0; i < 8; i++) {
            firstBytes <<= 8u;
            firstBytes += (*this)[i];
            lastBytes <<= 8u;
            lastBytes += (*this)[i];
        }

        return firstBytes ^ lastBytes ^ size();
    }

    auto ReplaceWithPath(const string& path) -> byte_vector& {
        std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
        const std::streamsize size = inputFileStream.tellg();
        inputFileStream.seekg(0, std::ios::beg);
        this->resize((size_t(size)));
        inputFileStream.read((char*)this->data(), size);
        return *this;
    }

    static auto FromPath(const string& path) -> byte_vector {
        std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
        const std::streamsize size = inputFileStream.tellg();
        inputFileStream.seekg(0, std::ios::beg);
        byte_vector serializedGame((size_t(size)));
        inputFileStream.read((char*)serializedGame.data(), size);
        return serializedGame;
    }

    static auto FromHex(const char* hexString) -> byte_vector { return FromHex(string(hexString)); }

    static auto FromHex(string hexString) -> byte_vector {
        if (hexString[0] == '0' && (hexString[1] == 'x' || hexString[1] == 'X')) {
            hexString = string(hexString.begin() + 1, hexString.end());
        }
        if (hexString[0] == 'x' || hexString[0] == 'X') {
            hexString = string(hexString.begin() + 1, hexString.end());
        }
        byte_vector vec;
        const size_t len = hexString.size();
        vec.reserve(len / 2);
        std::istringstream hexStream(hexString);

        while (size_t(hexStream.tellg()) < len) { vec += HexCharsToByte(hexStream); }
        return vec;
    }

    explicit operator std::string() const { return std::string(begin(), end()); }
};

inline auto operator<<(std::ostream& ostr, byte_vector vec) -> std::ostream& {
    ostr.write((char*)vec.data(), (std::streamsize)vec.size());
    return ostr;
}

#endif /* byte_vector_h */
