#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later

######################################################
# Importing modules
######################################################

import os
import struct
import gzip
import tempfile

import logging
log = logging.getLogger("BlendFileReader")

######################################################
# module global routines
######################################################


def ReadString(handle, length):
    '''
    ReadString reads a String of given length or a zero terminating String
    from a file handle
    '''
    if length != 0:
        return handle.read(length).decode()
    else:
        # length == 0 means we want a zero terminating string
        result = ""
        s = ReadString(handle, 1)
        while s != "\0":
            result += s
            s = ReadString(handle, 1)
        return result


def Read(type, handle, fileheader):
    '''
    Reads the chosen type from a file handle
    '''
    def unpacked_bytes(type_char, size):
        return struct.unpack(fileheader.StructPre + type_char, handle.read(size))[0]

    if type == 'ushort':
        return unpacked_bytes("H", 2)   # unsigned short
    elif type == 'short':
        return unpacked_bytes("h", 2)   # short
    elif type == 'uint':
        return unpacked_bytes("I", 4)   # unsigned int
    elif type == 'int':
        return unpacked_bytes("i", 4)   # int
    elif type == 'float':
        return unpacked_bytes("f", 4)   # float
    elif type == 'ulong':
        return unpacked_bytes("Q", 8)   # unsigned long
    elif type == 'pointer':
        # The pointersize is given by the header (BlendFileHeader).
        if fileheader.PointerSize == 4:
            return Read('uint', handle, fileheader)
        if fileheader.PointerSize == 8:
            return Read('ulong', handle, fileheader)


def openBlendFile(filename):
    '''
    Open a filename, determine if the file is compressed and returns a handle
    '''
    handle = open(filename, 'rb')
    magic = ReadString(handle, 7)
    if magic in {"BLENDER", "BULLETf"}:
        log.debug("normal blendfile detected")
        handle.seek(0, os.SEEK_SET)
        return handle
    else:
        log.debug("gzip blendfile detected?")
        handle.close()
        log.debug("decompressing started")
        fs = gzip.open(filename, "rb")
        handle = tempfile.TemporaryFile()
        data = fs.read(1024 * 1024)
        while data:
            handle.write(data)
            data = fs.read(1024 * 1024)
        log.debug("decompressing finished")
        fs.close()
        log.debug("resetting decompressed file")
        handle.seek(0, os.SEEK_SET)
        return handle


def Align(handle):
    '''
    Aligns the filehandle on 4 bytes
    '''
    offset = handle.tell()
    trim = offset % 4
    if trim != 0:
        handle.seek(4 - trim, os.SEEK_CUR)


######################################################
# module classes
######################################################

class BlendFile:
    '''
    Reads a blendfile and store the header, all the fileblocks, and catalogue
    structs found in the DNA fileblock

    - BlendFile.Header  (BlendFileHeader instance)
    - BlendFile.Blocks  (list of BlendFileBlock instances)
    - BlendFile.Catalog (DNACatalog instance)
    '''

    def __init__(self, handle):
        log.debug("initializing reading blend-file")
        self.Header = BlendFileHeader(handle)
        self.Blocks = []
        fileblock = BlendFileBlock(handle, self)
        found_dna_block = False
        while not found_dna_block:
            if fileblock.Header.Code in {"DNA1", "SDNA"}:
                self.Catalog = DNACatalog(self.Header, handle)
                found_dna_block = True
            else:
                fileblock.Header.skip(handle)

            self.Blocks.append(fileblock)
            fileblock = BlendFileBlock(handle, self)

        # appending last fileblock, "ENDB"
        self.Blocks.append(fileblock)

    # seems unused?
    """
    def FindBlendFileBlocksWithCode(self, code):
        #result = []
        #for block in self.Blocks:
            #if block.Header.Code.startswith(code) or block.Header.Code.endswith(code):
                #result.append(block)
        #return result
    """


class BlendFileHeader:
    '''
    BlendFileHeader allocates the first 12 bytes of a blend file.
    It contains information about the hardware architecture.
    Header example: BLENDER_v254

    BlendFileHeader.Magic             (str)
    BlendFileHeader.PointerSize       (int)
    BlendFileHeader.LittleEndianness  (bool)
    BlendFileHeader.StructPre         (str)   see http://docs.python.org/py3k/library/struct.html#byte-order-size-and-alignment
    BlendFileHeader.Version           (int)
    '''

    def __init__(self, handle):
        log.debug("reading blend-file-header")

        self.Magic = ReadString(handle, 7)
        log.debug(self.Magic)

        pointersize = ReadString(handle, 1)
        log.debug(pointersize)
        if pointersize == "-":
            self.PointerSize = 8
        if pointersize == "_":
            self.PointerSize = 4

        endianness = ReadString(handle, 1)
        log.debug(endianness)
        if endianness == "v":
            self.LittleEndianness = True
            self.StructPre = "<"
        if endianness == "V":
            self.LittleEndianness = False
            self.StructPre = ">"

        version = ReadString(handle, 3)
        log.debug(version)
        self.Version = int(version)

        log.debug("{0} {1} {2} {3}".format(self.Magic, self.PointerSize, self.LittleEndianness, version))


class BlendFileBlock:
    '''
    BlendFileBlock.File     (BlendFile)
    BlendFileBlock.Header   (FileBlockHeader)
    '''

    def __init__(self, handle, blendfile):
        self.File = blendfile
        self.Header = FileBlockHeader(handle, blendfile.Header)

    def Get(self, handle, path):
        log.debug("find dna structure")
        dnaIndex = self.Header.SDNAIndex
        dnaStruct = self.File.Catalog.Structs[dnaIndex]
        log.debug("found " + dnaStruct.Type.Name)
        handle.seek(self.Header.FileOffset, os.SEEK_SET)
        return dnaStruct.GetField(self.File.Header, handle, path)


class FileBlockHeader:
    '''
    FileBlockHeader contains the information in a file-block-header.
    The class is needed for searching to the correct file-block (containing Code: DNA1)

    Code        (str)
    Size        (int)
    OldAddress  (pointer)
    SDNAIndex   (int)
    Count       (int)
    FileOffset  (= file pointer of datablock)
    '''

    def __init__(self, handle, fileheader):
        self.Code = ReadString(handle, 4).strip()
        if self.Code != "ENDB":
            self.Size = Read('uint', handle, fileheader)
            self.OldAddress = Read('pointer', handle, fileheader)
            self.SDNAIndex = Read('uint', handle, fileheader)
            self.Count = Read('uint', handle, fileheader)
            self.FileOffset = handle.tell()
        else:
            self.Size = Read('uint', handle, fileheader)
            self.OldAddress = 0
            self.SDNAIndex = 0
            self.Count = 0
            self.FileOffset = handle.tell()
        #self.Code += ' ' * (4 - len(self.Code))
        log.debug("found blend-file-block-fileheader {0} {1}".format(self.Code, self.FileOffset))

    def skip(self, handle):
        handle.read(self.Size)


class DNACatalog:
    '''
    DNACatalog is a catalog of all information in the DNA1 file-block

    Header = None
    Names = None
    Types = None
    Structs = None
    '''

    def __init__(self, fileheader, handle):
        log.debug("building DNA catalog")
        self.Names = []
        self.Types = []
        self.Structs = []
        self.Header = fileheader

        SDNA = ReadString(handle, 4)

        # names
        NAME = ReadString(handle, 4)
        numberOfNames = Read('uint', handle, fileheader)
        log.debug("building #{0} names".format(numberOfNames))
        for i in range(numberOfNames):
            name = ReadString(handle, 0)
            self.Names.append(DNAName(name))
        Align(handle)

        # types
        TYPE = ReadString(handle, 4)
        numberOfTypes = Read('uint', handle, fileheader)
        log.debug("building #{0} types".format(numberOfTypes))
        for i in range(numberOfTypes):
            type = ReadString(handle, 0)
            self.Types.append(DNAType(type))
        Align(handle)

        # type lengths
        TLEN = ReadString(handle, 4)
        log.debug("building #{0} type-lengths".format(numberOfTypes))
        for i in range(numberOfTypes):
            length = Read('ushort', handle, fileheader)
            self.Types[i].Size = length
        Align(handle)

        # structs
        STRC = ReadString(handle, 4)
        numberOfStructures = Read('uint', handle, fileheader)
        log.debug("building #{0} structures".format(numberOfStructures))
        for structureIndex in range(numberOfStructures):
            type = Read('ushort', handle, fileheader)
            Type = self.Types[type]
            structure = DNAStructure(Type)
            self.Structs.append(structure)

            numberOfFields = Read('ushort', handle, fileheader)
            for fieldIndex in range(numberOfFields):
                fTypeIndex = Read('ushort', handle, fileheader)
                fNameIndex = Read('ushort', handle, fileheader)
                fType = self.Types[fTypeIndex]
                fName = self.Names[fNameIndex]
                structure.Fields.append(DNAField(fType, fName))


class DNAName:
    '''
    DNAName is a C-type name stored in the DNA.

    Name = str
    '''

    def __init__(self, name):
        self.Name = name

    def AsReference(self, parent):
        if parent is None:
            result = ""
        else:
            result = parent + "."

        result = result + self.ShortName()
        return result

    def ShortName(self):
        result = self.Name
        result = result.replace("*", "")
        result = result.replace("(", "")
        result = result.replace(")", "")
        Index = result.find("[")
        if Index != -1:
            result = result[0:Index]
        return result

    def IsPointer(self):
        return self.Name.find("*") > -1

    def IsMethodPointer(self):
        return self.Name.find("(*") > -1

    def ArraySize(self):
        result = 1
        Temp = self.Name
        Index = Temp.find("[")

        while Index != -1:
            Index2 = Temp.find("]")
            result *= int(Temp[Index + 1:Index2])
            Temp = Temp[Index2 + 1:]
            Index = Temp.find("[")

        return result


class DNAType:
    '''
    DNAType is a C-type stored in the DNA

    Name = str
    Size = int
    Structure = DNAStructure
    '''

    def __init__(self, aName):
        self.Name = aName
        self.Structure = None


class DNAStructure:
    '''
    DNAType is a C-type structure stored in the DNA

    Type = DNAType
    Fields = [DNAField]
    '''

    def __init__(self, aType):
        self.Type = aType
        self.Type.Structure = self
        self.Fields = []

    def GetField(self, header, handle, path):
        splitted = path.partition(".")
        name = splitted[0]
        rest = splitted[2]
        offset = 0
        for field in self.Fields:
            if field.Name.ShortName() == name:
                log.debug("found " + name + "@" + str(offset))
                handle.seek(offset, os.SEEK_CUR)
                return field.DecodeField(header, handle, rest)
            else:
                offset += field.Size(header)

        log.debug("error did not find " + path)
        return None


class DNAField:
    '''
    DNAField is a coupled DNAType and DNAName.

    Type = DNAType
    Name = DNAName
    '''

    def __init__(self, aType, aName):
        self.Type = aType
        self.Name = aName

    def Size(self, header):
        if self.Name.IsPointer() or self.Name.IsMethodPointer():
            return header.PointerSize * self.Name.ArraySize()
        else:
            return self.Type.Size * self.Name.ArraySize()

    def DecodeField(self, header, handle, path):
        if path == "":
            if self.Name.IsPointer():
                return Read('pointer', handle, header)
            if self.Type.Name == "int":
                return Read('int', handle, header)
            if self.Type.Name == "short":
                return Read('short', handle, header)
            if self.Type.Name == "float":
                return Read('float', handle, header)
            if self.Type.Name == "char":
                return ReadString(handle, self.Name.ArraySize())
        else:
            return self.Type.Structure.GetField(header, handle, path)
