import struct
from typing import List
import renderdoc

# Alias for convenience - we need to import as-is so types don't get confused
rd = renderdoc


def open_capture(filename="", cap: rd.CaptureFile=None, opts: rd.ReplayOptions=None):
    """
    Opens a capture file and begins a replay.

    :param filename: The filename to open, or empty if cap is used.
    :param cap: The capture file to use, or ``None`` if a filename is given.
    :param opts: The replay options to use, or ``None`` to use the default options.
    :return: A replay controller for the capture
    :rtype: renderdoc.ReplayController
    """

    if opts is None:
        opts = rd.ReplayOptions()

    # Open a capture file handle
    own_cap = False
    api = "Unknown"

    if cap is None:
        own_cap = True

        cap = rd.OpenCaptureFile()

        # Open a particular file
        status = cap.OpenFile(filename, '', None)

        # Make sure the file opened successfully
        if status != rd.ReplayStatus.Succeeded:
            cap.Shutdown()
            raise RuntimeError("Couldn't open '{}': {}".format(filename, str(status)))

        api = cap.DriverName()

        # Make sure we can replay
        if not cap.LocalReplaySupport():
            cap.Shutdown()
            raise RuntimeError("{} capture cannot be replayed".format(api))

    status, controller = cap.OpenCapture(opts, None)

    if own_cap:
        cap.Shutdown()

    if status != rd.ReplayStatus.Succeeded:
        raise RuntimeError("Couldn't initialise replay for {}: {}".format(api, str(rd.ReplayStatus(status))))

    return controller


def fetch_indices(controller: rd.ReplayController, draw: rd.DrawcallDescription, mesh: rd.MeshFormat, index_offset: int, first_index: int, num_indices: int):

    pipe = controller.GetPipelineState()
    restart_idx = pipe.GetStripRestartIndex() & ((1 << (mesh.indexByteStride*8)) - 1)
    restart_enabled = pipe.IsStripRestartEnabled() and rd.IsStrip(pipe.GetPrimitiveTopology())

    # If we have an index buffer
    if mesh.indexResourceId != rd.ResourceId.Null():
        offset = mesh.indexByteStride*(first_index + index_offset)

        avail_bytes = mesh.indexByteSize
        if avail_bytes > offset:
            avail_bytes = avail_bytes - offset
        else:
            avail_bytes = 0

        read_bytes = min([avail_bytes, mesh.indexByteStride*num_indices])

        # Fetch the data
        if read_bytes > 0:
            ibdata = controller.GetBufferData(mesh.indexResourceId,
                                              mesh.indexByteOffset + offset,
                                              read_bytes)
        else:
            ibdata = bytes()

        # Get the character for the width of index
        index_fmt = 'B'
        if mesh.indexByteStride == 2:
            index_fmt = 'H'
        elif mesh.indexByteStride == 4:
            index_fmt = 'I'

        avail_indices = int(len(ibdata) / mesh.indexByteStride)

        # Duplicate the format by the number of indices
        index_fmt = '=' + str(min([avail_indices, num_indices])) + index_fmt

        # Unpack all the indices
        indices = struct.unpack_from(index_fmt, ibdata)

        extra = []
        if avail_indices < num_indices:
            extra = [None] * (num_indices - avail_indices)

        # Apply the baseVertex offset
        return [i if restart_enabled and i == restart_idx else i + mesh.baseVertex for i in indices] + extra
    else:
        # With no index buffer, just generate a range
        return tuple(range(first_index, first_index + num_indices))


class MeshAttribute:
    mesh: rd.MeshFormat
    name: str


def get_vsin_attrs(controller: rd.ReplayController, vertexOffset: int, index_mesh: rd.MeshFormat):
    pipe: rd.PipeState = controller.GetPipelineState()
    inputs: List[rd.VertexInputAttribute] = pipe.GetVertexInputs()

    attrs: List[MeshAttribute] = []
    vbs: List[rd.BoundVBuffer] = pipe.GetVBuffers()

    for a in inputs:
        if not a.used:
            continue

        attr = MeshAttribute()
        attr.name = a.name
        attr.mesh = rd.MeshFormat(index_mesh)

        attr.mesh.vertexByteStride = vbs[a.vertexBuffer].byteStride
        attr.mesh.instStepRate = a.instanceRate
        attr.mesh.instanced = a.perInstance
        attr.mesh.vertexResourceId = vbs[a.vertexBuffer].resourceId

        offs = a.byteOffset + vertexOffset * attr.mesh.vertexByteStride

        attr.mesh.vertexByteOffset = vbs[a.vertexBuffer].byteOffset + offs
        attr.mesh.vertexByteSize = max([0, vbs[a.vertexBuffer].byteSize - offs])

        attr.mesh.format = a.format

        attrs.append(attr)

    return attrs


def get_postvs_attrs(controller: rd.ReplayController, mesh: rd.MeshFormat, data_stage: rd.MeshDataStage):
    pipe: rd.PipeState = controller.GetPipelineState()

    if data_stage == rd.MeshDataStage.VSOut:
        shader = pipe.GetShaderReflection(rd.ShaderStage.Vertex)
    else:
        shader = pipe.GetShaderReflection(rd.ShaderStage.Geometry)
        if shader is None:
            shader = pipe.GetShaderReflection(rd.ShaderStage.Domain)

    attrs: List[MeshAttribute] = []
    posidx = 0

    for sig in shader.outputSignature:
        attr = MeshAttribute()
        attr.mesh = rd.MeshFormat(mesh)

        # Construct a resource format for this element
        attr.mesh.format = rd.ResourceFormat()
        attr.mesh.format.compByteWidth = 8 if sig.varType == rd.VarType.Double else 4
        attr.mesh.format.compCount = sig.compCount
        attr.mesh.format.compType = rd.VarTypeCompType(sig.varType)
        attr.mesh.format.type = rd.ResourceFormatType.Regular

        attr.name = sig.semanticIdxName if sig.varName == '' else sig.varName

        if sig.systemValue == rd.ShaderBuiltin.Position:
            posidx = len(attrs)

        attrs.append(attr)

    # Shuffle the position element to the front
    if posidx > 0:
        pos = attrs[posidx]
        del attrs[posidx]
        attrs.insert(0, pos)

    accum_offset = 0

    for i in range(0, len(attrs)):
        # Note that some APIs such as Vulkan will pad the size of the attribute here
        # while others will tightly pack
        fmt = attrs[i].mesh.format

        elem_size = (8 if fmt.compByteWidth > 4 else 4)

        alignment = elem_size
        if fmt.compCount == 2:
            alignment = elem_size * 2
        elif fmt.compCount > 2:
            alignment = elem_size * 4

        if pipe.HasAlignedPostVSData(data_stage) and (accum_offset % alignment) != 0:
            accum_offset += alignment - (accum_offset % alignment)

        attrs[i].mesh.vertexByteOffset += accum_offset

        accum_offset += elem_size * fmt.compCount

    return attrs


# Unpack a tuple of the given format, from the data
def unpack_data(fmt: rd.ResourceFormat, data: bytes, data_offset: int):
    # We don't handle 'special' formats - typically bit-packed such as 10:10:10:2
    if fmt.Special():
        raise RuntimeError("Packed formats are not supported!")

    format_chars = {
        #                   012345678
        rd.CompType.UInt:  "xBHxIxxxQ",
        rd.CompType.SInt:  "xbhxixxxq",
        rd.CompType.Float: "xxexfxxxd",  # only 2, 4 and 8 are valid
    }

    # These types have identical decodes, but we might post-process them
    format_chars[rd.CompType.UNorm] = format_chars[rd.CompType.UInt]
    format_chars[rd.CompType.UScaled] = format_chars[rd.CompType.UInt]
    format_chars[rd.CompType.SNorm] = format_chars[rd.CompType.SInt]
    format_chars[rd.CompType.SScaled] = format_chars[rd.CompType.SInt]

    # We need to fetch compCount components
    vertex_format = '=' + str(fmt.compCount) + format_chars[fmt.compType][fmt.compByteWidth]

    if data_offset >= len(data):
        return None

    # Unpack the data
    try:
        value = struct.unpack_from(vertex_format, data, data_offset)
    except struct.error as ex:
        raise

    # If the format needs post-processing such as normalisation, do that now
    if fmt.compType == rd.CompType.UNorm:
        divisor = float((1 << (fmt.compByteWidth*8)) - 1)
        value = tuple(float(i) / divisor for i in value)
    elif fmt.compType == rd.CompType.SNorm:
        max_neg = -(1 << (fmt.compByteWidth*8 - 1))
        divisor = -float(max_neg+1)
        value = tuple(-1.0 if (i == max_neg) else float(i / divisor) for i in value)
    elif fmt.compType == rd.CompType.UScaled or fmt.compType == rd.CompType.SScaled:
        value = tuple(float(i) for i in value)

    # If the format is BGRA, swap the two components
    if fmt.BGRAOrder():
        value = tuple(value[i] for i in [2, 1, 0, 3])

    return value


def decode_mesh_data(controller: rd.ReplayController, indices: List[int], display_indices: List[int],
                     attrs: List[MeshAttribute], instance: int = 0, indexOffset: int = 0):
    ret = []

    buffer_ranges = {}
    for attr in attrs:
        begin = attr.mesh.vertexByteOffset
        end = min(begin + attr.mesh.vertexByteSize, 0xffffffffffffffff)

        # This could be more optimal if we figure out the lower/upper bounds of any attribute and only fetch the
        # data we need. For each referenced buffer, pick the attribute that references the largest range and fetch that
        if attr.mesh.vertexResourceId in buffer_ranges:
            buf_range = buffer_ranges[attr.mesh.vertexResourceId]

            if buf_range[0] < begin:
                begin = buf_range[0]
            if buf_range[1] > end:
                end = buf_range[1]

        buffer_ranges[attr.mesh.vertexResourceId] = (begin, end)

    buffer_data = {}
    for buf, buf_range in buffer_ranges.items():
        buffer_data[buf] = controller.GetBufferData(buf, buf_range[0], buf_range[1] - buf_range[0])

    # Calculate the strip restart index for this index width
    striprestart_index = None
    if controller.GetPipelineState().IsStripRestartEnabled() and attrs[0].mesh.indexResourceId != rd.ResourceId.Null():
        striprestart_index = (controller.GetPipelineState().GetStripRestartIndex() &
                              ((1 << (attrs[0].mesh.indexByteStride*8)) - 1))

    for i,idx in enumerate(indices):
        vertex = {'vtx': i, 'idx': display_indices[i]}

        if striprestart_index is None or idx != striprestart_index:
            for attr in attrs:
                if idx is None:
                    vertex[attr.name] = None
                    continue

                offset = attr.mesh.vertexByteStride * idx

                if attr.mesh.instanced:
                    offset = (attr.mesh.vertexByteStride +
                              attr.mesh.vertexByteStride * int(instance / max(attr.mesh.instStepRate, 1)))

                vertex[attr.name] = unpack_data(attr.mesh.format, buffer_data[attr.mesh.vertexResourceId],
                                                attr.mesh.vertexByteOffset + offset -
                                                buffer_ranges[attr.mesh.vertexResourceId][0])

        ret.append(vertex)

    return ret
