import re
import struct
import textwrap

VERTEX_FORMAT = {
    'uint8x2': (0x1401, 2, 0, 1),
    'uint8x4': (0x1401, 4, 0, 1),
    'sint8x2': (0x1400, 2, 0, 1),
    'sint8x4': (0x1400, 4, 0, 1),
    'unorm8x2': (0x1401, 2, 1, 0),
    'unorm8x4': (0x1401, 4, 1, 0),
    'snorm8x2': (0x1400, 2, 1, 0),
    'snorm8x4': (0x1400, 4, 1, 0),
    'uint16x2': (0x1403, 2, 0, 1),
    'uint16x4': (0x1403, 4, 0, 1),
    'sint16x2': (0x1402, 2, 0, 1),
    'sint16x4': (0x1402, 4, 0, 1),
    'unorm16x2': (0x1403, 2, 1, 0),
    'unorm16x4': (0x1403, 4, 1, 0),
    'snorm16x2': (0x1402, 2, 1, 0),
    'snorm16x4': (0x1402, 4, 1, 0),
    'float16x2': (0x140B, 2, 0, 0),
    'float16x4': (0x140B, 4, 0, 0),
    'float32': (0x1406, 1, 0, 0),
    'float32x2': (0x1406, 2, 0, 0),
    'float32x3': (0x1406, 3, 0, 0),
    'float32x4': (0x1406, 4, 0, 0),
    'uint32': (0x1405, 1, 0, 1),
    'uint32x2': (0x1405, 2, 0, 1),
    'uint32x3': (0x1405, 3, 0, 1),
    'uint32x4': (0x1405, 4, 0, 1),
    'sint32': (0x1404, 1, 0, 1),
    'sint32x2': (0x1404, 2, 0, 1),
    'sint32x3': (0x1404, 3, 0, 1),
    'sint32x4': (0x1404, 4, 0, 1),
}

IMAGE_FORMAT = {
    'r8unorm': (0x8229, 0x1903, 0x1401, 0x1800, 1, 1, 1, 1, 'f'),
    'rg8unorm': (0x822B, 0x8227, 0x1401, 0x1800, 2, 2, 1, 1, 'f'),
    'rgba8unorm': (0x8058, 0x1908, 0x1401, 0x1800, 4, 4, 1, 1, 'f'),
    'r8snorm': (0x8F94, 0x1903, 0x1401, 0x1800, 1, 1, 1, 1, 'f'),
    'rg8snorm': (0x8F95, 0x8227, 0x1401, 0x1800, 2, 2, 1, 1, 'f'),
    'rgba8snorm': (0x8F97, 0x1908, 0x1401, 0x1800, 4, 4, 1, 1, 'f'),
    'r8uint': (0x8232, 0x8D94, 0x1401, 0x1800, 1, 1, 1, 1, 'u'),
    'rg8uint': (0x8238, 0x8228, 0x1401, 0x1800, 2, 2, 1, 1, 'u'),
    'rgba8uint': (0x8D7C, 0x8D99, 0x1401, 0x1800, 4, 4, 1, 1, 'u'),
    'r16uint': (0x8234, 0x8D94, 0x1403, 0x1800, 1, 2, 1, 1, 'u'),
    'rg16uint': (0x823A, 0x8228, 0x1403, 0x1800, 2, 4, 1, 1, 'u'),
    'rgba16uint': (0x8D76, 0x8D99, 0x1403, 0x1800, 4, 8, 1, 1, 'u'),
    'r32uint': (0x8236, 0x8D94, 0x1405, 0x1800, 1, 4, 1, 1, 'u'),
    'rg32uint': (0x823C, 0x8228, 0x1405, 0x1800, 2, 8, 1, 1, 'u'),
    'rgba32uint': (0x8D70, 0x8D99, 0x1405, 0x1800, 4, 16, 1, 1, 'u'),
    'r8sint': (0x8231, 0x8D94, 0x1400, 0x1800, 1, 1, 1, 1, 'i'),
    'rg8sint': (0x8237, 0x8228, 0x1400, 0x1800, 2, 2, 1, 1, 'i'),
    'rgba8sint': (0x8D8E, 0x8D99, 0x1400, 0x1800, 4, 4, 1, 1, 'i'),
    'r16sint': (0x8233, 0x8D94, 0x1402, 0x1800, 1, 2, 1, 1, 'i'),
    'rg16sint': (0x8239, 0x8228, 0x1402, 0x1800, 2, 4, 1, 1, 'i'),
    'rgba16sint': (0x8D88, 0x8D99, 0x1402, 0x1800, 4, 8, 1, 1, 'i'),
    'r32sint': (0x8235, 0x8D94, 0x1404, 0x1800, 1, 4, 1, 1, 'i'),
    'rg32sint': (0x823B, 0x8228, 0x1404, 0x1800, 2, 8, 1, 1, 'i'),
    'rgba32sint': (0x8D82, 0x8D99, 0x1404, 0x1800, 4, 16, 1, 1, 'i'),
    'r16float': (0x822D, 0x1903, 0x1406, 0x1800, 1, 2, 1, 1, 'f'),
    'rg16float': (0x822F, 0x8227, 0x1406, 0x1800, 2, 4, 1, 1, 'f'),
    'rgba16float': (0x881A, 0x1908, 0x1406, 0x1800, 4, 8, 1, 1, 'f'),
    'r32float': (0x822E, 0x1903, 0x1406, 0x1800, 1, 4, 1, 1, 'f'),
    'rg32float': (0x8230, 0x8227, 0x1406, 0x1800, 2, 8, 1, 1, 'f'),
    'rgba32float': (0x8814, 0x1908, 0x1406, 0x1800, 4, 16, 1, 1, 'f'),
    'rgba8unorm-srgb': (0x8C43, 0x1908, 0x1401, 0x1800, 4, 4, 1, 1, 'f'),
    'depth16unorm': (0x81A5, 0x1902, 0x1403, 0x1801, 1, 2, 0, 2, 'f'),
    'depth24plus': (0x81A6, 0x1902, 0x1405, 0x1801, 1, 4, 0, 2, 'f'),
    'depth24plus-stencil8': (0x88F0, 0x84F9, 0x84FA, 0x84F9, 2, 4, 0, 6, 'x'),
    'depth32float': (0x8CAC, 0x1902, 0x1406, 0x1801, 1, 4, 0, 2, 'f'),
}

TOPOLOGY = {
    'points': 0,
    'lines': 1,
    'line_loop': 2,
    'line_strip': 3,
    'triangles': 4,
    'triangle_strip': 5,
    'triangle_fan': 6,
}

SHORT_VERTEX_FORMAT = {
    '2u1': ('uint8x2', 2),
    '4u1': ('uint8x4', 4),
    '2i1': ('sint8x2', 2),
    '4i1': ('sint8x4', 4),
    '2nu1': ('unorm8x2', 2),
    '4nu1': ('unorm8x4', 4),
    '2ni1': ('snorm8x2', 2),
    '4ni1': ('snorm8x4', 4),
    '2u2': ('uint16x2', 4),
    '4u2': ('uint16x4', 8),
    '2i2': ('sint16x2', 4),
    '4i2': ('sint16x4', 8),
    '2nu2': ('unorm16x2', 4),
    '4nu2': ('unorm16x4', 8),
    '2ni2': ('snorm16x2', 4),
    '4ni2': ('snorm16x4', 8),
    '2h': ('float16x2', 4),
    '4h': ('float16x4', 8),
    '1f': ('float32', 4),
    '2f': ('float32x2', 8),
    '3f': ('float32x3', 12),
    '4f': ('float32x4', 16),
    '1u': ('uint32', 4),
    '2u': ('uint32x2', 8),
    '3u': ('uint32x3', 12),
    '4u': ('uint32x4', 16),
    '1i': ('sint32', 4),
    '2i': ('sint32x2', 8),
    '3i': ('sint32x3', 12),
    '4i': ('sint32x4', 16),
}

CULL_FACE = {
    'front': 0x0404,
    'back': 0x0405,
    'front_and_back': 0x0408,
    'none': 0,
}

MIN_FILTER = {
    'nearest': 0x2600,
    'linear': 0x2601,
    'nearest_mipmap_nearest': 0x2700,
    'linear_mipmap_nearest': 0x2701,
    'nearest_mipmap_linear': 0x2702,
    'linear_mipmap_linear': 0x2703,
}

MAG_FILTER = {
    'nearest': 0x2600,
    'linear': 0x2601,
}

TEXTURE_WRAP = {
    'repeat': 0x2901,
    'clamp_to_edge': 0x812F,
    'mirrored_repeat': 0x8370,
}

COMPARE_MODE = {
    'ref_to_texture': 0x884E,
    'none': 0,
}

COMPARE_FUNC = {
    'never': 0x0200,
    'less': 0x0201,
    'equal': 0x0202,
    'lequal': 0x0203,
    'greater': 0x0204,
    'notequal': 0x0205,
    'gequal': 0x0206,
    'always': 0x0207,
}

BLEND_FUNC = {
    'add': 0x8006,
    'subtract': 0x800A,
    'reverse_subtract': 0x800B,
    'min': 0x8007,
    'max': 0x8008,
}

BLEND_CONSTANT = {
    'zero': 0,
    'one': 1,
    'src_color': 0x0300,
    'one_minus_src_color': 0x0301,
    'src_alpha': 0x0302,
    'one_minus_src_alpha': 0x0303,
    'dst_alpha': 0x0304,
    'one_minus_dst_alpha': 0x0305,
    'dst_color': 0x0306,
    'one_minus_dst_color': 0x0307,
    'src_alpha_saturate': 0x0308,
    'constant_color': 0x8001,
    'one_minus_constant_color': 0x8002,
    'constant_alpha': 0x8003,
    'one_minus_constant_alpha': 0x8004,
    'src1_alpha': 0x8589,
    'src1_color': 0x88F9,
    'one_minus_src1_color': 0x88FA,
    'one_minus_src1_alpha': 0x88FB,
}

STENCIL_OP = {
    'zero': 0,
    'keep': 0x1E00,
    'replace': 0x1E01,
    'incr': 0x1E02,
    'decr': 0x1E03,
    'invert': 0x150A,
    'incr_wrap': 0x8507,
    'decr_wrap': 0x8508,
}

STEP = {
    'vertex': 0,
    'instance': 1,
}

VERTEX_SHADER_BUILTINS = {
    'gl_VertexID',
    'gl_InstanceID',
    'gl_DrawID',
    'gl_BaseVertex',
    'gl_BaseInstance',
}

UNIFORM_PACKER = {
    0x1404: (0, 1, 'i'),
    0x8B53: (1, 2, 'i'),
    0x8B54: (2, 3, 'i'),
    0x8B55: (3, 4, 'i'),
    0x8B56: (4, 1, 'i'),
    0x8B57: (5, 2, 'i'),
    0x8B58: (6, 3, 'i'),
    0x8B59: (7, 4, 'i'),
    0x1405: (8, 1, 'I'),
    0x8DC6: (9, 2, 'I'),
    0x8DC7: (10, 3, 'I'),
    0x8DC8: (11, 4, 'I'),
    0x1406: (12, 1, 'f'),
    0x8B50: (13, 2, 'f'),
    0x8B51: (14, 3, 'f'),
    0x8B52: (15, 4, 'f'),
    0x8B5A: (16, 4, 'f'),
    0x8B65: (17, 6, 'f'),
    0x8B66: (18, 8, 'f'),
    0x8B67: (19, 6, 'f'),
    0x8B5B: (20, 9, 'f'),
    0x8B68: (21, 12, 'f'),
    0x8B69: (22, 8, 'f'),
    0x8B6A: (23, 12, 'f'),
    0x8B5C: (24, 16, 'f'),
}


class DefaultLoader:
    def __init__(self):
        import ctypes
        import sys

        if sys.platform.startswith('win'):
            lib = ctypes.WinDLL('Opengl32.dll')
            proc = ctypes.cast(lib.wglGetProcAddress, ctypes.CFUNCTYPE(ctypes.c_ulonglong, ctypes.c_char_p))

            def loader(name):
                return proc(name.encode()) or ctypes.cast(lib[name], ctypes.c_void_p).value

        elif sys.platform.startswith('linux'):
            lib = ctypes.CDLL('libGL.so')
            proc = ctypes.cast(lib.glXGetProcAddress, ctypes.CFUNCTYPE(ctypes.c_ulonglong, ctypes.c_char_p))

            def loader(name):
                return proc(name.encode()) or ctypes.cast(lib[name], ctypes.c_void_p).value

        elif sys.platform.startswith('darwin'):
            lib = ctypes.CDLL('/System/Library/Frameworks/OpenGL.framework/OpenGL')

            def loader(name):
                return ctypes.cast(lib[name], ctypes.c_void_p).value

        elif sys.platform.startswith('emscripten'):
            loader = None

        self.load_opengl_function = loader


def loader(headless=False):
    if headless:
        import glcontext

        return glcontext.default_backend()(glversion=330, mode='standalone')
    return DefaultLoader()


def detect_gles(info):
    return info['version'].startswith('OpenGL ES') or info['version'].startswith('WebGL')


def calcsize(layout):
    nodes = layout.split(' ')
    if nodes[-1] == '/i':
        nodes.pop()
    stride = 0
    for node in nodes:
        if node[-1] == 'x':
            stride += int(node[:-1])
            continue
        stride += SHORT_VERTEX_FORMAT[node][1]
    return stride


def bind(buffer, layout, *attributes):
    nodes = layout.split(' ')
    step = 'vertex'
    if nodes[-1] == '/i':
        step = 'instance'
        nodes.pop()
    res = []
    offset = 0
    idx = 0
    for node in nodes:
        if node[-1] == 'x':
            offset += int(node[:-1])
            continue
        if len(attributes) == idx:
            raise ValueError(f'Not enough vertex attributes for format "{layout}"')
        location = attributes[idx]
        format, size = SHORT_VERTEX_FORMAT[node]
        if location >= 0:
            res.append(
                {
                    'location': location,
                    'buffer': buffer,
                    'format': format,
                    'offset': offset,
                    'step': step,
                }
            )
        offset += size
        idx += 1

    if len(attributes) != idx:
        raise ValueError(f'Too many vertex attributes for format "{layout}"')

    for x in res:
        x['stride'] = offset

    return res


def vertex_array_bindings(vertex_buffers, index_buffer):
    res = [index_buffer]
    for obj in vertex_buffers:
        buffer = obj['buffer']
        if buffer is not None:
            res.extend([buffer, obj['location'], obj['offset'], obj['stride'], STEP[obj['step']], obj['format']])
    return tuple(res)


def resource_bindings(resources):
    uniform_buffers = []
    for obj in sorted((x for x in resources if x['type'] == 'uniform_buffer'), key=lambda x: x['binding']):
        binding = obj['binding']
        buffer = obj['buffer']
        offset = obj.get('offset', 0)
        size = obj.get('size', buffer.size - offset)
        uniform_buffers.extend([binding, buffer, offset, size])

    samplers = []
    for obj in sorted((x for x in resources if x['type'] == 'sampler'), key=lambda x: x['binding']):
        params = (
            MIN_FILTER[obj.get('min_filter', 'linear')],
            MAG_FILTER[obj.get('mag_filter', 'linear')],
            float(obj.get('min_lod', -1000.0)),
            float(obj.get('max_lod', 1000.0)),
            float(obj.get('lod_bias', 0.0)),
            TEXTURE_WRAP[obj.get('wrap_x', 'repeat')],
            TEXTURE_WRAP[obj.get('wrap_y', 'repeat')],
            TEXTURE_WRAP[obj.get('wrap_z', 'repeat')],
            COMPARE_MODE[obj.get('compare_mode', 'none')],
            COMPARE_FUNC[obj.get('compare_func', 'never')],
            float(obj.get('max_anisotropy', 1.0)),
        )
        samplers.extend([obj['binding'], obj['image'], params])

    return tuple(uniform_buffers), tuple(samplers)


def framebuffer_attachments(attachments):
    if attachments is None:
        return None
    attachments = [x.face() if hasattr(x, 'face') else x for x in attachments]
    size = attachments[0].size
    samples = attachments[0].samples
    for attachment in attachments:
        if attachment.size != size:
            raise ValueError('Attachments must be images with the same size')
        if attachment.samples != samples:
            raise ValueError('Attachments must be images with the same number of samples')
    depth_stencil_attachment = None
    if not attachments[-1].flags & 1:
        depth_stencil_attachment = attachments[-1]
        attachments = attachments[:-1]
    for attachment in attachments:
        if not attachment.flags & 1:
            raise ValueError('The depth stencil attachments must be the last item in the framebuffer')
    return size, tuple(attachments), depth_stencil_attachment


def settings(cull_face, depth, stencil, blend, attachments):
    num_color_attachments = 1 if attachments is None else len(attachments[1])
    has_depth = attachments is None or (attachments[2] is not None and attachments[2].flags & 2)
    has_stencil = attachments is None or (attachments[2] is not None and attachments[2].flags & 4)

    res = [num_color_attachments, CULL_FACE[cull_face]]

    if depth is None:
        depth = {}

    if has_depth:
        res.extend([True, COMPARE_FUNC[depth.get('func', 'less')], bool(depth.get('write', True))])

    else:
        res.append(False)

    if stencil is None:
        stencil = {}

    if has_stencil:
        front = stencil.get('front', stencil.get('both', {}))
        back = stencil.get('back', stencil.get('both', {}))
        res.extend(
            [
                True,
                STENCIL_OP[front.get('fail_op', 'keep')],
                STENCIL_OP[front.get('pass_op', 'keep')],
                STENCIL_OP[front.get('depth_fail_op', 'keep')],
                COMPARE_FUNC[front.get('compare_op', 'always')],
                int(front.get('compare_mask', 0xFF)),
                int(front.get('write_mask', 0xFF)),
                int(front.get('reference', 0)),
                STENCIL_OP[back.get('fail_op', 'keep')],
                STENCIL_OP[back.get('pass_op', 'keep')],
                STENCIL_OP[back.get('depth_fail_op', 'keep')],
                COMPARE_FUNC[back.get('compare_op', 'always')],
                int(back.get('compare_mask', 0xFF)),
                int(back.get('write_mask', 0xFF)),
                int(back.get('reference', 0)),
            ]
        )

    else:
        res.append(False)

    if blend is not None:
        res.append(True)
        res.extend(
            [
                BLEND_FUNC[blend.get('op_color', 'add')],
                BLEND_FUNC[blend.get('op_alpha', 'add')],
                BLEND_CONSTANT[blend.get('src_color', 'one')],
                BLEND_CONSTANT[blend.get('dst_color', 'zero')],
                BLEND_CONSTANT[blend.get('src_alpha', 'one')],
                BLEND_CONSTANT[blend.get('dst_alpha', 'zero')],
            ]
        )

    else:
        res.append(False)

    return tuple(res)


def program(vertex_shader, fragment_shader, layout, includes):
    def include(match):
        name = match.group(1)
        content = includes.get(name)
        if content is None:
            raise KeyError(f'cannot include "{name}"')
        return content

    vert = textwrap.dedent(vertex_shader).strip()
    vert = re.sub(r'#include\s+"([^"]+)"', include, vert)
    vert = vert.encode().replace(b'\r', b'')

    frag = textwrap.dedent(fragment_shader).strip()
    frag = re.sub(r'#include\s+"([^"]+)"', include, frag)
    frag = frag.encode().replace(b'\r', b'')

    bindings = []
    for obj in sorted(layout, key=lambda x: x['name']):
        bindings.extend((obj['name'], obj['binding']))

    return (vert, 0x8B31), (frag, 0x8B30), tuple(bindings)


def compile_error(shader: bytes, shader_type: int, log: bytes):
    name = {0x8B31: 'Vertex Shader', 0x8B30: 'Fragment Shader'}[shader_type]
    log = log.rstrip(b'\x00').decode()
    raise ValueError(f'{name} Error\n\n{log}')


def linker_error(vertex_shader: bytes, fragment_shader: bytes, log: bytes):
    log = log.rstrip(b'\x00').decode()
    raise ValueError(f'Linker Error\n\n{log}')


def flatten(iterable):
    try:
        for x in iterable:
            yield from flatten(x)
    except TypeError:
        yield iterable


def clean_glsl_name(name):
    if name.endswith('[0]'):
        return name[:-3]
    return name


def uniforms(interface, selection, uniform_data):
    uniform_map = {clean_glsl_name(obj['name']): obj for obj in interface[1]}
    uniforms = []
    layout = bytearray()
    offset = 0

    layout.extend(struct.pack('i', len(selection)))
    for name, values in selection.items():
        if name not in uniform_map:
            raise KeyError(f'Uniform "{name}" does not exist')
        location = uniform_map[name]['location']
        size = uniform_map[name]['size']
        gltype = uniform_map[name]['gltype']
        if gltype not in UNIFORM_PACKER:
            raise ValueError(f'Uniform "{name}" has an unknown type')
        function, items, format = UNIFORM_PACKER[gltype]
        if values is None:
            values_count = size * items
            values = bytes(values_count * 4)
        else:
            values = tuple(flatten(values))
            values_count = len(values)
            values = b''.join(struct.pack(format, x) for x in values)
        count = values_count // items
        if values_count > size * items:
            raise ValueError(f'Uniform "{name}" must be {size * items} long at most')
        if values_count % items:
            raise ValueError(f'Uniform "{name}" must have a length divisible by {items}')
        layout.extend(struct.pack('4i', function, location, count, offset))
        uniforms.append((name, slice(offset, offset + len(values)), values))
        offset += len(values)

    data = uniform_data if uniform_data else memoryview(bytearray(offset))

    if len(data) != offset:
        raise ValueError(f'uniform_data must be {offset} bytes long')

    mapping = {}
    for name, idx, values in uniforms:
        data[idx] = values
        mapping[name] = data[idx]
    return mapping, memoryview(layout), data


def layout_bindings(layout):
    res = []
    if not layout:
        return res
    for obj in layout:
        name = str(obj['name'])
        binding = int(obj['binding'])
        res.append((name, binding))
    return res


def validate(interface, layout, resources, vertex_buffers, limits):
    attributes, uniforms, uniform_buffers = interface
    attributes = [
        {
            'name': obj['name'].replace('[0]', f'[{i:d}]'),
            'location': obj['location'] + i if obj['location'] >= 0 else -1,
        }
        for obj in attributes
        for i in range(obj['size'])
        if obj['name'] not in VERTEX_SHADER_BUILTINS
    ]
    uniforms = [
        {
            'name': obj['name'].replace('[0]', f'[{i}]'),
            'location': obj['location'] + i if obj['location'] >= 0 else -1,
        }
        for obj in uniforms
        for i in range(obj['size'])
        if obj['gltype'] not in UNIFORM_PACKER
    ]
    bound_attributes = set()
    bound_uniforms = set()
    bound_uniform_buffers = set()
    uniform_binding_map = {}
    uniform_buffer_binding_map = {}
    attribute_map = {obj['location']: obj for obj in attributes}
    uniform_map = {obj['name']: obj for obj in uniforms}
    uniform_buffer_map = {obj['name']: obj for obj in uniform_buffers}
    layout_map = {obj['name']: obj for obj in layout}
    uniform_buffer_resources = {obj['binding']: obj for obj in resources if obj['type'] == 'uniform_buffer'}
    sampler_resources = {obj['binding']: obj for obj in resources if obj['type'] == 'sampler'}
    max_uniform_block_size = limits['max_uniform_block_size']

    for obj in uniform_buffers:
        if obj['size'] > max_uniform_block_size:
            raise ValueError(
                f'Uniform buffer "{obj["name"]}" is too large, the maximum supported size is {max_uniform_block_size}'
            )

    for obj in vertex_buffers:
        location = obj['location']
        if location < 0:
            continue
        if location not in attribute_map:
            raise ValueError(f'Invalid vertex attribute location {location}')
        if location in bound_attributes:
            name = attribute_map[location]['name']
            raise ValueError(f'Duplicate vertex attribute binding for "{name}" at location {location}')
        bound_attributes.add(location)

    for obj in attributes:
        location = obj['location']
        if location < 0:
            continue
        if location not in bound_attributes:
            name = obj['name']
            raise ValueError(f'Unbound vertex attribute "{name}" at location {location}')

    for obj in layout:
        name = obj['name']
        binding = obj['binding']
        if name in uniform_map:
            uniform_binding_map[binding] = obj
        elif name in uniform_buffer_map:
            uniform_buffer_binding_map[binding] = obj
        else:
            raise ValueError(f'Cannot set layout binding for "{name}"')

    for obj in uniforms:
        name = obj['name']
        location = obj['location']
        if location < 0:
            continue
        if name not in layout_map:
            raise ValueError(f'Missing layout binding for "{name}"')
        binding = layout_map[name]['binding']
        if binding not in sampler_resources:
            raise ValueError(f'Missing resource for "{name}" with binding {binding}')

    for obj in uniform_buffers:
        name = obj['name']
        if name not in layout_map:
            raise ValueError(f'Missing layout binding for "{name}"')
        binding = layout_map[name]['binding']
        if binding not in uniform_buffer_resources:
            raise ValueError(f'Missing resource for "{name}" with binding {binding}')

    for obj in resources:
        resource_type = obj['type']
        binding = obj['binding']
        if resource_type == 'uniform_buffer':
            buffer = obj['buffer']
            if binding not in uniform_buffer_binding_map:
                raise ValueError(f'Uniform buffer binding {binding} does not exist')
            name = uniform_buffer_binding_map[binding]['name']
            if binding in bound_uniform_buffers:
                raise ValueError(f'Duplicate uniform buffer binding for "{name}" with binding {binding}')
            size = uniform_buffer_map[name]['size']
            if buffer.size < size:
                raise ValueError(
                    f'Uniform buffer is too small {buffer.size} is less than '
                    f'{size} for "{name}" with binding {binding}'
                )
            bound_uniform_buffers.add(binding)
        elif resource_type == 'sampler':
            image = obj['image']
            if binding not in uniform_binding_map:
                raise ValueError(f'Sampler binding {binding} does not exist')
            name = uniform_binding_map[binding]['name']
            if binding in bound_uniforms:
                raise ValueError(f'Duplicate sampler binding for "{name}" with binding {binding}')
            if image.samples != 1:
                raise ValueError(f'Multisample images cannot be attached to "{name}" with binding {binding}')
            bound_uniforms.add(binding)
        else:
            raise ValueError(f'Invalid resource type "{resource_type}"')
