///////////////////////////////////////////////////////////////////////////////
  const mat4 = glMatrix.mat4;

  const init = async () => {
    // Init
    /////////////////////////////////////////////////////////////////////////
    const gpu = navigator.gpu;
    const adapter = await gpu.requestAdapter();
    const device = await adapter.requestDevice();
    const canvas = document.getElementById("my_canvas");
    const context = canvas.getContext("webgpu");
    /////////////////////////////////////////////////////////////////////////

    // Swap Chain
    /////////////////////////////////////////////////////////////////////////
    const presentationFormat = gpu.getPreferredCanvasFormat();

    context.configure({
      device, // Create link between GPU and canvas.
      format: presentationFormat,
      alphaMode: "opaque"
    });
    /////////////////////////////////////////////////////////////////////////

    // SetUp Vertex (position (vec4), color(vec4))
    /////////////////////////////////////////////////////////////////////////
    // Pack them all into one array
    // Each vertex has a position and a color packed in memory in (x, y, z, w) / (xn, yn, zn) order
    const vertices = new Float32Array([
      // Vertex 0
      -0.5, -0.5, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0,
      // Vertex 1
      0.5, -0.5, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0,
      // Vertex 2
      0.5, 0.5, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0,
      // Vertex 3
      -0.5, 0.5, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0
    ]);
    /////////////////////////////////////////////////////////////////////////

    // SetUp Indexes
    /////////////////////////////////////////////////////////////////////////
    let indices = new Uint16Array([
      0, 1, 2,
      0, 2, 3
    ]);
    /////////////////////////////////////////////////////////////////////////

    // Define Basic Shader
    /////////////////////////////////////////////////////////////////////////
    const shaderModule = device.createShaderModule({
      code: `
      @group(0) @binding(0) var viewProjection : mat4x4;
      @group(0) @binding(1) var model : mat4x4;

      struct VertexOut {
        @builtin(position) position : vec4,
        @location(0) color : vec4,
      };
      
      @vertex
      fn vertex_main(@location(0) position: vec4,
        @location(1) color: vec4) -> VertexOut
      {
        var output : VertexOut;
        output.position = viewProjection * model * position;
        output.color = color;
        return output;
      }  
      
      @fragment
      fn fragment_main(fragData: VertexOut) -> @location(0) vec4
      {
        return fragData.color;
      }
      `,
    });
    /////////////////////////////////////////////////////////////////////////

    // Create Camera
    /////////////////////////////////////////////////////////////////////////
    const cameraPosition = [0, 1, 2];
    const cameraTarget = [0, 0, 0];
    const cameraUp = [0, 1, 0];
    const viewMatrix = mat4.create();
    mat4.lookAt(viewMatrix, cameraPosition, cameraTarget, cameraUp);

    const aspect = canvas.width / canvas.height;
    const fieldOfView = (45 * Math.PI) / 180;
    const near = 0.1;
    const far = 100.0;
    const projectionMatrix = mat4.create();
    mat4.perspective(projectionMatrix, fieldOfView, aspect, near, far);

    const viewProjectionMatrix = mat4.create();
    mat4.multiply(viewProjectionMatrix, projectionMatrix, viewMatrix);

    const viewProjectionBuffer = device.createBuffer({
      size: viewProjectionMatrix.byteLength,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
      mappedAtCreation: true
    });

    new Float32Array(viewProjectionBuffer.getMappedRange()).set(viewProjectionMatrix);
    viewProjectionBuffer.unmap();

    /////////////////////////////////////////////////////////////////////////

    // Vertex Buffer
    /////////////////////////////////////////////////////////////////////////
    const vertexBuffer = device.createBuffer({
      size: vertices.byteLength,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
      mappedAtCreation: true,
    });

    new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
    vertexBuffer.unmap();

    // Vertex Descriptor
    /////////////////////////////////////////////////////////////////////////
    const vertexBuffersDescriptors = [
      {
        attributes: [
          {
            shaderLocation: 0,
            offset: 0,
            format: "float32x4",
          },
          {
            shaderLocation: 1,
            offset: 16,
            format: "float32x4",
          },
        ],
        arrayStride: 32,
        stepMode: "vertex",
      },
    ];
    /////////////////////////////////////////////////////////////////////////

    // Index Buffers
    /////////////////////////////////////////////////////////////////////////
    const indexBuffer = device.createBuffer({
      size: indices.byteLength,
      usage: GPUBufferUsage.INDEX,
      mappedAtCreation: true,
    });

    new Uint16Array(indexBuffer.getMappedRange()).set(indices);
    indexBuffer.unmap();
    /////////////////////////////////////////////////////////////////////////

    // Create render pipeline
    /////////////////////////////////////////////////////////////////////////
    const pipeline = device.createRenderPipeline({
      layout: "auto",
      vertex: {
        module: shaderModule,
        entryPoint: "vertex_main",
        buffers: vertexBuffersDescriptors,
      },
      fragment: {
        module: shaderModule,
        entryPoint: "fragment_main",
        targets: [
          {
            format: presentationFormat,
          },
        ],
      },
      primitive: {
        topology: "triangle-list",
        indexFormat: "uint16",
      },
    });
    /////////////////////////////////////////////////////////////////////////

    // Define render loop
    function frame() {

      // Add Transformations
      /////////////////////////////////////////////////////////////////////////

      const modelMatrix = mat4.create();

      // Add rotation to the model matrix
      const time = performance.now() / 750;
      mat4.rotateX(modelMatrix, modelMatrix, time * 0.1);
      mat4.rotateY(modelMatrix, modelMatrix, time * 0.2);
      mat4.rotateZ(modelMatrix, modelMatrix, time * 0.3);

      const modelBuffer = device.createBuffer({
        size: modelMatrix.byteLength,
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
        mappedAtCreation: true
      });

      new Float32Array(modelBuffer.getMappedRange()).set(modelMatrix);
      modelBuffer.unmap();

      /////////////////////////////////////////////////////////////////////////

      // Pass the camera & model uniforms
      /////////////////////////////////////////////////////////////////////////
      const bindGroupLayout = pipeline.getBindGroupLayout(0);

      const bindGroup = device.createBindGroup({
        layout: bindGroupLayout,
        entries: [
          {
            binding: 0,
            resource: {
              buffer: viewProjectionBuffer,
              offset: 0,
              size: viewProjectionMatrix.byteLength
            }
          },
          {
            binding: 1,
            resource: {
              buffer: modelBuffer,
              offset: 0,
              size: modelMatrix.byteLength
            }
          }
        ]
      });
      /////////////////////////////////////////////////////////////////////////

      // Create render pass descriptor
      /////////////////////////////////////////////////////////////////////////
      const renderPassDescriptor = {
        colorAttachments: [
          {
            loadOp: "clear", // Clear image on each load
            clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, // Clear image with this color
            storeOp: "store", // Write result to the view
          },
        ],
      };
      // Get the latest swap chain image, set as our output color attachment image
      renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView();
      /////////////////////////////////////////////////////////////////////////

      // Begin to draw
      /////////////////////////////////////////////////////////////////////////
      // Create command encoder to record rendering commands
      const commandEncoder = device.createCommandEncoder();

      // Pass render pass descriptor to get back a GPURenderPassEncorder
      // So that we can record rendering commands
      const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
      /////////////////////////////////////////////////////////////////////////

      // Draw
      /////////////////////////////////////////////////////////////////////////
      // Configure the pass encoder
      passEncoder.setPipeline(pipeline);
      passEncoder.setBindGroup(0, bindGroup);

      // Draw the triangle
      passEncoder.setVertexBuffer(0, vertexBuffer);
      passEncoder.setIndexBuffer(indexBuffer, "uint16");
      passEncoder.drawIndexed(indices.length);
      /////////////////////////////////////////////////////////////////////////

      // End draw
      /////////////////////////////////////////////////////////////////////////
      // End the render pass
      passEncoder.end();

      // Get command buffer to submit to GPU, by calling commandEncoder.finish()
      device.queue.submit([commandEncoder.finish()]);
      /////////////////////////////////////////////////////////////////////////

      requestAnimationFrame(frame);
    }

    requestAnimationFrame(frame);
  };

  init();
  ///////////////////////////////////////////////////////////////////////////////