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

#ifndef EAGLE0_COMMON_BYTE_VECTOR_HPP
#define EAGLE0_COMMON_BYTE_VECTOR_HPP

#include <charconv>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <initializer_list>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

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

class byte_vector {
private:
    std::vector<uint8_t> bytes_;

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 HexCharsToByte(const std::string_view digits) -> uint8_t {
        unsigned int parsedByte = 0;
        const auto [ptr, ec] =
                std::from_chars(digits.data(), digits.data() + digits.size(), parsedByte, 16);
        if (ec != std::errc() || ptr != digits.data() + digits.size()) {
            throw HexException(ptr == digits.data() + digits.size() ? digits.back() : *ptr);
        }
        return static_cast<uint8_t>(parsedByte);
    }

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

    auto operator=(const byte_vector& rhs) -> byte_vector& = default;
    auto operator=(byte_vector&& rhs) noexcept -> byte_vector& = default;

    [[nodiscard]] auto operator==(const std::vector<uint8_t>& vec) const -> bool {
        return bytes_ == vec;
    }
    [[nodiscard]] auto operator==(const byte_vector& vec) const -> bool {
        return bytes_ == vec.bytes_;
    }

    [[nodiscard]] auto data() -> uint8_t* { return bytes_.data(); }
    [[nodiscard]] auto data() const -> const uint8_t* { return bytes_.data(); }
    [[nodiscard]] auto size() const -> size_t { return bytes_.size(); }
    [[nodiscard]] auto empty() const -> bool { return bytes_.empty(); }
    void reserve(const size_t count) { bytes_.reserve(count); }
    void resize(const size_t count) { bytes_.resize(count); }
    void swap(byte_vector& other) noexcept { bytes_.swap(other.bytes_); }

    [[nodiscard]] auto begin() -> std::vector<uint8_t>::iterator { return bytes_.begin(); }
    [[nodiscard]] auto begin() const -> std::vector<uint8_t>::const_iterator {
        return bytes_.begin();
    }
    [[nodiscard]] auto end() -> std::vector<uint8_t>::iterator { return bytes_.end(); }
    [[nodiscard]] auto end() const -> std::vector<uint8_t>::const_iterator { return bytes_.end(); }
    [[nodiscard]] auto cbegin() const -> std::vector<uint8_t>::const_iterator {
        return bytes_.cbegin();
    }
    [[nodiscard]] auto cend() const -> std::vector<uint8_t>::const_iterator {
        return bytes_.cend();
    }

    [[nodiscard]] auto operator[](const size_t index) -> uint8_t& { return bytes_[index]; }
    [[nodiscard]] auto operator[](const size_t index) const -> uint8_t { return bytes_[index]; }

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

    template<class T>
    auto append(const T* elts, const size_t count) {
        append(reinterpret_cast<const 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& {
        bytes_.push_back(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& {
        bytes_.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(static_cast<size_t>(size));
        inputFileStream.read(reinterpret_cast<char*>(this->data()), size);
        return *this;
    }

    static auto FromPath(const string& path) -> byte_vector {
        std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
        if (!inputFileStream.is_open()) {
            throw std::runtime_error("Failed to open file: " + path);
        }
        const std::streamsize size = inputFileStream.tellg();
        inputFileStream.seekg(0, std::ios::beg);
        byte_vector serializedGame(static_cast<size_t>(size));
        inputFileStream.read(reinterpret_cast<char*>(serializedGame.data()), size);
        return serializedGame;
    }

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

    static auto FromHex(const string& hexString) -> byte_vector {
        std::string_view digits(hexString);
        if (digits.starts_with("0x") || digits.starts_with("0X")) {
            digits.remove_prefix(2);
        } else if (digits.starts_with("x") || digits.starts_with("X")) {
            digits.remove_prefix(1);
        }

        if (digits.size() % 2 != 0) { throw HexException(digits.empty() ? '\0' : digits.back()); }

        byte_vector vec;
        vec.reserve(digits.size() / 2);

        for (size_t index = 0; index < digits.size(); index += 2) {
            vec += HexCharsToByte(digits.substr(index, 2));
        }
        return vec;
    }

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

inline auto operator==(const std::vector<uint8_t>& lhs, const byte_vector& rhs) -> bool {
    return rhs == lhs;
}

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

#endif  // EAGLE0_COMMON_BYTE_VECTOR_HPP
