from functools import wraps
import ctypes
import re
import math
import glm
import os
import types
import cProfile
import hashlib
import pickle
import subprocess
import sys
import wget
import chardet
import requests
import moderngl
from geolite2 import geolite2
from _ctypes import PyObj_FromPtr

from .GlassConfig import GlassConfig

profiler = cProfile.Profile()

def run_exe(exe_path, *args):
    cmd = [exe_path, *args]
    result = subprocess.run(cmd, capture_output=True, text=True)
    stdout = result.stdout.strip()
    stderr = result.stderr.strip()
    return stdout, stderr

def cat(file_name):
    in_file = open(file_name, "r", encoding=sys.getdefaultencoding(), errors="ignore")
    content = in_file.read()
    in_file.close()
    return content

def bincat(file_name):
    in_file = open(file_name, "rb")
    content = in_file.read()
    in_file.close()
    return content

def uint64_to_uvec2(uint64_value):
    result = glm.uvec2()
    bytes_data = uint64_value.to_bytes(8, byteorder='little', signed=False)
    result.x = int.from_bytes(bytes_data[:4], byteorder='little', signed=False)
    result.y = int.from_bytes(bytes_data[4:], byteorder='little', signed=False)
    return result

def same_type(var1, var2):
    return (isinstance(var1, type(var2)) and isinstance(var2, type(var1)))

def checktype(func):
    if not GlassConfig.debug:
        return func
    
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not GlassConfig.debug:
            return func(*args, **kwargs)

        func_args = func.__code__.co_varnames[:func.__code__.co_argcount]
        annotation = func.__annotations__
        defaults = func.__defaults__
        len_func_args = len(func_args)

        len_defaults = 0
        if defaults is not None:
            len_defaults = len(defaults)
        defauts_start = len_func_args - len_defaults

        ext_kwargs = {}
        for i in range(defauts_start, len_func_args):
            ext_kwargs[func_args[i]] = defaults[i-defauts_start]
        ext_kwargs.update(kwargs)

        def change_annotation(arg_name):
            if annotation[arg_name] is None:
                annotation[arg_name] = (type(None),)

            if isinstance(annotation[arg_name], type):
                annotation[arg_name] = (annotation[arg_name], type(None))

            if float in annotation[arg_name] and \
                int not in annotation[arg_name]:
                annotation[arg_name] = (*annotation[arg_name], int)

            if None in annotation[arg_name]:
                type_set = set(annotation[arg_name])
                type_set.remove(None)
                type_set.add(type(None))
                annotation[arg_name] = tuple(type_set)

            if type(None) not in annotation[arg_name]:
                annotation[arg_name] = (*annotation[arg_name], type(None))

        for i in range(min(len(args), len_func_args)):
            arg_name = func_args[i]
            arg_value = args[i]

            if arg_name in annotation:
                if isinstance(annotation[arg_name], (type,tuple,type(None))):
                    change_annotation(arg_name)
                    if not isinstance(arg_value, annotation[arg_name]):
                        raise TypeError(func.__name__ + "() argument " + arg_name + " should be in type " + str(annotation[arg_name]) + \
                                        ", " + str(type(arg_value)) + " value passed.")
                elif isinstance(annotation[arg_name], (list,set)):
                    if arg_value not in annotation[arg_name]:
                        raise ValueError(func.__name__ + "() argument " + arg_name + " can only be in " + str(annotation[arg_name]) + \
                                         ", " + str(arg_value) + " passed.")

        for arg_name in ext_kwargs:
            arg_value = ext_kwargs[arg_name]
            if arg_name in annotation:
                if isinstance(annotation[arg_name], (type,tuple,type(None))):
                    change_annotation(arg_name)
                    if not isinstance(arg_value, annotation[arg_name]):
                        raise TypeError(func.__name__ + "() argument " + arg_name + " should be in type " + str(annotation[arg_name]) + \
                                        ", " + str(type(arg_value)) + " value passed.")
                elif isinstance(annotation[arg_name], (list,set)):
                    if arg_value not in annotation[arg_name]:
                        raise ValueError(func.__name__ + "() argument " + arg_name + " can only be in " + str(annotation[arg_name]) + \
                                         ", " + str(arg_value) + " passed.")

        return_value = func(*args, **kwargs)
        if "return" in annotation:
            if isinstance(annotation["return"], (type,tuple,type(None))):
                change_annotation("return")
                if not isinstance(return_value, annotation["return"]):
                    raise TypeError(func.__name__ + "() return value should be in type " + str(annotation["return"]) + \
                                    ", " + str(type(return_value)) + " value returned.")
            elif isinstance(annotation["return"], (list,set)):
                if return_value not in annotation["return"]:
                    raise ValueError(func.__name__ + "() return value can only be in " + str(annotation["return"]) + \
                                     ", " + str(return_value) + " returned.")

        return return_value

    return wrapper

def public_ip():
    try:
        response = requests.get('https://httpbin.org/ip')
        return response.json().get('origin')
    except:
        return None

def is_China_ip(ip_address):
    reader = geolite2.reader()
    location = reader.get(ip_address)
    country = location.get('country', {}).get('iso_code') if location else None
    return country == 'CN'

def pip_install(package_name:str):
    install_cmd = [sys.executable, "-m", "pip", "install", package_name]
    if "/" not in package_name and is_China_ip(public_ip()):
        install_cmd = [sys.executable, "-m", "pip", "install", package_name, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple"]
    
    return_code = subprocess.call(install_cmd)
    if return_code != 0:
        if "/" not in package_name:
            raise RuntimeError(f"failed to install {package_name}")

        target_file = GlassConfig.cache_folder + "/" + os.path.basename(package_name)
        wget.download(package_name, target_file)
        install_cmd = [sys.executable, "-m", "pip", "install", target_file]
        return_code = subprocess.call(install_cmd)
        if return_code != 0:
            raise RuntimeError(f"failed to install {package_name}")

def is_overridden(method):
    cls = method.__self__.__class__
    mro = cls.__mro__
    code = method.__code__
    method_name = method.__name__
    for i, base_cls in enumerate(mro):
        if method_name in base_cls.__dict__:
            base_method = base_cls.__dict__[method_name]
            if isinstance(base_method, types.FunctionType) and base_method.__code__ is not code:
                return True
    return False

def delete(method):
    @wraps(method)
    def wrapper(*args, **kwargs):
        self = args[0]
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{method.__name__}'")
    return wrapper

def extname(filename):
    return os.path.splitext(filename)[1][1:].lower()

def has_valid(content):
    its = list(re.finditer(r"\S", content))
    return its.__bool__()

def rget_token(content, i):
    token = {}

    if i < 0 or i >= len(content):
        return "", -1

    i = rskip_space(content, i)
    if i < 0:
        return "", -1

    pos_start = i
    i = rskip_valid(content, i)
    pos_end = i

    token["span"] = (pos_end+1, pos_start+1)
    token["word"] = content[pos_end+1:pos_start+1]

    return token, i

def reg_rfind(content, pattern, pos_start, pos_end):
    str_index, comment_index = get_invalid_index(content)

    its = list(re.finditer(pattern, content[pos_start:pos_end]))
    if not its:
        return -1

    for it in reversed(its):
        if it.start() not in str_index and it.start() not in comment_index:
            return it.start()

    return it.start()

def rskip_space(content, i):
    if i < 0 or i >= len(content):
        return -1

    while True:
        if i < 0 or content[i] not in " \t\r.":
            break
        i -= 1

    return i

def rskip_valid(content, i):
    if i < 0 or i >= len(content):
        return -1

    if content[i] in "+-*/%!=<>,":
        i -= 1
        return i

    str_index, comment_index = get_invalid_index(content)
    while i >= 0:
        if i not in str_index and i not in comment_index:
            if content[i] in "\n\t+-*/%!=<>,([{":
                break

            if content[i] in " \t":
                j = rskip_space(content, i)
                if j == -1:
                    break

                if content[j] not in ")]}":
                    break
                else:
                    i = rfind_pair(content, j)

            elif content[i] in ")]}":
                i = rfind_pair(content, i)

        i -= 1

    return i

def find_pair(content, i):
    len_content = len(content)
    if i < 0 or i >= len_content or content[i] not in "([{":
        return -1

    start_pair = content[i]
    end_pair = None
    if content[i] == '(':
        end_pair = ')'
    elif content[i] == '[':
        end_pair = ']'
    elif content[i] == '{':
        end_pair = '}'

    n_start_pair = 0
    str_index, comment_index = get_invalid_index(content)
    while i < len_content:
        if i not in str_index and i not in comment_index:
            if content[i] == start_pair:
                n_start_pair += 1
            elif content[i] == end_pair:
                n_start_pair -= 1

            if n_start_pair == 0:
                break

        i += 1

    return i

def is_text_file(file_path):
    try:
        with open(file_path, 'rb') as f:
            result = chardet.detect(f.read())
            return (result['encoding'] is not None)
    except:
        return False

def is_url(line:str)->bool:
    pattern = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
    return bool(pattern.match(line))

def md5s(content):
    md5_hash = hashlib.md5()
    if isinstance(content, str):
        md5_hash.update(content.encode('utf-8'))
    elif isinstance(content, (bytes,bytearray)):
        md5_hash.update(content)
    else:
        md5_hash.update(pickle.dumps(content))

    return md5_hash.hexdigest()

def modify_time(file_name):
    if not os.path.isfile(file_name):
        return 0
    
    return os.path.getmtime(file_name)

def relative_path(file_name, start_path="."):
    try:
        return os.path.relpath(file_name, start_path).replace("\\", "/")
    except:
        return os.path.abspath(file_name).replace("\\", "/")

def save_var(var, file_path):
    with open(file_path, 'wb') as file:
        pickle.dump(var, file)

def load_var(file_path):
    with open(file_path, 'rb') as file:
        var = pickle.load(file)
    return var

def subscript(var, subscript_chain, feed_index:int=None):
    for operator, operant in subscript_chain:
        if operator == "getattr":
            var = getattr(var, operant)
        else:
            used_index = (operant if operant != "{0}" else feed_index)
            var = var[used_index]

    return var

def subscript_set(var, subscript_chain, value, feed_index:int=None, compare_before_set:bool=True):
    len_chain = len(subscript_chain)
    last_index = len_chain - 1
    for i in range(len_chain):
        item = subscript_chain[i]
        operator = item[0]
        operant = item[1]
        
        if operator == "getattr":
            old_value = getattr(var, operant)
            if i != last_index:
                var = old_value
            elif not compare_before_set or old_value != value:
                setattr(var, operant, value)
        else:
            used_index = (operant if operant != "{0}" else feed_index)
            old_value = var[used_index]
            if i != last_index:
                var = old_value
            elif not compare_before_set or old_value != value:
                var[used_index] = value

def get_subscript_chain(suffix):
    subscript_chain = []
    len_suffix = len(suffix)
    pos_start = 0
    while pos_start < len_suffix:
        start_char = suffix[pos_start]
        if start_char == "." or \
        'a' <= start_char <= 'z' or \
        'A' <= start_char <= 'Z':
            if start_char == ".":
                pos_start += 1

            pos_point = suffix.find(".", pos_start)
            if pos_point == -1:
                pos_point = len_suffix

            pos_braket = suffix.find("[", pos_start)
            if pos_braket == -1:
                pos_braket = len_suffix

            pos_end = min(pos_point, pos_braket)
            attr_name = suffix[pos_start:pos_end]
            subscript_chain.append(("getattr", attr_name))
            pos_start = pos_end
        elif start_char == "[":
            pos_end = find_pair(suffix, pos_start)
            if pos_end == -1:
                pos_end = len_suffix

            index = int(suffix[pos_start+1:pos_end])
            subscript_chain.append(("getitem", index))
            pos_start = pos_end + 1

    return subscript_chain

def rfind_pair(content, i):
    len_content = len(content)
    if i < 0 or i >= len_content or content[i] not in ")]}":
        return -1

    start_pair = content[i]
    end_pair = None
    if content[i] == ')':
        end_pair = '('
    elif content[i] == ']':
        end_pair = '['
    elif content[i] == '}':
        end_pair = '{'

    n_start_pair = 0
    str_index, comment_index = get_invalid_index(content)
    while i < len_content:
        if i not in str_index and i not in comment_index:
            if content[i] == start_pair:
                n_start_pair += 1
            elif content[i] == end_pair:
                n_start_pair -= 1

            if n_start_pair == 0:
                break

        i -= 1

    return i

__str_index_dict = {}
__comment_index_dict = {}
def get_invalid_index(content):
    global __str_index_dict
    if content in __str_index_dict:
        return __str_index_dict[content], __comment_index_dict[content]

    str_index = set()
    comment_index = set()
    in_comment = False
    in_str = False
    str_start_char = None
    last_is_slash = False
    i = 0
    len_content = len(content)
    while i < len_content:
        should_add = True
        if content[i] == "'" and not last_is_slash:
            if not in_str:
                should_add = False
                in_str = True
                str_start_char = "'"
            elif str_start_char == "'":
                in_str = False
                str_start_char = None
        elif content[i] == '"' and not last_is_slash:
            if not in_str:
                should_add = False
                in_str = True
                str_start_char = '"'
            elif str_start_char == '"':
                in_str = False
                str_start_char = None
        elif content[i] in "#\\" and not in_str:
            in_comment = True
        elif content[i] == "\n" and in_comment:
            in_comment = False
        
        if content[i] == '\\' and in_str:
            last_is_slash = (not last_is_slash)
        else:
            last_is_slash = False

        if should_add:
            if in_str:
                str_index.add(i)
            if in_comment:
                comment_index.add(i)

        i += 1

    __str_index_dict[content] = str_index
    __comment_index_dict[content] = comment_index

    return str_index, comment_index


def delete_python_comments(content):
    i = 0

    str_index, _ = get_invalid_index(content)
    len_content = len(content)

    while i < len_content:
        if i not in str_index:
            if content[i] == "#":
                pos_endl = content.find("\n", i)
                if pos_endl == -1:
                    pos_endl = len_content
                content = content[:i] + content[pos_endl:]
            elif content[i] == '\\':
                pos_endl = content.find("\n", i)
                if pos_endl == -1:
                    pos_endl = len_content
                else:
                    pos_endl += 1

                content = content[:i] + content[pos_endl:]

        i += 1

    return content

def split_var_str(var_str):
    pos = len(var_str)-1
    if var_str[pos] in ")]}":
        while pos >= 0 and var_str[pos] in ")]}":
            pos = rfind_pair(var_str, pos)
            pos -= 1
            pos = rskip_space(var_str, pos)
        return var_str[:pos+1], var_str[pos+1:]
    else:
        pos = var_str.rfind(".")
        if pos != -1:
            return var_str[:pos], var_str[pos:]
        else:
            return "", ""

def di(id_var):
    return PyObj_FromPtr(id_var)

def LP_LP_c_char(str_list):
    lp_c_char = ctypes.POINTER(ctypes.c_char)
    len_str_list = len(str_list)
    result = (lp_c_char*len_str_list)()
    for i in range(len_str_list):
        result[i] = ctypes.create_string_buffer(str_list[i].encode("utf-8"))
    return result

def capacity_of(length):
    if length <= 0:
        return 2
    else:
        return int(math.pow(2, math.floor(1 + math.log2(length))))

@checktype
def quat_to_mat4(q:glm.quat):
    w = q.w
    x = q.x
    y = q.y
    z = q.z

    return glm.mat4(1-2*(y**2 + z**2), 2*(x*y + w*z), 2*(x*z - w*y), 0,
                     2*(x*y - w*z), 1-2*(x**2 + z**2), 2*(y*z + w*x), 0,
                     2*(x*z + w*y), 2*(y*z - w*x), 1-2*(x**2 + y**2), 0,
                     0, 0, 0, 1)

@checktype
def quat_to_mat3(q:glm.quat):
    w = q.w
    x = q.x
    y = q.y
    z = q.z

    return glm.mat3(1-2*(y**2 + z**2), 2*(x*y + w*z), 2*(x*z - w*y),
                     2*(x*y - w*z), 1-2*(x**2 + z**2), 2*(y*z + w*x),
                     2*(x*z + w*y), 2*(y*z - w*x), 1-2*(x**2 + y**2))

def scale_to_mat4(s:glm.vec3):
    return glm.mat4(s.x, 0  , 0  , 0,
                    0  , s.y, 0  , 0,
                    0  , 0  , s.z, 0,
                    0  , 0  , 0  , 1)

def scale_to_mat3(s:glm.vec3):
    return glm.mat3(s.x, 0  , 0  ,
                    0  , s.y, 0  ,
                    0  , 0  , s.z)

def translate_to_mat4(t:glm.vec3):
    return glm.mat4(1  , 0  , 0  , 0,
                    0  , 1  , 0  , 0,
                    0  , 0  , 1  , 0,
                    t.x, t.y, t.z, 1)