import ctypes as ct
import importlib.util
import os
import platform
from pathlib import Path

import bson

from . import meta
from .typedefs import *


class wrapped_bson(ct.Structure):
    _fields_ = [
        ("value", ct.c_void_p),
        ("value_length", ct.c_size_t),
    ]

    @staticmethod
    def from_bytes(bson_bytes: bytes) -> "wrapped_bson":
        # Encode the value to bytes
        passed_buffer = malloc(len(bson_bytes))
        ct.memmove(passed_buffer, bson_bytes, len(bson_bytes))

        # Construct the result
        result = wrapped_bson()
        result.value = passed_buffer
        result.value_length = len(bson_bytes)

        return result

    @classmethod
    def from_bson(cls, bson_obj: BsonLike) -> "wrapped_bson":
        # Encode the value to bytes
        return cls.from_bytes(bson.encode(bson_obj))  # type: ignore

    def consume_into_bson(self) -> BsonDoc:
        # Get the pointed-to bytes as bytes object
        unowned_bytes = (ct.c_char * self.value_length).from_address(self.value)
        owned_bytes = bytes(unowned_bytes)

        # Parse the BSON
        return bson.decode(owned_bytes)

    def consume_into_response(self) -> BsonLike:
        result = self.consume_into_bson()
        if result["success"]:
            return result["result"]
        else:
            raise NotmongoError(result["errorMessage"])  # type: ignore


def compression_to_int(compression: Compression) -> int:
    if compression is None:
        return 0

    if compression == "zip":
        return 1

    raise ValueError(f"Unknown compression type: {compression}")


# Load the native library
def _load_so_file() -> ct.CDLL:
    # If the environment variable "NOTMONGO_SO_PATH" is set, use that
    if "NOTMONGO_SO_PATH" in os.environ:
        so_path = Path(os.environ["NOTMONGO_SO_PATH"])

        if not so_path.exists():
            raise FileNotFoundError(
                f"The path to the native library was explicitly set using the environment variable `NOTMONGO_SO_PATH`, but the file does not exist: {so_path}"
            )

    # Otherwise fetch it from the package's shared_objects directory
    else:
        SHARED_OBJECTS_DIR = Path(__file__).parent / "shared_objects"
        so_path = SHARED_OBJECTS_DIR / "libnotmongo.so"

        if not so_path.exists():
            raise FileNotFoundError(
                f"The native library was not found at the expected path: {so_path}"
            )


    return ct.CDLL(str(so_path))


SO_FILE = _load_so_file()


# Helper functions

test_function = SO_FILE.notmongo_test_function
test_function.argtypes = [ct.c_size_t]
test_function.restype = ct.c_size_t

assert test_function(1) == 2, "Test function failed"

notmongo_get_metadata = SO_FILE.notmongo_get_metadata
notmongo_get_metadata.argtypes = []
notmongo_get_metadata.restype = wrapped_bson

SO_META = notmongo_get_metadata().consume_into_bson()

_so_version = SO_META["version"]
_py_version = meta.__version__
assert (
    _so_version == _py_version
), f"The native library version does not match that of the python library: {_so_version} vs {_py_version}"

malloc = SO_FILE.notmongo_malloc
malloc.argtypes = [ct.c_size_t]
malloc.restype = ct.c_void_p

free = SO_FILE.notmongo_free
free.argtypes = [ct.c_void_p]
free.restype = None

# Database creation / destruction

db_new_empty = SO_FILE.notmongo_db_new_empty
db_new_empty.argtypes = []
db_new_empty.restype = ct.c_void_p

db_free = SO_FILE.notmongo_db_free
db_free.argtypes = [ct.c_void_p]
db_free.restype = None

# Data loading

db_populate_from_bson_value = SO_FILE.notmongo_db_populate_from_bson_value
db_populate_from_bson_value.argtypes = [ct.c_void_p, wrapped_bson]
db_populate_from_bson_value.restype = wrapped_bson

db_populate_from_bson_file = SO_FILE.notmongo_db_populate_from_bson_file
db_populate_from_bson_file.argtypes = [ct.c_void_p, ct.c_char_p, ct.c_uint32]
db_populate_from_bson_file.restype = wrapped_bson

db_populate_from_json_string = SO_FILE.notmongo_db_populate_from_json_string
db_populate_from_json_string.argtypes = [ct.c_void_p, wrapped_bson]
db_populate_from_json_string.restype = wrapped_bson

db_populate_from_json_file = SO_FILE.notmongo_db_populate_from_json_file
db_populate_from_json_file.argtypes = [ct.c_void_p, ct.c_char_p, ct.c_uint32]
db_populate_from_json_file.restype = wrapped_bson

# Data dumping

db_dump_to_json_file = SO_FILE.notmongo_db_dump_to_json_file
db_dump_to_json_file.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    ct.c_bool,
    ct.c_bool,
    ct.c_uint32,
    ct.c_uint32,
]
db_dump_to_json_file.restype = wrapped_bson

db_dump_to_json_string = SO_FILE.notmongo_db_dump_to_json_string
db_dump_to_json_string.argtypes = [ct.c_void_p, ct.c_bool, ct.c_bool]
db_dump_to_json_string.restype = wrapped_bson

db_dump_to_bson_file = SO_FILE.notmongo_db_dump_to_bson_file
db_dump_to_bson_file.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    ct.c_uint32,
    ct.c_uint32,
]
db_dump_to_bson_file.restype = wrapped_bson

db_dump_to_bson_value = SO_FILE.notmongo_db_dump_to_bson_value
db_dump_to_bson_value.argtypes = [ct.c_void_p]
db_dump_to_bson_value.restype = wrapped_bson

# Query: Database Meta

db_drop_collection = SO_FILE.notmongo_db_drop_collection
db_drop_collection.argtypes = [ct.c_void_p, ct.c_char_p]
db_drop_collection.restype = wrapped_bson

db_n_collections = SO_FILE.notmongo_db_n_collections
db_n_collections.argtypes = [ct.c_void_p]
db_n_collections.restype = wrapped_bson

db_collection_names = SO_FILE.notmongo_db_collection_names
db_collection_names.argtypes = [ct.c_void_p]
db_collection_names.restype = wrapped_bson

# Query: Collection Meta

collection_count_all_documents = SO_FILE.notmongo_collection_count_all_documents
collection_count_all_documents.argtypes = [ct.c_void_p, ct.c_char_p]
collection_count_all_documents.restype = wrapped_bson

collection_count_documents = SO_FILE.notmongo_collection_count_documents
collection_count_documents.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    wrapped_bson,
    ct.c_size_t,
    ct.c_size_t,
]
collection_count_documents.restype = wrapped_bson

# Query: Delete

collection_delete_one = SO_FILE.notmongo_collection_delete_one
collection_delete_one.argtypes = [ct.c_void_p, ct.c_char_p, wrapped_bson]
collection_delete_one.restype = wrapped_bson

collection_delete_many = SO_FILE.notmongo_collection_delete_many
collection_delete_many.argtypes = [ct.c_void_p, ct.c_char_p, wrapped_bson]
collection_delete_many.restype = wrapped_bson

# Query: Find

collection_find = SO_FILE.notmongo_collection_find
collection_find.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    wrapped_bson,
    wrapped_bson,
    ct.c_size_t,
    ct.c_size_t,
    wrapped_bson,
]
collection_find.restype = wrapped_bson

collection_find_one = SO_FILE.notmongo_collection_find_one
collection_find_one.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    wrapped_bson,
    wrapped_bson,
    ct.c_size_t,
    wrapped_bson,
]
collection_find_one.restype = wrapped_bson

# Query: Insert

collection_insert_one = SO_FILE.notmongo_collection_insert_one
collection_insert_one.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    wrapped_bson,
]
collection_insert_one.restype = wrapped_bson

collection_insert_many = SO_FILE.notmongo_collection_insert_many
collection_insert_many.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    wrapped_bson,
]
collection_insert_many.restype = wrapped_bson

# Query: Replace

collection_replace_one = SO_FILE.notmongo_collection_replace_one
collection_replace_one.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    wrapped_bson,
    wrapped_bson,
    ct.c_bool,
]
collection_replace_one.restype = wrapped_bson

# Query: Update

collection_update_one = SO_FILE.notmongo_collection_update_one
collection_update_one.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    wrapped_bson,
    wrapped_bson,
    ct.c_bool,
]
collection_update_one.restype = wrapped_bson

collection_update_many = SO_FILE.notmongo_collection_update_many
collection_update_many.argtypes = [
    ct.c_void_p,
    ct.c_char_p,
    wrapped_bson,
    wrapped_bson,
    ct.c_bool,
]
collection_update_many.restype = wrapped_bson
