import os
import subprocess
import time
import hashlib
from filelock import FileLock, Timeout as FileLockTimeout
from feathersdk.utils.common import timediff, currtime
from feathersdk.utils.logger import debug, info
from feathersdk.utils.feathertypes import *


_LIB_DIR = os.path.dirname(os.path.abspath(__file__))
_LIB_SRC = os.path.join(_LIB_DIR, "socketcan_tcp.c")
_LIB_SO = os.path.join(_LIB_DIR, "libsocketcantcp.so")
_LIB_HASH_FILE = os.path.join(_LIB_DIR, "sct_source_hash.h")
_LIB_LOCK = os.path.join(_LIB_DIR, ".libsocketcantcp.so.lock")
_LIB_COMPILE_TIMEOUT_SECONDS = float(os.getenv("SOCKETCAN_COMPILE_TIMEOUT", "3.0"))

_START_SO_HASH_STR = b"__FEATHER_SOURCE_HASH__"
_END_SO_HASH_STR = b"__END_FEATHER_SOURCE_HASH__"


class CompilationFailedError(Exception):
    pass


def _get_files_to_hash() -> List[str]:
    path = os.path.dirname(__file__)
    valid_file = lambda f: f.endswith((".h", ".c")) and os.path.join(path, f) != _LIB_HASH_FILE
    return [os.path.join(path, f) for f in os.listdir(path) if valid_file(f)]


def _hash_file(file: str) -> str:
    """Eventually, might add more normalization to the file like removing comments"""
    with open(file, 'r') as f:
        data = f.read()
    
    hasher = hashlib.md5()
    hasher.update(data.encode("utf-8"))
    return hasher.hexdigest()


def _compile_lib(hash_str: str) -> None:
    """Compiles the library. Assumes we have file lock and are free to modify files"""
    info(f"Recompiling the socketcan_tcp library with new hash value: {hash_str}")
    
    # Place the hash val in the source code
    with open(_LIB_HASH_FILE, 'rb') as f:
        data = f.read()

    start, rest = data.split(_START_SO_HASH_STR)
    _, end = rest.split(_END_SO_HASH_STR)
    data = start + _START_SO_HASH_STR + hash_str.encode("ascii") + _END_SO_HASH_STR + end

    with open(_LIB_HASH_FILE, 'wb') as f:
        f.write(data)

    # Allow environment variable to override compile flags (useful for CI)
    compile_flags = os.getenv("SOCKETCAN_COMPILE_FLAGS", "-O3 -march=native -flto")
    cmd = ["gcc", "-fvisibility=hidden", "-shared"] + compile_flags.split() + ["-fPIC", "-o", _LIB_SO, _LIB_SRC]

    debug(f"Compiling library {_LIB_SO} with command: ", " ".join(cmd))
    try:
        subprocess.run(cmd, check=True, timeout=_LIB_COMPILE_TIMEOUT_SECONDS)
        if not os.path.exists(_LIB_SO):
            raise CompilationFailedError(f"Library {_LIB_SO} didn't exist after compilation")
    except Exception as e:
        raise e
    info(f"Library {_LIB_SO} compiled successfully")


def _needs_recompile(source_hash: str) -> bool:
    """Returns true if the file doesn't exist, or if source has changed since last recompile"""
    if not os.path.exists(_LIB_SO):
        debug(f"Library {_LIB_SO} doesn't exist, recompiling")
        return True

    if (so_hash_str := _get_binary_stored_hash()) is None:
        debug(f"Library {_LIB_SO} doesn't have hash string")
        return True

    debug(f"Library {_LIB_SO} hash value: {so_hash_str}, new hash value: {source_hash}")
    return source_hash != so_hash_str


def _get_binary_stored_hash() -> str:
    """Returns the hash string stored in the binary, or None if we couldn't find it"""
    with open(_LIB_SO, 'rb') as f:
        data = f.read()
    if _START_SO_HASH_STR not in data or _END_SO_HASH_STR not in data:
        return None
    
    return data.split(_START_SO_HASH_STR)[1].split(_END_SO_HASH_STR)[0].decode("ascii")


def _get_source_hash() -> str:
    """Returns the hash of the current source code"""
    return str(sum(int(_hash_file(f), 16) for f in _get_files_to_hash()))
        

def recompile_if_needed() -> str:
    """Recompiles library if it doesn't exist or there were changes to the file, then returns the filepath to .so:"""
    try:
        with FileLock(_LIB_LOCK, timeout=_LIB_COMPILE_TIMEOUT_SECONDS, mode=0o666):
            source_hash = _get_source_hash()
            if _needs_recompile(source_hash):
                _compile_lib(source_hash)
        
    except FileLockTimeout:
        # If we couldn't acquire the lock, another process is compiling. Wait for the library to appear and be complete
        start_time = currtime()
        while timediff(start_time) < _LIB_COMPILE_TIMEOUT_SECONDS:
            if os.path.exists(_LIB_SO):
                # Check if file size is stable (not being written)
                size1 = os.path.getsize(_LIB_SO)
                time.sleep(0.01)
                size2 = os.path.getsize(_LIB_SO)
                if size1 == size2 and size1 > 0:
                    break
            time.sleep(0.1)
        else:
            raise CompilationFailedError(f"Library was not compiled within {_LIB_COMPILE_TIMEOUT_SECONDS} seconds")
    
    return _LIB_SO
