# -*- coding: utf-8 -*-
"""
Author  : CraftPilot
"""

import json
import os
import time
from typing import Dict, List
from enum import Enum

from .dson_geo import HeightField, InstancedStaticMesh, Texture, StaticMesh
from .meta_helper import Singleton

"""note:
io_mode:
0: server_text (like houdini_text)
1: houdini_text (support)
2: houdini_binary (support)
3: houdini_hdk (not support)
 """


class FieldDesc:
    """
    helper struct used when create field or get outputs from field
    """

    def __init__(self, work_path, io_mode=1, raw_compress=False) -> None:
        self.work_path: str = work_path
        self.raw_compress: bool = raw_compress
        self.io_mode: int = io_mode


def unique_name():
    n = 1
    while True:
        yield n
        n += 1
        if n > 10000:
            n = 1


unique_name_generator = unique_name()

'''
FieldManager   
'''


class FieldManager(metaclass=Singleton):
    def __init__(self):
        self.fieldMap = {}  # map from field_type_name(str) to actual Field

    def create_field(self, json_object, input_name, field_desc: FieldDesc):
        """create a Field

        Args:
            json_object(Dict)
            work_path(str)
        Return:
            Field
            """
        # try:
        assert "If List" in json_object
        if json_object['If List']:
            return ListField.from_json(json_object, input_name, field_desc)
        else:
            return self.fieldMap[json_object['Type']].from_json(json_object, input_name, field_desc)
        # except Exception as e:
        #     logging.exception("get_input failed, json_object:" + str(json_object))

    def __getitem__(self, item):
        return self.fieldMap[item]


'''
define Field
'''

BOOL_TYPE = 'paramType.Bool'
INT_TYPE = 'paramType.Int'
INT2_TYPE = 'paramType.Int2'
INT3_TYPE = 'paramType.Int3'
INT4_TYPE = 'paramType.Int4'
FLOAT_TYPE = 'paramType.Float'
FLOAT2_TYPE = 'paramType.Float2'
FLOAT3_TYPE = 'paramType.Float3'
FLOAT4_TYPE = 'paramType.Float4'
STRING_TYPE = 'paramType.String'
JSON_TYPE = 'paramType.Json'
FILE_TYPE = 'paramType.File'
LIST_TYPE = 'paramType.List'
HEIGHTFIELD_TYPE = 'paramType.HeightField'
INSTANCED_STATIC_MESH_TYPE = 'paramType.InstancedStaticMesh'
STATIC_MESH_TYPE = 'paramType.StaticMesh'
TEXTURE_TYPE = 'paramType.Texture'
CURVE_TYPE = 'paramType.Curve'
TRANSFORM_TYPE = 'paramType.Transform'
COLOR_TYPE = 'paramType.Color'


class FieldCategory(Enum):
    Input = 0
    Param = 1
    Output = 2


class FieldMeta(type):
    def __init__(cls, *args, **kwargs):
        assert hasattr(cls, 'field_type_name')
        FieldManager().fieldMap[cls.field_type_name] = cls
        super().__init__(*args, **kwargs)


"""some notes about Field
1. field_type_name will be registered in FieldManager. So FiledManager Can create Field according to 'Type' field in json object
2. create_json_field(): for supporting generate json from function's signature
3. from_json : for creating field from json field in Inputs or Params
4. get_input: functions use this method to get actual value to impl_function
5. get_output: fill the 'Value Data' hole in json_field
"""


class Field(metaclass=FieldMeta):
    field_type_name = ""
    if_list = False
    support_param = True  # if this field can be a param(with default value)

    @classmethod
    def create_json_field(cls, field_category=FieldCategory.Input, name: str = None):
        """ create a json object
        """
        """get json format field"""
        field = {'Type': cls.field_type_name, 'If List': cls.if_list, 'FieldCategory': field_category.name}
        if name is not None:
            field["Label"] = name
        return field

    def __init__(self, value=None):
        self._value = value

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        """
        Args:
            json_object: the json field describe this field type in json(dson) file
        """
        return cls(json_object['Value Data'])

    def get_input(self):
        """ pass input to impl function"""
        self.type_assert()
        return self._value

    def type_assert(self):
        pass

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        json_field[data_field] = self._value

    @classmethod
    def get_param_default(cls, json_object, default_value):
        """fill default value to json field"""
        json_object["Default Value"] = default_value


# base class for those who want to support list field
class ListSubFieldBase:
    @classmethod
    def list_from_json_value(cls, json_value, work_path=None):
        return json_value

    @classmethod
    def list_to_json_value(cls, val, work_path=None):
        return val


class Bool(Field):
    field_type_name = BOOL_TYPE

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        return cls(bool(json_object['Value Data'][0]))

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        # json_field[data_field] = [str(self._value)]
        json_field[data_field] = [self._value]


class Int(Field, ListSubFieldBase):
    field_type_name = INT_TYPE

    def type_assert(self):
        assert isinstance(self._value, int)

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        return cls(int(json_object['Value Data'][0]))

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        # json_field[data_field] = [str(self._value)]
        json_field[data_field] = [self._value]

    @classmethod
    def get_param_default(cls, json_object, default_value):
        json_object["Default Value"] = [default_value]


class Int2(Field, ListSubFieldBase):
    field_type_name = INT2_TYPE

    def type_assert(self):
        assert isinstance(self._value, tuple) or isinstance(self._value, list)
        if len(self._value) != 2:
            raise Exception("value is not int2: " + str(self._value))

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        return cls(list(map(lambda a: int(a), json_object['Value Data'])))

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        # json_field[data_field] = list(map(lambda a: str(a), self._value))
        json_field[data_field] = self._value


class Int3(Int2):
    field_type_name = INT3_TYPE

    def type_assert(self):
        assert isinstance(self._value, tuple) or isinstance(self._value, list)
        if len(self._value) != 3:
            raise Exception("value is not int3: " + str(self._value))


class Int4(Int2):
    field_type_name = INT4_TYPE

    def type_assert(self):
        assert isinstance(self._value, tuple) or isinstance(self._value, list)
        if len(self._value) != 4:
            raise Exception("value is not int4: " + str(self._value))


class Float(Field):
    field_type_name = FLOAT_TYPE

    def type_assert(self):
        assert isinstance(self._value, float)

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        return cls(float(json_object['Value Data'][0]))

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        # json_field[data_field] = [str(self._value)]
        json_field[data_field] = [self._value]

    @classmethod
    def get_param_default(cls, json_object, default_value):
        assert isinstance(default_value, float)
        json_object['Default Value'] = [default_value]


class Float2(Field, ListSubFieldBase):
    field_type_name = FLOAT2_TYPE

    def type_assert(self):
        assert isinstance(self._value, tuple) or isinstance(self._value, list)
        if len(self._value) != 2:
            raise Exception("value is not float2: " + str(self._value))

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        # return cls(list(map(lambda a: float(a), json_object['Value Data'])))
        return cls(json_object['Value Data'])

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        # json_field['Value Data'] = list(map(lambda a: str(a), self._value))
        json_field[data_field] = self._value


class Float3(Float2, ListSubFieldBase):
    field_type_name = FLOAT3_TYPE

    def type_assert(self):
        assert isinstance(self._value, tuple) or isinstance(self._value, list)
        if len(self._value) != 3:
            raise Exception("value is not float2:" + str(self._value))


class Float4(Float2):
    field_type_name = FLOAT4_TYPE

    def type_assert(self):
        assert isinstance(self._value, tuple) or isinstance(self._value, list)
        if len(self._value) != 4:
            raise Exception("value is not float2:" + str(self._value))


class String(Field, ListSubFieldBase):
    field_type_name = STRING_TYPE

    def type_assert(self):
        assert isinstance(self._value, str)

    @classmethod
    def create_json_field(cls, field_category=FieldCategory.Input, name: str = None):
        field = super().create_json_field(field_category, name)
        field["String Type"] = "stringParmType.Regular"
        return field

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        return cls(json_object['Value Data'][0])

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        json_field[data_field] = [self._value]

    @classmethod
    def get_param_default(cls, json_object, default_value):
        assert isinstance(default_value, str)
        json_object['Default Value'] = [default_value]


# _value is file name
class FileField(Field, ListSubFieldBase):
    field_type_name = FILE_TYPE
    support_param = False

    def __init__(self, file_name=None, work_path=None):
        super().__init__(file_name)
        self._work_path = work_path

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        return FileField(json_object['Value Data'], field_desc.work_path)

    def type_assert(self):
        assert isinstance(self._value, str)

    def get_input(self):
        """return absolute path"""
        return os.path.join(self._work_path, self._value).replace("\\", '/')

    @classmethod
    def list_to_json_value(cls, val, work_path=None):
        if work_path is None:
            raise Exception("file as list sub field need work_path")
        return val._value


    @classmethod
    def list_from_json_value(cls, json_value, work_path=None):
        return FileField(json_value, work_path)


# _value is file path
class JsonField(Field, ListSubFieldBase):
    field_type_name = JSON_TYPE
    support_param = False

    def __init__(self, value, filename=None, work_path=None):
        super().__init__(value)
        self._filename = filename
        self._work_path = work_path

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        filename = json_object['Value Data'][0]
        file_path = os.path.join(field_desc.work_path, filename).replace("\\", '/')
        f = open(file_path, 'r')
        value = json.loads(f.read())
        return cls(value, filename, field_desc.work_path)

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        if self._filename is None:
            self._filename = "jsonfile_" + json_field['label'] + ".json"
        # save file
        file_path = os.path.join(field_desc.work_path, self._filename).replace('\\', '/')
        f = open(file_path, "w")
        json.dump(self._value, f)
        # fill data field
        json_field[data_field] = [self._filename]

    @classmethod
    def list_to_json_value(cls, val, work_path=None):
        if work_path is None:
            raise Exception("json file as list sub field need work_path")
        file_name = "jsonfile_" + time.strftime("%H%M%S", time.localtime()) + str(next(unique_name_generator)) + ".json"
        file_path = os.path.join(work_path, file_name).replace("\\", '/')
        f = open(file_path, 'w')
        json.dump(val, f)
        return file_name

    @classmethod
    def list_from_json_value(cls, json_value, work_path=None):
        if work_path is None:
            return {}
        file_path = os.path.join(work_path, json_value).replace("\\", "/")
        f = open(file_path, 'r')
        value = json.loads(f.read())
        f.close()
        return value


class ListMeta(FieldMeta):
    """to support sth like 'ListField[Int]' """

    def __getitem__(self, item):
        assert issubclass(item, ListSubFieldBase)
        cls_name = 'ListField' + item.field_type_name
        cls_dict = {cls.__name__: cls for cls in ListField.__subclasses__()}
        if cls_name in cls_dict:
            return cls_dict[cls_name]
        a = ListMeta('ListField' + item.field_type_name, (ListField,), {'inner_field': item})
        a.inner_field = item
        return a


class ListField(Field, metaclass=ListMeta):
    if_list = True
    inner_field = Field
    field_type = LIST_TYPE
    support_param = True

    def __init__(self, values, inner_field_type=None):
        super().__init__(values)
        self.inner_field_type = self.inner_field if inner_field_type is None else inner_field_type

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        values = json_object["Value Data"]
        inner_field_type = FieldManager()[json_object['Data Type']]

        def helper(value):
            return inner_field_type.list_from_json_value(value, field_desc.work_path)

        values = list(map(helper, values))
        return ListField(values, inner_field_type)

    @classmethod
    def create_json_field(cls, field_category=FieldCategory.Input, name: str = None):
        field = {'Type': cls.field_type, 'Data Type': cls.inner_field.field_type_name, 'If List': cls.if_list,
                 'FieldCategory': field_category.name}
        if name is not None:
            field["Label"] = name
        return field

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        def helper(value):
            return self.inner_field_type.list_to_json_value(value, work_path=field_desc.work_path)

        values = list(map(helper, self._value))
        json_field[data_field] = values


class HeightFieldField(Field):
    """
    _value is list of HeightField
    """
    field_type_name = HEIGHTFIELD_TYPE
    support_param = False

    def __init__(self, value=None):
        self._value: List[HeightField] = value

    @classmethod
    def from_json(cls, json_object, input_name, field_desc):
        if field_desc.work_path is None:
            raise Exception("HeightFieldField from_json failed:work path need to be set, not allowed")
        value = []
        for editing_layer_key, editing_layer_val in json_object.items():
            if 'EditingLayer_' in editing_layer_key:
                heightfield = HeightField.create_from_json(json_object, editing_layer_val, field_desc.work_path, field_desc.io_mode, field_desc.raw_compress)
                value.append(heightfield)
        return cls(value)

    def get_input(self):
        return self._value

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        geo_index = 0
        for heightfield in self._value:
            num = heightfield.to_json(geo_index, output_index, json_field, field_desc.io_mode, field_desc.work_path, field_desc.raw_compress)
            geo_index += num


class InstancedStaticMeshField(Field):
    """
    _value is a InstanceStaticMesh
    """
    field_type_name = INSTANCED_STATIC_MESH_TYPE
    support_param = False

    def __init__(self, value):
        self._value = value

    @classmethod
    def from_json(cls, json_object, input_name, field_desc:FieldDesc):
        if field_desc.work_path is None:
            raise Exception("HeightFieldField from_json failed:work path need to be set, not allowed")
        value = InstancedStaticMesh.create_from_json(json_object, field_desc.work_path, field_desc.io_mode, input_name)
        return InstancedStaticMeshField(value)

    def get_input(self):
        return self._value

    def get_output(self, json_field, field_desc, data_field='Value Data', output_index=0):
        geo_index = 0
        self._value.to_json(geo_index, output_index, json_field, field_desc.io_mode, field_desc.work_path)


class StaticMeshField(Field):
    """
    _value is a StaticMesh
    """
    field_type_name = STATIC_MESH_TYPE
    support_param = False

    def __init__(self, value):
        self._value = value

    @classmethod
    def from_json(cls, json_object, input_name, field_desc:FieldDesc):
        if field_desc.work_path is None:
            raise Exception("StaticMesh from_json failed:work path need to be set, not allowed")
        # kivlin. todo
        value = StaticMesh.create_from_json(json_object, field_desc.work_path, field_desc.io_mode, input_name)
        return StaticMeshField(value)

    def get_input(self) -> StaticMesh:
        return self._value

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        geo_index = 0
        self._value.to_json(geo_index, output_index, json_field, field_desc.io_mode, field_desc.work_path)


class TextureField(Field):
    """
    _value is a Texture
    """
    field_type_name = TEXTURE_TYPE
    support_param = False

    def __init__(self, value=None):
        self._value: Texture = value

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        if field_desc.work_path is None:
            raise Exception("TextureField from_json failed:work path need to be set, not allowed")
        value = Texture.create_from_json(json_object, field_desc.work_path, field_desc.io_mode, field_desc.raw_compress, input_name)
        return cls(value)

    def get_input(self):
        return self._value

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        geo_index = 0
        self._value.to_json(geo_index, output_index, json_field, field_desc.io_mode, field_desc.work_path, field_desc.raw_compress)


class CurveField(Field, ListSubFieldBase):
    """
    _value is list of file paths (strings)
    """
    field_type_name = CURVE_TYPE
    support_param = False

    def __init__(self, value=None, work_path=None):
        super().__init__(value)
        self._work_path = work_path

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        # Value Data contains list of filenames
        return cls(json_object['Value Data'], field_desc.work_path)

    def get_input(self):
        if self._work_path is None:
            return self._value
        return [os.path.join(self._work_path, f).replace("\\", '/') for f in self._value]

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        json_field[data_field] = self._value

    @classmethod
    def list_to_json_value(cls, val, work_path=None):
        return val._value

    @classmethod
    def list_from_json_value(cls, json_value, work_path=None):
        return cls(json_value, work_path)


class TransformField(Field, ListSubFieldBase):
    """
    _value is list of 10 floats: [LocX, LocY, LocZ, RotX, RotY, RotZ, RotW, ScaleX, ScaleY, ScaleZ]
    """
    field_type_name = TRANSFORM_TYPE

    def type_assert(self):
        assert isinstance(self._value, list) or isinstance(self._value, tuple)
        if len(self._value) != 10:
            raise Exception("value is not transform (10 floats): " + str(self._value))

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        return cls(list(map(lambda a: float(a), json_object['Value Data'])))

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        json_field[data_field] = self._value

    @classmethod
    def get_param_default(cls, json_object, default_value):
        json_object["Default Value"] = default_value


class ColorField(Field, ListSubFieldBase):
    """
    _value is list of 4 floats: [R, G, B, A]
    """
    field_type_name = COLOR_TYPE

    def type_assert(self):
        assert isinstance(self._value, list) or isinstance(self._value, tuple)
        if len(self._value) != 4:
            raise Exception("value is not color (4 floats): " + str(self._value))

    @classmethod
    def from_json(cls, json_object, input_name, field_desc: FieldDesc):
        return cls(list(map(lambda a: float(a), json_object['Value Data'])))

    def get_output(self, json_field, field_desc: FieldDesc, data_field='Value Data', output_index=0):
        json_field[data_field] = self._value

    @classmethod
    def get_param_default(cls, json_object, default_value):
        json_object["Default Value"] = default_value