import json
import math
import os
import shutil
import threading
import zipfile
from pathlib import Path

from PIL import Image
from django.core.files import File
from nbt import nbt

from CMSBlueprintStation.settings import BASE_DIR, STATIC_URL
from home.models import Tag, Assets, Record

STATIC_ROOT = BASE_DIR / 'static'

thread_lock = threading.Lock()


def update_assets(user):
    with thread_lock:
        try:
            files = (BASE_DIR / 'tmp').iterdir()
            if not any(files):
                with zipfile.ZipFile(BASE_DIR / "tmp_mod_file", 'r') as zip_ref:
                    for file_info in zip_ref.infolist():
                        if file_info.filename.startswith('assets/'):
                            zip_ref.extract(file_info, BASE_DIR / 'tmp')
            create_count = update_count = copy_count = item_update_count = item_create_count = 0
            files = (BASE_DIR / 'tmp' / 'assets').iterdir()
            for mod_assets in files:
                lang = BASE_DIR / 'tmp' / 'assets' / mod_assets / 'lang' / 'zh_cn.json'
                if not lang.exists():
                    lang = BASE_DIR / 'tmp' / 'assets' / mod_assets / 'lang' / 'en_us.json'
                if lang.exists():
                    with lang.open(encoding='utf-8') as f:
                        lang_json = json.load(f)
                        for file in (BASE_DIR / 'tmp' / 'assets' / mod_assets / 'models' / 'item').glob('*.json'):
                            name = f"{mod_assets.name}.{file.name.removesuffix('.json')}"
                            if f"item.{name}" in lang_json:
                                full_name = f"item.{name}"
                                _, create = Tag.objects.update_or_create(sec_name=full_name, type='item',
                                                                         defaults={'name': lang_json[full_name]})
                                if create:
                                    item_create_count += 1
                                else:
                                    item_update_count += 1
                            elif f"block.{name}" in lang_json:
                                full_name = f"block.{name}"
                                _, create = Tag.objects.update_or_create(sec_name=full_name, type='item',
                                                                         defaults={'name': lang_json[full_name]})
                                if create:
                                    item_create_count += 1
                                else:
                                    item_update_count += 1
                for folder in ['models', 'blockstates', 'textures']:
                    for dir_path, dir_names, files in os.walk(BASE_DIR / 'tmp' / 'assets' / mod_assets / folder):
                        dir_path = Path(dir_path)
                        for file in files:
                            if file.endswith('.json'):
                                assets_file = dir_path / file
                                relpath = assets_file.relative_to(BASE_DIR / 'tmp' / 'assets')
                                with assets_file.open() as f:
                                    assets_json = json.load(f)
                                    _, create = Assets.objects.update_or_create(name=str(relpath).replace('/', '\\'),
                                                                                defaults={'file': assets_json})
                                    if create:
                                        create_count += 1
                                    else:
                                        update_count += 1
                            elif file.endswith('.png') or file.endswith('obj') or file.endswith('mtl'):
                                if file.endswith('.png'):
                                    assets_file = dir_path / file
                                    # 如果有同名的 .mcmeta 文件，则裁剪为正方形
                                    mcmeta_file = assets_file.with_suffix('.png.mcmeta')
                                    if mcmeta_file.exists():
                                        with Image.open(assets_file) as img:
                                            width, height = img.size
                                            if height > width:
                                                # 裁剪成正方形，保留顶部部分
                                                img = img.crop((0, 0, width, width))
                                                img.save(assets_file)
                                target_path = (
                                        STATIC_ROOT / 'assets' / dir_path.relative_to(BASE_DIR / 'tmp' / 'assets'))
                                target_path.mkdir(parents=True, exist_ok=True)
                                (dir_path / file).replace(target_path / file)

                                copy_count += 1
            shutil.rmtree(BASE_DIR / 'tmp' / 'assets')
            refresh_item_ico()

            Record(type='update', user=user,
                   comment=f"完成！新增 {create_count} , "
                           f"更新 {update_count}  "
                           f"收录 {copy_count} "
                           f"添加物品 {item_create_count} "
                           f"更新物品 {item_update_count} ").save()
        except zipfile.BadZipfile as e:
            print(e)
            Record(type='update', user=user, comment="损坏的压缩包").save()
        except FileNotFoundError as e:
            print(e)
            Record(type='update', user=user, comment="文件不存在").save()


def refresh_item_ico():
    counter = 0
    for item in Tag.objects.filter(type='item', generated=True, ico=''):
        if refresh_single_item_ico(item): counter += 1
    return counter


def refresh_single_item_ico(item):
    try:
        sec_name = item.sec_name.split('.')
        path = Path(sec_name[1]) / 'models' / 'item' / (sec_name[2] + '.json')
        model_json = get_model_json(path)
        if 'parent' in model_json and model_json['parent'] == "builtin/generated":
            texture = to_texture_path(model_json['textures']['layer0'], True)
            with texture.open('rb') as texture_file:
                item.ico = File(texture_file, texture.name)
                item.generated = False
                item.save()
                return True
    except Exception as e:
        print(e)
    return False


def rotation_matrix(angle, axis):
    angle_rad = math.radians(angle)
    cos_angle = math.cos(angle_rad)
    sin_angle = math.sin(angle_rad)

    if axis == 'x':
        matrix = [[1, 0, 0],
                  [0, cos_angle, -sin_angle],
                  [0, sin_angle, cos_angle]]
    elif axis == 'y':
        matrix = [[cos_angle, 0, sin_angle],
                  [0, 1, 0],
                  [-sin_angle, 0, cos_angle]]
    elif axis == 'z':
        matrix = [[cos_angle, -sin_angle, 0],
                  [sin_angle, cos_angle, 0],
                  [0, 0, 1]]
    else:
        return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

    return matrix


def matrix_rotate(matrix, coords, origin):
    # 将原点移到原点位置
    x = coords[0] - origin[0]
    y = coords[1] - origin[1]
    z = coords[2] - origin[2]
    # 计算旋转后的坐标
    rotated_coords = [
        matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z + origin[0],
        matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z + origin[1],
        matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z + origin[2]
    ]

    return rotated_coords


def rot_uv(uv, r, texture_size):
    x = float(texture_size[0])
    y = float(texture_size[1])
    uv = [uv[0] / x, 1 - uv[1] / y, uv[2] / x, 1 - uv[3] / y]
    if r == 0 or r == 360:
        # ↙ ↘ ↖ ↗
        return [uv[0], uv[3],
                uv[2], uv[3],
                uv[0], uv[1],
                uv[2], uv[1]]
    if r == 90:
        # ↘ ↗ ↙ ↖
        return [uv[2], uv[3],
                uv[2], uv[1],
                uv[0], uv[3],
                uv[0], uv[1]]
    if r == 180:
        # ↗ ↖ ↘ ↙
        return [uv[2], uv[1],
                uv[0], uv[1],
                uv[2], uv[3],
                uv[0], uv[3]]
    if r == 270:
        # ↖ ↙ ↗ ↘
        return [uv[0], uv[1],
                uv[0], uv[3],
                uv[2], uv[1],
                uv[2], uv[3]]


def to_face_list(model_json, x=0, y=0, z=0, rx=0, ry=0):
    face_list = []
    texture_list = {}
    if 'model' in model_json:
        obj_path = model_json.get("model")
        if ":" in obj_path:
            mod, path = obj_path.split(":")
        else:
            mod, path = "minecraft", obj_path
        obj_path = BASE_DIR / STATIC_ROOT / 'assets' / mod / path
        mtl = ''
        with obj_path.open() as obj_file:
            for line in obj_file:
                if line.startswith('mtllib'):
                    mtl = line.strip().split()[1]
                    break
        obj_url = "/{}assets/{}/{}".format(STATIC_URL, mod, path)
        mtl_url = obj_url.replace(obj_path.name, mtl)
        txrp = {}
        for tx in model_json['textures']:
            txrp[f"#{tx}"] = to_texture_path(rec_texture(model_json['textures'], model_json['textures'][tx]))
        face_list.append({
            'obj': obj_url,
            'mtl': mtl_url,
            'txrp': txrp,
            'rot': [rx, ry],
            'pos': [x, y, z]
        })

    for element in model_json.get("elements", []):
        f = element['from']
        t = element['to']
        for name, face in element['faces'].items():
            if name == "north":
                a = [t[0], f[1], f[2]]
                b = [f[0], f[1], f[2]]
                c = [t[0], t[1], f[2]]
                d = [f[0], t[1], f[2]]
            elif name == "south":
                a = [f[0], f[1], t[2]]
                b = [t[0], f[1], t[2]]
                c = [f[0], t[1], t[2]]
                d = [t[0], t[1], t[2]]
            elif name == "east":
                a = [t[0], f[1], t[2]]
                b = [t[0], f[1], f[2]]
                c = [t[0], t[1], t[2]]
                d = [t[0], t[1], f[2]]
            elif name == "west":
                a = [f[0], f[1], f[2]]
                b = [f[0], f[1], t[2]]
                c = [f[0], t[1], f[2]]
                d = [f[0], t[1], t[2]]
            elif name == "up":
                a = [f[0], t[1], t[2]]
                b = [t[0], t[1], t[2]]
                c = [f[0], t[1], f[2]]
                d = [t[0], t[1], f[2]]
            elif name == "down":
                a = [f[0], f[1], f[2]]
                b = [t[0], f[1], f[2]]
                c = [f[0], f[1], t[2]]
                d = [t[0], f[1], t[2]]
            else:
                continue

            if "rotation" in element:
                rotation = element['rotation']
                matrix = rotation_matrix(rotation['angle'], rotation['axis'])
                origin = rotation['origin']
                a = matrix_rotate(matrix, a, origin)
                b = matrix_rotate(matrix, b, origin)
                c = matrix_rotate(matrix, c, origin)
                d = matrix_rotate(matrix, d, origin)
            origin = [8, 8, 8]
            if rx != 0:
                matrix = rotation_matrix(-rx, "x")
                a = matrix_rotate(matrix, a, origin)
                b = matrix_rotate(matrix, b, origin)
                c = matrix_rotate(matrix, c, origin)
                d = matrix_rotate(matrix, d, origin)
            if ry != 0:
                matrix = rotation_matrix(-ry, "y")
                a = matrix_rotate(matrix, a, origin)
                b = matrix_rotate(matrix, b, origin)
                c = matrix_rotate(matrix, c, origin)
                d = matrix_rotate(matrix, d, origin)
            width = height = 16  # 默认值
            if name == "north" or name == "south":
                width = abs(t[0] - f[0])
                height = abs(t[1] - f[1])
            elif name == "east" or name == "west":
                width = abs(t[2] - f[2])
                height = abs(t[1] - f[1])
            elif name == "up" or name == "down":
                width = abs(t[0] - f[0])
                height = abs(t[2] - f[2])
            a = [a[0] + x * 16, a[1] + y * 16, a[2] + z * 16]
            b = [b[0] + x * 16, b[1] + y * 16, b[2] + z * 16]
            c = [c[0] + x * 16, c[1] + y * 16, c[2] + z * 16]
            d = [d[0] + x * 16, d[1] + y * 16, d[2] + z * 16]
            tx = rec_texture(model_json['textures'], face['texture'])
            if tx not in texture_list:
                texture_list[tx] = to_texture_path(tx)
            face_list.append({
                'a': a,
                'b': b,
                'c': c,
                'd': d,
                "uv": rot_uv(face.get("uv", [0, 0, width, height]), face.get("rotation", 0), [16, 16]),
                "tx": tx
            })
    return face_list, texture_list


class Model:
    def __init__(self, prop, name, state=None):
        self.part = []
        if state is not None:
            if isinstance(state, list):
                state = state[0]
            self.part.append({'model': state['model'], "x": state.get("x", 0), "y": state.get("y", 0)})
        self.prop = prop
        self.name = name

    def add_part(self, state):
        if isinstance(state, list):
            state = state[0]
        self.part.append({'model': state['model'], "x": state.get("x", 0), "y": state.get("y", 0)})

    def to_face(self, x, y, z):
        face_list = []
        texture_list = {}
        for p in self.part:
            model_json = get_model_json(to_assets_path(p['model'], 'models'))
            f, t = to_face_list(model_json, x, y, z, p['x'], p['y'])
            face_list += f
            texture_list.update(t)
        return face_list, texture_list


def rec_texture(textures, name: str):
    while name.startswith("#"):
        name = textures[name[1:]]
    return name


def get_model(name, prop):
    try:
        state_json = get_model_json(to_assets_path(name, 'blockstates'))
    except Assets.DoesNotExist:
        return []
    if "multipart" in state_json:
        m = Model(prop, name)
        states = state_json["multipart"]
        for state in states:
            if 'when' not in state:
                m.add_part(state["apply"])
                continue
            if 'OR' in state['when']:
                for p in state['when']['OR']:
                    same = True
                    for k, v in p.items():
                        if k not in prop:
                            continue
                        if v is str:
                            if prop[k] not in v.split("|"):
                                same = False
                                break
                        else:
                            if prop[k] != v:
                                same = False
                                break
                    if same:
                        m.add_part(state["apply"])
                    break
                continue
            same = True
            for k, v in state["when"].items():
                if k in prop and prop[k] not in v.split("|"):
                    same = False
                    break
            if same:
                m.add_part(state["apply"])
        return m
    elif "variants" in state_json:
        states = state_json["variants"]
        for state in states:
            if state == "":
                return Model(prop, name, states[state])
            prop_list = state.split(",")
            same = True
            for p in prop_list:
                k, v = p.split("=")
                if k in prop and prop[k] != v:
                    same = False
                    break
            if same:
                return Model(prop, name, states[state])
        return Model(prop, name)
    else:
        return Model(prop, name)


class Block:
    def __init__(self, x: int, y: int, z: int, model):
        self.x = x
        self.y = y
        self.z = z
        self.model = model


def to_texture_path(key, local=False):
    if ":" in key:
        mod, path = key.split(":")
    else:
        mod, path = "minecraft", key
    if local:
        return BASE_DIR / STATIC_ROOT / 'assets' / mod / 'textures' / (path + ".png")
    return "/{}assets/{}/textures/{}.png".format(STATIC_URL, mod, path)


def get_model_json(path: Path):
    try:
        assets = Assets.objects.get(name=str(path).replace('/', '\\'))
        model_json = assets.file
    except Assets.DoesNotExist:
        return {}
    parent = {}
    if "parent" in model_json:
        parent = get_model_json(to_assets_path(model_json["parent"], 'models'))
    return merge_dict(model_json, parent)


def to_assets_path(key, folder):
    if ":" in key:
        mod, path = key.split(":")
    else:
        mod, path = "minecraft", key
    return Path(mod) / folder / (path + ".json")


def merge_dict(d1, d2):
    if 'parent' in d1 and 'parent' in d2:
        d1['parent'] = d2['parent']
    for k, v in d1.items():
        if k in d2 and type(d2[k]) is dict:
            d2[k].update(v)
        else:
            d2[k] = v
    return d2


def analyze_blueprint(file):
    try:
        blueprint = nbt.NBTFile(file)
    except:
        return None, None
    if len(blueprint["palette"]) > 200:
        return None, None
    if len(blueprint["blocks"]) > 2000:
        return None, None
    context = {"x": blueprint['size'][0].value, "y": blueprint['size'][1].value, "z": blueprint['size'][2].value}

    models = []
    for state in blueprint["palette"]:
        tag_prop = state.get('Properties', {})
        prop = {}
        for p in state.get('Properties', {}):
            prop[p] = tag_prop[p].value
        models.append(get_model(state["Name"].value, prop))

    blocks = []
    count = {}
    for block in blueprint['blocks']:
        blocks.append(Block(
            block['pos'][0].value,
            block['pos'][1].value,
            block['pos'][2].value,
            models[block['state'].value]
        ))
        name = models[block['state'].value].name
        if name not in count:
            count[name] = 0
        count[name] += 1

    face = []
    texture = {}
    for block in blocks:
        f, t = block.model.to_face(block.x, block.y, block.z)
        face += f
        texture.update(t)
    context['f'] = face
    context['t'] = texture
    return json.dumps(context), json.dumps(dict(sorted(count.items(), key=lambda item: item[1], reverse=True)))


def get_item_face(key):
    sec_name = key.split('.')
    if len(sec_name) != 3:
        return {}
    model_json = get_model_json(Path(sec_name[1]) / 'models' / 'item' / (sec_name[2] + '.json'))
    f, t = to_face_list(model_json)
    gui = {'rotation': [0, 0, 0], 'scale': [1, 1, 1], 'translation': [0, 0, 0]}
    gui.update(model_json.get('display', {}).get('gui', gui))
    return {'f': f, 't': t, 'gui': gui, 'gui_light': model_json.get('gui_light', 'side')}
