var render = {}

var io = use('io')
var controller = use('controller')
var tracy = use('tracy')
var graphics = use('graphics')
var imgui = use('imgui')
var transform = use('transform')
var color = use('color')

var base_pipeline = {
  vertex: "sprite.vert",
  fragment: "sprite.frag",
  primitive: "triangle", // point, line, linestrip, triangle, trianglestrip
  fill: true, // false for lines
  depth: {
    compare: "greater_equal",  // never/less/equal/less_equal/greater/not_equal/greater_equal/always
    test: false,
    write: false,
    bias: 0,
    bias_slope_scale: 0,
    bias_clamp: 0
  },
  stencil: {
    enabled: true,
    front: {
      compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
      fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
      depth_fail: "keep",
      pass: "keep"
    },
    back: {
      compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always
      fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap
      depth_fail: "keep",
      pass: "keep"
    },
    test: true,
    compare_mask: 0,
    write_mask: 0
  },
  blend: {
    enabled: false,
    src_rgb: "zero", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
    dst_rgb: "zero",
    op_rgb: "add", // add/sub/rev_sub/min/max
    src_alpha: "one",
    dst_alpha: "zero",
    op_alpha: "add"
  },
  cull: "none", // none/front/back
  face: "cw", // cw/ccw
  alpha_to_coverage: false,
  multisample: {
    count: 1, // number of multisamples
    mask: 0xFFFFFFFF,
    domask: false
  },
  label: "scripted pipeline",
  target: {}
}

var sprite_pipeline = Object.create(base_pipeline);
sprite_pipeline.blend = {
  enabled:true,
  src_rgb: "src_alpha", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate
  dst_rgb: "one_minus_src_alpha",
  op_rgb: "add", // add/sub/rev_sub/min/max
  src_alpha: "one",
  dst_alpha: "zero",
  op_alpha: "add"
};

var context;

sprite_pipeline.target = {
  color_targets: [{
    format:"rgba8",
    blend:sprite_pipeline.blend
  }],
  depth: "d32 float s8"
};

var driver = "vulkan"
switch(os.platform()) {
  case "Linux":
    driver = "vulkan"
    break
  case "Windows":
//    driver = "direct3d12"
    driver = "vulkan"
    break
  case "macOS":
    driver = "metal"
    break
}

var unit_transform = new transform;

var cur = {};
cur.images = [];
cur.samplers = [];

var tbuffer;
function full_upload(buffers) {
  var cmds = context.acquire_cmd_buffer();
  tbuffer = context.upload(cmds, buffers, tbuffer);
  cmds.submit();
}

full_upload[cell.DOC] = `Acquire a command buffer and upload the provided data buffers to the GPU, then submit.

:param buffers: An array of data buffers to be uploaded.
:return: None
`

function bind_pipeline(pass, pipeline) {
  make_pipeline(pipeline)
  pass.bind_pipeline(pipeline.gpu)
  pass.pipeline = pipeline;
}

bind_pipeline[cell.DOC] = `Ensure the specified pipeline is created on the GPU and bind it to the given render pass.

:param pass: The current render pass to bind the pipeline to.
:param pipeline: The pipeline object containing shader and state info.
:return: None
`

var main_pass;

var cornflower = [62/255,96/255,113/255,1];

function get_pipeline_ubo_slot(pipeline, name) {
  if (!pipeline.vertex.reflection.ubos) return;
  for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
    var ubo = pipeline.vertex.reflection.ubos[i];
    if (ubo.name.endsWith(name))
      return i;
  }
  return null;
}

get_pipeline_ubo_slot[cell.DOC] = `Return the index of a uniform buffer block within the pipeline's vertex reflection data by name suffix.

:param pipeline: The pipeline whose vertex reflection is inspected.
:param name: A string suffix to match against the uniform buffer block name.
:return: The integer index of the matching UBO, or null if not found.
`

function transpose4x4(val) {
  var out = [];
  out[0] = val[0]; out[1] = val[4]; out[2] = val[8];  out[3] = val[12];
  out[4] = val[1]; out[5] = val[5]; out[6] = val[9];  out[7] = val[13];
  out[8] = val[2]; out[9] = val[6]; out[10] = val[10];out[11] = val[14];
  out[12] = val[3];out[13] = val[7];out[14] = val[11];out[15] = val[15];
  return out;
}

transpose4x4[cell.DOC] = `Return a new 4x4 matrix array that is the transpose of the passed matrix.

:param val: An array of length 16 representing a 4x4 matrix in row-major format.
:return: A new array of length 16 representing the transposed matrix.
`

function ubo_obj_to_array(pipeline, name, obj) {
  var ubo;
  for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) {
    ubo = pipeline.vertex.reflection.ubos[i];
    if (ubo.name.endsWith(name)) break;
  }
  var type = pipeline.vertex.reflection.types[ubo.type];
  var len = 0;
  for (var mem of type.members)
    len += type_to_byte_count(mem.type);

  var buf = new ArrayBuffer(len);
  var view = new DataView(buf);

  for (var mem of type.members) {
    var val = obj[mem.name];
    if (!val) throw new Error (`Could not find ${mem.name} on supplied object`);

    if (mem.name == 'model')
      val = transpose4x4(val.array());
      
    for (var i = 0; i < val.length; i++)
      view.setFloat32(mem.offset + i*4, val[i],true);
  }
  return buf;
}

ubo_obj_to_array[cell.DOC] = `Construct an ArrayBuffer containing UBO data from the provided object, matching the pipeline's reflection info.

:param pipeline: The pipeline whose vertex reflection is read for UBO structure.
:param name: The name suffix that identifies the target UBO in the reflection data.
:param obj: An object whose properties match the UBO members.
:return: An ArrayBuffer containing packed UBO data.
`

function type_to_byte_count(type) {
  switch (type) {
    case 'float': return 4;
    case 'vec2': return 8;
    case 'vec3': return 12;
    case 'vec4': return 16;
    case 'mat4': return 64;
    default: throw new Error("Unknown or unsupported float-based type: " + type);
  }
}

type_to_byte_count[cell.DOC] = `Return the byte size for known float-based types.

:param type: A string type identifier (e.g., 'float', 'vec2', 'vec3', 'vec4', 'mat4').
:return: Integer number of bytes.
`

var sprite_model_ubo = {
  model: unit_transform,
  color: [1,1,1,1]
};

var shader_cache = {};
var shader_times = {};

function make_pipeline(pipeline) {
  if (pipeline.hasOwnProperty("gpu")) return; // this pipeline has already been made

  if (typeof pipeline.vertex == 'string')
    pipeline.vertex = make_shader(pipeline.vertex);
  if (typeof pipeline.fragment == 'string')
    pipeline.fragment = make_shader(pipeline.fragment)
    
  // 1) Reflection data for vertex shader
  var refl = pipeline.vertex.reflection
  if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) {
    pipeline.gpu = context.make_pipeline(pipeline);
    return;
  }

  var inputs = refl.inputs
  var buffer_descriptions = []
  var attributes = []

  // 2) Build buffer + attribute for each reflection input
  for (var i = 0; i < inputs.length; i++) {
    var inp = inputs[i]
    var typeStr = inp.type
    var nameStr = (inp.name || "").toUpperCase()
    var pitch = 4
    var fmt = "float1"

    if (typeStr == "vec2") {
      pitch = 8
      fmt = "float2"
    } else if (typeStr == "vec3") {
      pitch = 12
      fmt = "float3"
    } else if (typeStr == "vec4") {
      if (nameStr.indexOf("COLOR") >= 0) {
        pitch = 16
        fmt = "color"
      } else {
        pitch = 16
        fmt = "float4"
      }
    }

    buffer_descriptions.push({
      slot: i,
      pitch: pitch,
      input_rate: "vertex",
      instance_step_rate: 0,
      name:inp.name.split(".").pop()
    })

    attributes.push({
      location: inp.location,
      buffer_slot: i,
      format: fmt,
      offset: 0
    })
  }

  pipeline.vertex_buffer_descriptions = buffer_descriptions
  pipeline.vertex_attributes = attributes

  pipeline.gpu = context.make_pipeline(pipeline);
}

make_pipeline[cell.DOC] = `Create and store a GPU pipeline object if it has not already been created.

:param pipeline: An object describing the pipeline state, shaders, and reflection data.
:return: None
`

var shader_type;

function make_shader(sh_file) {
  var file = `shaders/${shader_type}/${sh_file}.${shader_type}`
  if (shader_cache[file]) return shader_cache[file]
  var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`))
  
  var shader = {
    code: io.slurpbytes(file),
    format: shader_type,
    stage: sh_file.endsWith("vert") ? "vertex" : "fragment",
    num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0,
    num_textures: 0,
    num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0,
    num_uniform_buffers: refl.ubos ? refl.ubos.length : 0,
    entrypoint: shader_type == "msl" ? "main0" : "main"
  }

  shader.gpu = context.make_shader(shader)
  shader.reflection = refl;
  shader_cache[file] = shader
  shader.file = sh_file
  return shader
}

make_shader[cell.DOC] = `Load and compile a shader from disk, caching the result. Reflective metadata is also loaded.

:param sh_file: The base filename (without extension) of the shader to compile.
:return: A shader object with GPU and reflection data attached.
`

var render_queue = [];
var hud_queue = [];

var current_queue = render_queue;

var std_sampler = {
  min_filter: "nearest",
  mag_filter: "nearest",
  mipmap: "linear",
  u: "repeat",
  v: "repeat",
  w: "repeat",
  mip_bias: 0,
  max_anisotropy: 0,
  compare_op: "none",
  min_lod: 0,
  max_lod: 0,
  anisotropy: false,
  compare: false
};

function upload_model(model) {
  var bufs = [];
  for (var i in model) {
    if (typeof model[i] != 'object') continue;
    bufs.push(model[i]);
  }
  context.upload(this, bufs);
}

upload_model[cell.DOC] = `Upload all buffer-like properties of the given model to the GPU.

:param model: An object whose buffer properties are to be uploaded.
:return: None
`

function bind_model(pass, pipeline, model) {
  var buffers = pipeline.vertex_buffer_descriptions;
  var bufs = [];
  if (buffers)
    for (var b of buffers) {
      if (b.name in model) bufs.push(model[b.name])
      else throw Error (`could not find buffer ${b.name} on model`);
    }
  pass.bind_buffers(0,bufs);
  pass.bind_index_buffer(model.indices);
}

bind_model[cell.DOC] = `Bind the model's vertex and index buffers for the given pipeline and render pass.

:param pass: The current render pass.
:param pipeline: The pipeline object with vertex buffer descriptions.
:param model: The model object containing matching buffers and an index buffer.
:return: None
`

function bind_mat(pass, pipeline, mat) {
  var imgs = [];
  var refl = pipeline.fragment.reflection;
  if (refl.separate_images) {
    for (var i of refl.separate_images) {
      if (i.name in mat) {
        var tex = mat[i.name];
        imgs.push({texture:tex.texture, sampler:tex.sampler});
      } else
        throw Error (`could not find all necessary images: ${i.name}`)
    }
    pass.bind_samplers(false, 0,imgs);
  }
}

bind_mat[cell.DOC] = `Bind the material images and samplers needed by the pipeline's fragment shader.

:param pass: The current render pass.
:param pipeline: The pipeline whose fragment shader reflection indicates required textures.
:param mat: An object mapping the required image names to {texture, sampler}.
:return: None
`

function group_sprites_by_texture(sprites, mesh) {
  if (sprites.length == 0) return;
  for (var i = 0; i < sprites.length; i++) {
    sprites[i].mesh = mesh;
    sprites[i].first_index = i*6;
    sprites[i].num_indices = 6;
  }
  return;
  // The code below is an alternate approach to grouping by image. Currently not in use.
  /*
  var groups = [];
  var group = {image:sprites[0].image, first_index:0};
  var count = 1;
  for (var i = 1; i < sprites.length; i++) {
    if (sprites[i].image == group.image) {
      count++;
      continue;
    }
    group.num_indices = count*6;
    var newgroup = {image:sprites[i].image, first_index:group.first_index+group.num_indices};
    group = newgroup;
    groups.push(group);
    count=1;
  }
  group.num_indices = count*6;
  return groups;
  */
}

group_sprites_by_texture[cell.DOC] = `Assign each sprite to the provided mesh, generating index data as needed.

:param sprites: An array of sprite objects.
:param mesh: A mesh object (pos, color, uv, indices, etc.) to link to each sprite.
:return: None
`

var main_color = {
  type:"2d",
  format: "rgba8",
  layers: 1,
  mip_levels: 1,
  samples: 0,
  sampler:true,
  color_target:true
};

var main_depth = {
  type: "2d",
  format: "d32 float s8",
  layers:1,
  mip_levels:1,
  samples:0,
  sampler:true,
  depth_target:true
};

function render_camera(cmds, camera) {
  var pass;
  delete camera.target // TODO: HORRIBLE
  if (!camera.target) {
    main_color.width = main_depth.width = camera.size.x;
    main_color.height = main_depth.height = camera.size.y;
    camera.target = {
      color_targets: [{
        texture: context.texture(main_color),
        mip_level:0,
        layer: 0,
        load:"clear",
        store:"store",
        clear: cornflower
      }],
      depth_stencil: {
        texture: context.texture(main_depth),
        clear:1,
        load:"dont_care",
        store:"dont_care",
        stencil_load:"dont_care",
        stencil_store:"dont_care",
        stencil_clear:0
      }
    };
  }

  var buffers = [];
  buffers = buffers.concat(graphics.queue_sprite_mesh(render_queue));
  var unique_meshes = [...new Set(render_queue.map(x => x.mesh))];
  for (var q of unique_meshes)
    buffers = buffers.concat([q.pos, q.color, q.uv, q.indices]);

  buffers = buffers.concat(graphics.queue_sprite_mesh(hud_queue));
  for (var q of hud_queue)
    if (q.type == 'geometry') buffers = buffers.concat([q.mesh.pos, q.mesh.color, q.mesh.uv, q.mesh.indices]);

  full_upload(buffers)

  var pass = cmds.render_pass(camera.target);
  
  var pipeline = sprite_pipeline;
  bind_pipeline(pass,pipeline);

  var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
  if (camslot != null)
    cmds.camera(camera, camslot);

  modelslot = get_pipeline_ubo_slot(pipeline, "model");
  if (modelslot != null) {
    var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo); 
    cmds.push_vertex_uniform_data(modelslot, ubo);
  }

  var mesh;
  var img;
  var modelslot;

  cmds.push_debug_group("draw")
  for (var group of render_queue) {
    if (mesh != group.mesh) {
      mesh = group.mesh;
      bind_model(pass,pipeline,mesh);
    }

    if (group.image && img != group.image) {
      img = group.image;
      img.sampler = std_sampler;
      bind_mat(pass,pipeline,{diffuse:img});
    }

    pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
  }
  cmds.pop_debug_group()

  cmds.push_debug_group("hud")
  var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer');
  if (camslot != null)
    cmds.hud(camera.size, camslot);
  
  for (var group of hud_queue) {
    if (mesh != group.mesh) {
      mesh = group.mesh;
      bind_model(pass,pipeline,mesh);
    }

    if (group.image && img != group.image) {
      img = group.image;
      img.sampler = std_sampler;
      bind_mat(pass,pipeline,{diffuse:img});
    }
    
    pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0);
  }
  cmds.pop_debug_group();

  pass?.end();

  render_queue = [];
  hud_queue = [];
}

render_camera[cell.DOC] = `Render a scene using the provided camera, drawing both render queue and HUD queue items.

:param cmds: A command buffer obtained from the GPU context.
:param camera: The camera object (with size, optional target, etc.).
:return: None
`

var swaps = [];
render.present = function() {
  os.clean_transforms();
  var cmds = context.acquire_cmd_buffer();
  render_camera(cmds, prosperon.camera);
  var swapchain_tex = cmds.acquire_swapchain();
  if (!swapchain_tex)
    cmds.cancel();
  else {
    var torect = prosperon.camera.draw_rect(prosperon.window.size);
    torect.texture = swapchain_tex;
    if (swapchain_tex) {
      cmds.blit({
        src: prosperon.camera.target.color_targets[0].texture,
        dst: torect,
        filter:"nearest",
        load: "clear"
      });

      if (imgui) { // draws any imgui commands present
        cmds.push_debug_group("imgui")
        imgui.prepend(cmds);
        var pass = cmds.render_pass({
          color_targets:[{texture:swapchain_tex}]});
        imgui.endframe(cmds,pass);
        pass.end();
        cmds.pop_debug_group()
      }
    }
    cmds.submit()
  }
}

render.present[cell.DOC] = `Perform the per-frame rendering and present the final swapchain image, including imgui pass if available.

:return: None
`

var stencil_write = {
  compare: "always",
  fail_op: "replace",
  depth_fail_op: "replace",
  pass_op: "replace"
};

function stencil_writer(ref) {
  var pipe = Object.create(base_pipeline);
  Object.assign(pipe, {
    stencil: {
      enabled: true,
      front: stencil_write,
      back: stencil_write,
      write:true,
      read:true,
      ref:ref
    },
    write_mask: colormask.none
  });
  return pipe;
}.hashify();

// objects by default draw where the stencil buffer is 0
function fillmask(ref) {
  var pipe = stencil_writer(ref);
  render.use_shader('screenfill.cg', pipe);
  render.draw(shape.quad);
}

render.fillmask[cell.DOC] = `Draw a fullscreen shape using a 'screenfill' shader to populate the stencil buffer with a given reference.

:param ref: The stencil reference value to write.
:return: None
`

var stencil_invert = {
  compare: "always",
  fail_op: "invert",
  depth_fail_op: "invert",
  pass_op: "invert"
};

function mask(image, pos, scale, rotation = 0, ref = 1) {
  if (typeof image == 'string')
    image = graphics.texture(image);

  var tex = image.texture;
  if (scale) scale = scale.div([tex.width,tex.height]);
  else scale = [1,1,1]
    
  var pipe = stencil_writer(ref);
  render.use_shader('sprite.cg', pipe);
  var t = new transform;
  t.trs(pos, null, scale);
  set_model(t);
  render.use_mat({
    diffuse:image.texture,
    rect: image.rect,
    shade: color.white
  });
  render.draw(shape.quad);
}

render.mask[cell.DOC] = `Draw an image to the stencil buffer, marking its area with a specified reference value.

:param image: A texture or string path (which is converted to a texture).
:param pos: The translation (x, y) for the image placement.
:param scale: Optional scaling applied to the texture.
:param rotation: Optional rotation in radians (unused by default).
:param ref: The stencil reference value to write.
:return: None
`

render.viewport = function(rect) {
  context.viewport(rect);
}

render.viewport[cell.DOC] = `Set the GPU viewport to the specified rectangle.

:param rect: A rectangle [x, y, width, height].
:return: None
`

render.scissor = function(rect) {
  render.viewport(rect)
}

render.scissor[cell.DOC] = `Set the GPU scissor region to the specified rectangle (alias of render.viewport).

:param rect: A rectangle [x, y, width, height].
:return: None
`

var std_sampler

if (tracy) tracy.gpu_init()

render.queue = function(cmd) {
  if (Array.isArray(cmd))
    for (var i of cmd) current_queue.push(i)
  else
    current_queue.push(cmd)
}

render.queue[cell.DOC] = `Enqueue one or more draw commands. These commands are batched until render_camera is called.

:param cmd: Either a single command object or an array of command objects.
:return: None
`

render.setup_draw = function() {
  current_queue = render_queue;
  prosperon.draw();
}

render.setup_draw[cell.DOC] = `Switch the current queue to the primary scene render queue, then invoke 'prosperon.draw' if defined.

:return: None
`

render.setup_hud = function() {
  current_queue = hud_queue;
  prosperon.hud();
}

render.setup_hud[cell.DOC] = `Switch the current queue to the HUD render queue, then invoke 'prosperon.hud' if defined.

:return: None
`

render.initialize = function(config)
{
  var default_conf = {
    title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
    width: 1280,
    height: 720,
    icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
    high_dpi:0,
    alpha:1,
    fullscreen:0,
    sample_count:1,
    enable_clipboard:true,
    enable_dragndrop: true,
    max_dropped_files: 1,
    swap_interval: 1,
    name: "Prosperon",
    version:prosperon.version + "-" + prosperon.revision,
    identifier: "world.pockle.prosperon",
    creator: "Pockle World LLC",
    copyright: "Copyright Pockle World 2025",
    type: "game",
    url: "https://prosperon.dev"
  }

  config.__proto__ = default_conf

  prosperon.camera = use('ext/camera').make()
  prosperon.camera.size = [config.width,config.height]

  prosperon.window = prosperon.engine_start(config)

  context = prosperon.window.make_gpu(false,driver)
  context.window = prosperon.window
  context.claim_window(prosperon.window)
  context.set_swapchain('sdr', 'vsync')

  if (imgui) imgui.init(context, prosperon.window)

  shader_type = context.shader_format()[0];

  std_sampler = context.make_sampler({
    min_filter: "nearest",
    mag_filter: "nearest",
    mipmap_mode: "nearest",
    address_mode_u: "repeat",
    address_mode_v: "repeat",
    address_mode_w: "repeat"
  });
}

return render

