from inc_noesis import *
import noesis
import rapi
import os

def registerNoesisTypes():
    handle = noesis.register("Illusion", ".xx")
    noesis.setHandlerTypeCheck(handle, noepyCheckType)
    noesis.setHandlerLoadModel(handle, noepyLoadModel) #see also noepyLoadModelRPG
    noesis.setHandlerWriteModel(handle, noepyWriteModel)
    return 1

#check if it's this type based on the data
def noepyCheckType(data):

    return 1

#load the model
def noepyLoadModel(data, mdlList):
    ctx = rapi.rpgCreateContext()
    parser = Illusion_XX(data)
    parser.parse_file()
    mdl = rapi.rpgConstructModel()
    mdl.setModelMaterials(NoeModelMaterials(parser.texList, parser.matList))
    mdl.setBones(parser.boneList)
    mdlList.append(mdl)
    return 1

def noepyWriteModel(mdl, bs):
    
    if mdl:
        writer = XX_Export(mdl, bs)
        writer.write_file()
        return 1
    return 0

def get_directory_path():
    
    dirPath = rapi.getDirForFilePath(rapi.getInputName())
    if not dirPath:
        dirPath = os.path.dirname(os.path.abspath(rapi.getInputName())) + os.sep
    return dirPath

class XX_Texture(object):
    
    def __init__(self):
        
        self.name = ""
        self.path = ""
        self.size = 0
        self.width = 0
        self.height = 0
        self.ext = ""
        self.data = bytes()
        
class XX_Material(object):
    
    def __init__(self):
        
        self.name = ""
        self.texName = ""
        self.specName = ""
        self.normName = ""
        self.alphaName = ""

class XX_Frame(object):
    '''An Illusion XX Frame object'''
    
    def __init__(self, index):
        self.index = index
        self.parent = -1
        self.name = ""
        self.children = []
        self.meshes = []
        self.transform = NoeMat44()
    
class XX_Model(object):
    '''The XX object'''
    
    def __init__(self):
        
        self.frames = []
        self.materials = []
        self.textures = []
        
    def add_frame_child(self, index, frame):
        
        self.frames[index].children.append(frame)
        
    def add_frame_mesh(self, index, mesh):
        
        self.frames[index].meshes.append(mesh)
    
class XX_Export(object):
    
    def __init__(self, model, outFile):    
        
        self.xxFormat = -1
        self.outFile = outFile
        self.mdl = model
        self.dirpath = get_directory_path()
        
        self.xxFile = XX_Model()

        #helper attributes
        self.matList = []
        self.texList = []
        self.texNames = []
        self.matNames = [] #for index lookups
        
    def encrypt_string(self, string):
        '''Return the encrypted string. XOR each character in the string by
        FF to get the actual character. Strings are null-terminated.'''
        
        outStr = bytes()
        for i in range(len(string)):
            outStr += struct.pack("B", ord(string[i]) ^ 0xFF)
        return outStr
    
    def write_name(self, name):
        '''All names are null-terminated, preceded with the length of the
        string, including the null-char'''
        
        if name:
            string = self.encrypt_string(name + "\x00")
            self.outFile.writeUInt(len(string))
            self.outFile.writeBytes(string)
        else:
            self.outFile.writeUInt(0)
    
    def write_material_textures(self, mat):
        
        #diffuse map
        diffuseTex = os.path.basename(mat.texName)
        self.write_name(diffuseTex)
        self.outFile.writeBytes(b'\x00' * 16)
        
        #specular map?
        self.write_name("")
        self.outFile.writeBytes(b'\x00' * 16)
        
        #normal map?
        self.write_name("")
        self.outFile.writeBytes(b'\x00' * 16)        
                
        #alpha map?
        self.write_name("")
        self.outFile.writeBytes(b'\x00' * 16)        
        
    def write_materials(self):
        
        #self.outFile.writeBytes(b"\x9A\x19\x07\x44")
        self.outFile.writeBytes(b"\x00\x00\xE0\x40")
        numMat = len(self.matList)
        self.outFile.writeUInt(numMat)
        for i in range(numMat):
            
            mat = self.matList[i]
            ambient = NoeVec4((-1, -1, -1, -1))
            diffuse = NoeVec4((-1, -1, -1, -1))
            emissive = NoeVec4((-0.2, -0.2, -0.2, -1))
            specular = NoeVec4((0, 0, 0, -1))
            power = -20
            matUnk1 = b"\x00\x01\x02\x03"
            
            self.write_name(mat.name)
            self.outFile.writeBytes(ambient.toBytes())
            self.outFile.writeBytes(diffuse.toBytes())
            self.outFile.writeBytes(specular.toBytes())
            self.outFile.writeBytes(emissive.toBytes())
            self.outFile.writeFloat(power)
            self.write_material_textures(mat)
            self.outFile.writeBytes(matUnk1)
            self.outFile.writeBytes(b'\x00' * 84)
	        
    def write_textures(self):
        
        #how many textures we actually find
        texFound = 0
        numTex = len(self.texList)
        pos = self.outFile.tell()
        self.outFile.writeUInt(numTex)
        for tex in self.texList:
            texName = tex.name
            unk1 = b'\x00\x00\x00\x00'
            unk2 = b'\x01\x00\x00\x00\x01\x00\x00\x00\x14\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00'
            texture = rapi.loadTexByHandler(tex.data, tex.ext)
            if texture:
                self.write_name(texName)
                self.outFile.writeBytes(unk1)
                self.outFile.writeUInt(texture.width)
                self.outFile.writeUInt(texture.height)
                self.outFile.writeBytes(unk2)
                self.outFile.writeByte(1) #checksum
                self.outFile.writeUInt(tex.size)
                self.outFile.writeBytes(tex.data)
                texFound += 1
            else:
                print("%s not found" %texName)
                continue
            
        curr = self.outFile.tell()
        self.outFile.seek(pos)
        self.outFile.writeUInt(texFound)
        self.outFile.seek(curr)
            
    def write_dupe_vertices(self, mesh):
        
        self.outFile.writeUShort(0) #no dupes
        self.outFile.writeBytes(b"\x12\x01\x00\x00\x20\x00\x00\x00")
        
    def write_vertices(self, mesh, numVerts):
        
        for i in range(numVerts):
            
            if self.xxFormat >= 4:
                self.outFile.writeUShort(i)
            else:
                self.outFile.writeUInt(i)            
            self.outFile.writeBytes(mesh.positions[i].toBytes())
            self.outFile.writeBytes(NoeVec3().toBytes())
            self.outFile.writeBytes(b"\x00\x00\x00\x00")
            self.outFile.writeBytes(mesh.normals[i].toBytes())
            self.outFile.writeBytes(mesh.uvs[i].toBytes()[:8])
            
    def write_faces(self, mesh, numIdx):
        
        for i in range(numIdx):
            self.outFile.writeUShort(mesh.indices[i])
            
    def write_mesh_unknowns(self, numVerts, meshType):
        
        #unknown A
        self.outFile.writeBytes(b'\x00' * (numVerts * 8 * meshType))
        
        #unknown B
        self.outFile.writeBytes(b'\x00' * 100)
        
        #unknown C
        if self.xxFormat >= 3:
            self.outFile.writeBytes(b'\x00' * 64)
            
        #unknown D
        if self.xxFormat >= 5:
            self.outFile.writeBytes(b'\x00' * 20)
            
        #unknown E
        if self.xxFormat >= 6:
            self.outFile.writeBytes(b'\x00' * 28)
        
    def write_mesh(self, mesh, meshType):

        meshName = mesh.name
        meshMat = mesh.matName
        numVerts = len(mesh.positions)
        numIdx = len(mesh.indices)
        meshUnk = b'\x02\x64\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        try:
            matNum = self.matNames.index(meshMat)        
        except:
            matNum = -1
        
        self.outFile.writeBytes(meshUnk)
        self.outFile.writeUInt(matNum)
        self.outFile.writeUInt(numIdx)
        self.write_faces(mesh, numIdx)
        self.outFile.writeUInt(numVerts)
        self.write_vertices(mesh, numVerts)        
        self.write_mesh_unknowns(numVerts, meshType)
        
    def write_bones(self, mesh):
        
        #hardcode
        self.outFile.writeUInt(0)
    
    def write_frame(self, root):
        
        index = root.index
        name = root.name
        subframes = root.children
        meshes = root.meshes
        transform = root.transform
        numChildren = len(subframes)
        numMesh = len(meshes)
        unkA = struct.pack("4L", *[0, 100, 0, 0]) #harcode
        
        if numMesh:
            unkB = b"\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00"
        else:
            unkB = b"\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        bbMin = self.xxFile.bbox_min
        bbMax = self.xxFile.bbox_max
        
        self.write_name(name)
        self.outFile.writeUInt(numChildren)
        self.outFile.writeBytes(transform.toBytes())
        self.outFile.writeBytes(unkA)
        self.outFile.writeUInt(numMesh)
        self.outFile.writeBytes(bbMin.toBytes())
        self.outFile.writeBytes(bbMax.toBytes())
        self.outFile.writeBytes(unkB)
        
        if numMesh > 0:
            meshType = 0
            self.outFile.writeUByte(meshType)
            for i in range(numMesh):
                mesh = meshes[i]
                self.write_mesh(mesh, meshType)
            self.write_dupe_vertices(mesh)
            self.write_bones(mesh)
            
        for i in range(numChildren):
            self.write_frame(subframes[i])
        
    #A couple helper functions
    
    def write_header(self):
        
        self.outFile.writeUInt(self.xxFormat)
        self.outFile.writeUByte(0)
        self.outFile.writeFloat(6.05)
        self.outFile.writeFloat(0)
        self.outFile.writeBytes(b"\x00" * 13)
        
    def load_texture(self, texPath, ext):
        
        imageData = bytes()
        if os.path.exists(texPath):
            f = open(texPath, 'rb')
            data = f.read()
            f.close()
            
            if ext.lower() == ".bmp":
                imageData = bytes()
                imageData += b'\x00\x00'
                imageData += data[2:]
            else:
                imageData = data
        return imageData
        
    def check_texture(self, texName):
        
        
        filename, ext = os.path.splitext(texName)
        if ext != ".png":
            texName = filename + ext + ".png"
            ext = ".png"
        path = os.path.abspath(texName)
        if texName not in self.texNames:
            imageData = self.load_texture(path, ext)
            tex = XX_Texture()
            tex.name = texName
            tex.path = path
            tex.size = len(imageData)
            tex.data = imageData
            tex.ext = ext
            
            self.texNames.append(texName)
            if tex.size > 0:
                self.texList.append(tex)
        return texName
            
    def check_materials(self):
        
        if self.mdl.modelMats:
            for mat in self.mdl.modelMats.matList:
                material = XX_Material()
                material.name = mat.name
                texName = self.check_texture(mat.texName)
                material.texName = texName
                
                self.matNames.append(mat.name)
                self.matList.append(material)
        
    def calculate_bounding_box(self):
        
        minx, miny, minz = 999999999, 9999999, 99999999
        maxx, maxy, maxz = 0, 0, 0
        
        for mesh in self.mdl.meshes:
            for x, y, z in mesh.positions:
                minx = min(minx, x)
                miny = min(miny, y)
                minz = min(minz, z)
                maxx = max(maxx, x)
                maxy = max(maxy, y)
                maxz = max(maxz, z)
                
        self.xxFile.bbox_min = NoeVec3((minx, miny, minz))
        self.xxFile.bbox_max = NoeVec3((maxx, maxy, maxz))

    def build_frame(self, name, index, parent):
        
        frame = XX_Frame(index)
        frame.name = name
        if parent != -1:
            self.xxFile.add_frame_child(parent, frame)
            frame.parent = parent
            frame.transform = NoeMat44()
        self.xxFile.frames.append(frame)
        
    def check_frames(self):
        
        self.build_frame("All_Root", 0, -1)
        self.build_frame("SCENE_ROOT", 1, 0)
        
        meshes = self.mdl.meshes
        index = 2
        for i in range(len(meshes)):
            mesh = meshes[i]
            self.build_frame(mesh.name, index, 1)
            self.xxFile.add_frame_mesh(index, mesh)
            index += 1
	
    def write_file(self):
        
        #hardcode format and initialize an XX file
        self.xxFormat = 3
        self.check_frames()
        self.check_materials()
        self.calculate_bounding_box()
        self.write_header()

        #write frames
        root = self.xxFile.frames[0]
        self.write_frame(root)
        self.write_materials()
        self.write_textures()
        if self.xxFormat >= 2:
            self.outFile.writeBytes(b"\x00" * 10)

class Illusion_XX(object):
    
    def __init__(self, data):    
        
        self.inFile = NoeBitStream(data)
        self.animList = []
        self.texList = []
        self.matList = []
        self.boneList = []
        self.meshInfo = []
      
    def decrypt_string(self, string):
        '''Return the decrypted string. XOR each character in the string by
        FF to get the actual character. Strings are null-terminated.'''
        
        inverted = ""
        for i in range(len(string)-1):
            inverted += chr(string[i] ^ 0xFF)
        return inverted
    
    def read_name(self):
        
        length = self.inFile.readUInt()
        return self.decrypt_string(self.inFile.readBytes(length))
    
    def get_format(self):
        
        xxFormat = 0
        fmt = self.inFile.readUByte()
        xxInt = self.inFile.read('4B')
        if (fmt >= 0x01 and fmt <= 0x06 and xxInt[0] == 0):
            xxFormat = fmt
        elif xxInt[2] == 0x3F:
            xxFormat = -1
        return xxFormat
    
    def build_meshes(self):
            
        for meshInfo in self.meshInfo:
            
            vertBuff = meshInfo[0]
            idxBuff = meshInfo[1]
            numIdx = meshInfo[2]
            matNum = meshInfo[3]
            meshName = meshInfo[4]
            
            if self.xxFormat >= 4:
                rapi.rpgBindPositionBufferOfs(vertBuff, noesis.RPGEODATA_FLOAT, 70, 2)
                rapi.rpgBindNormalBufferOfs(vertBuff, noesis.RPGEODATA_FLOAT, 70, 30)
                rapi.rpgBindUV1BufferOfs(vertBuff, noesis.RPGEODATA_FLOAT, 70, 42)
            else:
                rapi.rpgBindPositionBufferOfs(vertBuff, noesis.RPGEODATA_FLOAT, 52, 4)
                rapi.rpgBindNormalBufferOfs(vertBuff, noesis.RPGEODATA_FLOAT, 52, 32)
                rapi.rpgBindUV1BufferOfs(vertBuff, noesis.RPGEODATA_FLOAT, 52, 44)
            
            if len(self.matList) > matNum:
                mat = self.matList[matNum]            
                matName = mat.name
                rapi.rpgSetMaterial(matName)
            rapi.rpgSetName(meshName)
            rapi.rpgCommitTriangles(idxBuff, noesis.RPGEODATA_USHORT, numIdx, noesis.RPGEO_TRIANGLE, 1)
            rapi.rpgClearBufferBinds()    
    
    def parse_textures(self, numTex):
        
        for i in range(numTex):
            texName = self.read_name()
            texUnk1 = self.inFile.readBytes(4)
            width, height = self.inFile.read('2L')
            
            texUnk2 = self.inFile.readBytes(20)
            checksum = self.inFile.readByte()
            dataSize = self.inFile.readUInt()
            ext = os.path.splitext(texName)[1]
            data = self.inFile.readBytes(dataSize)
            if ext == ".bmp":
                texData = bytes()
                texData += b'\x42\x4D'
                texData += data[2:]
                tex = rapi.loadTexByHandler(texData, ext)
            else:
                tex = rapi.loadTexByHandler(data, ext)
           
            if tex is not None:
                tex.name = texName
                self.texList.append(tex)   

                
    def parse_materials(self, numMat):

        for i in range(numMat):
            matName = self.read_name()
            diffuse = self.inFile.read('4f')
            ambient = self.inFile.read('4f')
            specular = self.inFile.read('4f')
            emissive = self.inFile.read('4f')
            power = self.inFile.readFloat()
            
            diffuseName = self.read_name()
            diffuseUnknown = self.inFile.readBytes(16)
            
            specName = self.read_name()
            self.inFile.readBytes(16)
           
            normName = self.read_name()
            self.inFile.readBytes(16)
            
            alphaMap = self.read_name()
            self.inFile.readBytes(16)            
            if self.xxFormat < 0:
                matUnk1 = self.inFile.readBytes(4)
            else:
                matUnk1 = self.inFile.readBytes(88)
                
            material = NoeMaterial(matName, "")
            material.setTexture(diffuseName)
            self.matList.append(material)
    
    def parse_faces(self, numIdx):

        idxList = self.inFile.readBytes(numIdx * 2)
        return idxList
        
    def parse_vertices(self, numVerts):

        if self.xxFormat >= 4:
            vertBuff = self.inFile.readBytes(numVerts*70)
        else:
            vertBuff = self.inFile.readBytes(numVerts*52) 
        return vertBuff
    
    def parse_dupe_vertices(self, numVertDupe):
        
        for i in range(numVertDupe):
            if self.xxFormat >= 4:
                index = self.inFile.readUShort()
            else:
                index = self.inFile.readUInt()
            
            self.inFile.readBytes(48)
            #vx, vy, vz = self.inFile.read('3f')
            #weights = self.inFile.read('3f')
            #boneIndices = self.inFile.read('4B')
            #nx, ny, nz = self.inFile.read('3f')
            #tu, tv = self.inFile.read('2f')
            
            if self.xxFormat >= 4:
                unk = self.inFile.readBytes(20)
            else:
                unk = 0
        
    def parse_bones(self, numBones):
        
        #DOES NOT WORK
        prev = ""
        for i in range(numBones):
            boneName = self.read_name()
            boneIndex = self.inFile.readUInt()
            boneMat = NoeMat44.fromBytes(self.inFile.readBytes(64)).toMat43()
            bone = NoeBone(boneIndex, boneName, boneMat, prev, i-1)
            self.boneList.append(bone)
            prev = boneName
    
    def parse_mesh(self, numMesh, name):
    
        if self.xxFormat > 6:
            meshName = self.read_name()
        else:
            meshName = name
        meshType = self.inFile.readByte()
        for i in range(numMesh):
            if numMesh == 1:
                objName = meshName
            else:
                objName = "%s[%d]" %(meshName, i)
    
            meshUnknown = self.inFile.read('4f')   #unknown
            matNum = self.inFile.readUInt()  #material index
            numIdx = self.inFile.readUInt()
            idxBuff = self.parse_faces(numIdx)
            
            numVerts = self.inFile.readUInt()
            vertBuff = self.parse_vertices(numVerts)
            
            unknownA = self.inFile.readBytes(numVerts * 8 * meshType)
            if self.xxFormat >= 2:
                unknownB = self.inFile.readBytes(100)
            if self.xxFormat >= 3:
                unknownC = self.inFile.readBytes(64)        
            if self.xxFormat >= 5:
                unknownD = self.inFile.readBytes(20)   
            if self.xxFormat >= 6:
                unknownE = self.inFile.readBytes(28)
            
            self.meshInfo.append([vertBuff, idxBuff, numIdx, matNum, objName])
        
        numVertDupe = self.inFile.readUShort()
        self.inFile.read('2L')
        self.parse_dupe_vertices(numVertDupe)     
        numBones = self.inFile.readUInt()
        self.parse_bones(numBones)
            
    def parse_frames(self):
        
        frameName = self.read_name()
        numChildren = self.inFile.readUInt()  #numChildFrames
        frameTransform = self.inFile.read('16f')    #transform matrix
        frameUnknownA = self.inFile.read('4f')      #unknown
        
        numMesh = self.inFile.readUInt()
        bbMin = self.inFile.read('3f')   #min bounds
        bbMax =  self.inFile.read('3f')   #max bounds
        frameUnknownB = self.inFile.read('4f')   #unknown
        if self.xxFormat == 6:
            unk5 = self.inFile.readUInt()

        if numMesh > 0:
            self.parse_mesh(numMesh, frameName)
        for j in range(numChildren):
            childFrames = self.parse_frames()
            
        #frame.name = frameName
        #frame.numChildren = numChildren
        #frame.transform = frameTransform
        #frame.unknownA = frameUnknownA
        #frame.numMesh = numMesh
        #frame.bbMin = bbMin
        #frame.bbMax = bbMax
        #frame.unknownB = frameUnknownB
        #frame.childFrames = childFrames
                    
    def parse_file(self):
        
        self.xxFormat = self.get_format()
        self.inFile.seek(0)
        
        #objectSection [0] unknown
        if self.xxFormat > 0:
            self.inFile.seek(26, 1)
        else:
            self.inFile.seek(21, 1)
        self.parse_frames()
        matSectUnknown = self.inFile.readBytes(4)
        numMat = self.inFile.readUInt()
        self.parse_materials(numMat)
        numTex = self.inFile.readUInt()
        self.parse_textures(numTex)
        
        ##unknown object end
        #if self.xxFormat < 2:
            #unk = 0
        #else:
            #unk = self.inFile.seek(10, 1)
            
        self.build_meshes()