import inspect
import ctypes
from ctypes import wintypes
from enum import IntFlag, Enum, unique

from helpers.ConfigHelper import is_windows


error_message = "'%s' function is only supported in Windows OS."

# ==========================
# Structures
# ==========================
class FILETIME(ctypes.Structure):
    _fields_ = [
        ("dwLowDateTime", wintypes.DWORD),
        ("dwHighDateTime", wintypes.DWORD),
    ]

class WIN32_FILE_ATTRIBUTE_DATA(ctypes.Structure):
    _fields_ = [
        ("dwFileAttributes", wintypes.DWORD),
        ("ftCreationTime", FILETIME),
        ("ftLastAccessTime", FILETIME),
        ("ftLastWriteTime", FILETIME),
        ("nFileSizeHigh", wintypes.DWORD),
        ("nFileSizeLow", wintypes.DWORD),
    ]

# Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
@unique
class FileAttributeConstants(IntFlag):
    __str__ = Enum.__str__
    FILE_ATTRIBUTE_PINNED = 0x00080000
    FILE_ATTRIBUTE_UNPINNED = 0x00100000
    FILE_ATTRIBUTE_ARCHIVE = 0x00000020

GetFileAttributesExW = None
GetCompressedFileSizeW = None

if is_windows():
    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

    GetFileAttributesExW = kernel32.GetFileAttributesExW
    GetFileAttributesExW.argtypes = [
        wintypes.LPCWSTR,
        ctypes.c_int,
        ctypes.POINTER(WIN32_FILE_ATTRIBUTE_DATA),
    ]
    GetFileAttributesExW.restype = wintypes.BOOL

    GetCompressedFileSizeW = kernel32.GetCompressedFileSizeW
    GetCompressedFileSizeW.argtypes = [
        wintypes.LPCWSTR,
        ctypes.POINTER(wintypes.DWORD),
    ]
    GetCompressedFileSizeW.restype = wintypes.DWORD


def get_file_attributes(path):
    if is_windows():
        data = WIN32_FILE_ATTRIBUTE_DATA()
        success = GetFileAttributesExW(path, 0, ctypes.byref(data))
        if not success:
            raise ctypes.WinError(ctypes.get_last_error())
        attributes = FileAttributeConstants(data.dwFileAttributes)
        mask = (
            FileAttributeConstants.FILE_ATTRIBUTE_PINNED |
            FileAttributeConstants.FILE_ATTRIBUTE_UNPINNED |
            FileAttributeConstants.FILE_ATTRIBUTE_ARCHIVE
        )
        return attributes & mask
    raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name)


def get_compressed_file_size(path):
    if is_windows():
        high = wintypes.DWORD(0)
        low = GetCompressedFileSizeW(path, ctypes.byref(high))

        if low == 0xFFFFFFFF:
            err = ctypes.get_last_error()
            if err != 0:
                raise ctypes.WinError(err)

        return (high.value << 32) | low
    raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name)


def resource_archived(resource_path):
    if is_windows():
        return bool(get_file_attributes(resource_path) & FileAttributeConstants.FILE_ATTRIBUTE_ARCHIVE)
    raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name)


def resource_pinned(resource_path):
    if is_windows():
        return bool(get_file_attributes(resource_path) & FileAttributeConstants.FILE_ATTRIBUTE_PINNED)
    raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name)


def resource_unpinned(resource_path):
    if is_windows():
        return bool(get_file_attributes(resource_path) & FileAttributeConstants.FILE_ATTRIBUTE_UNPINNED)
    raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name)


def is_placeholder_resource(resource_path):
    if is_windows():
        size_on_disk = get_compressed_file_size(resource_path)
        unpinned = resource_unpinned(resource_path)
        pinned = resource_pinned(resource_path)
        archived = resource_archived(resource_path)
        return (not size_on_disk or unpinned) and not (pinned and archived)
    raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name)


def is_file_downloaded(resource_path):
    if is_windows():
        size_on_disk = get_compressed_file_size(resource_path)
        pinned = resource_pinned(resource_path)
        unpinned = resource_unpinned(resource_path)
        archived = resource_archived(resource_path)
        return size_on_disk and (pinned or archived) and not unpinned
    raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name)
