import {
  WebGLState,
  WebGLBufferState,
  WebGLTextureState,
  WebGLTextureUnitState,
  WebGLProgramState,
  WebGLFramebufferState,
  WebGLVertexArrayState,
  WebGLVertexAttribState,
  WebGLCapabilityState,
  WebGLViewportState,
  WebGLClearState,
  WebGLBlendState,
  WebGLDepthState,
  WebGLStencilState,
  WebGLColorState,
  WebGLCullingState,
  WebGLLineState,
  WebGLPolygonOffsetState,
  WebGLSampleState,
  WebGLPixelStoreState,
  WebGLTransformFeedbackState,
  WebGLRenderbufferState,
  WebGLSamplerState,
  WebGLQueryState,
  WebGLIndexedBufferBinding,
  GL_MAX_VERTEX_ATTRIBS,
  GL_MAX_UNIFORM_BUFFER_BINDINGS,
  GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS,
  GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,
  GL_MAX_COLOR_ATTACHMENTS,
  GLUndefined,
  isDefined,
} from './WebGLState';

/**
 * Global flag to enable WebGL error checking after each state operation.
 * Set to true for debugging to catch GL errors immediately after they occur.
 * Default: false
 */
export let CHECK_ERRORS = false;

/**
 * Helper function to check if a property is defined on a state object,
 * handling the case where the state object itself might be undefined
 */
function isPropertyDefined<T>(state: T | undefined, prop: keyof T): boolean {
  return state !== undefined && isDefined((state as any)[prop]);
}

/**
 * Helper function to determine if state should be applied based on saved vs current state.
 * Returns true if the states differ or if one is undefined and the other is not.
 *
 * @param saved - The saved/desired state (may be undefined to reset to defaults)
 * @param current - The current state (may be undefined if not tracked)
 * @returns true if the state should be applied, false if no change needed
 */
function shouldApplyState<T extends { equals(other: T): boolean }>(
  saved: T | undefined,
  current: T | undefined
): boolean {
  // Both undefined - no change needed
  if (saved === undefined && current === undefined) {
    return false;
  }

  // One is undefined and the other isn't - need to apply/reset
  if (saved === undefined || current === undefined) {
    return true;
  }

  // Both defined - check if they're different using equals()
  return !saved.equals(current);
}

/**
 * Helper function to check and log WebGL errors after state application
 * @param gl - The WebGL2RenderingContext to check
 * @param stepName - Name of the state application step for logging
 */
function checkGLError(gl: WebGL2RenderingContext, stepName: string): void {
  if (!CHECK_ERRORS) return;

  const error = gl.getError();
  if (error !== gl.NO_ERROR) {
    const errorName = getGLErrorName(gl, error);
    const message = `[WebGLStateApply] GL error after ${stepName}: ${errorName} (0x${error.toString(16)})`;

    throw new Error(message);
  }
}

/**
 * Get human-readable name for a WebGL error code
 */
function getGLErrorName(gl: WebGL2RenderingContext, error: number): string {
  switch (error) {
    case gl.INVALID_ENUM:
      return 'INVALID_ENUM';
    case gl.INVALID_VALUE:
      return 'INVALID_VALUE';
    case gl.INVALID_OPERATION:
      return 'INVALID_OPERATION';
    case gl.INVALID_FRAMEBUFFER_OPERATION:
      return 'INVALID_FRAMEBUFFER_OPERATION';
    case gl.OUT_OF_MEMORY:
      return 'OUT_OF_MEMORY';
    case gl.CONTEXT_LOST_WEBGL:
      return 'CONTEXT_LOST_WEBGL';
    default:
      return 'UNKNOWN_ERROR';
  }
}

/**
 * Apply all defined state from a WebGLState object to a WebGL context
 * @param gl - The WebGL2RenderingContext to apply state to
 * @param state - The WebGLState object containing tracked state
 * @param current - The current WebGLState for comparison
 */
export function apply(gl: WebGL2RenderingContext, state: WebGLState, current: WebGLState): void {
  // Apply state in a specific order to handle dependencies

  // 2. Vertex Array Object binding (this may temporarily change ARRAY_BUFFER)
  if (shouldApplyState(state.vertexArrays, current.vertexArrays)) {
    applyVertexArrayState(gl, state.vertexArrays, current.vertexArrays);
    checkGLError(gl, 'vertex array state');
  }

  // 1. Buffer bindings (needed before VAO state)
  if (shouldApplyState(state.buffers, current.buffers)) {
    applyBufferState(gl, state.buffers, current.buffers, state.validBuffers);
    checkGLError(gl, 'buffer state');
  }

  // 4. Texture bindings
  if (shouldApplyState(state.textures, current.textures)) {
    applyTextureState(gl, state.textures, current.textures);
    checkGLError(gl, 'texture state');
  }

  // 5. Sampler bindings
  if (shouldApplyState(state.samplers, current.samplers)) {
    applySamplerState(gl, state.samplers, current.samplers);
    checkGLError(gl, 'sampler state');
  }

  // 6. Program binding
  if (shouldApplyState(state.programs, current.programs)) {
    applyProgramState(gl, state.programs, current.programs);
    checkGLError(gl, 'program state');
  }

  // 7. Framebuffer bindings and attachments
  if (shouldApplyState(state.framebuffers, current.framebuffers)) {
    applyFramebufferState(gl, state.framebuffers, current.framebuffers);
    checkGLError(gl, 'framebuffer state');
  }

  // 8. Renderbuffer binding
  if (shouldApplyState(state.renderbuffer, current.renderbuffer)) {
    applyRenderbufferState(gl, state.renderbuffer, current.renderbuffer);
    checkGLError(gl, 'renderbuffer state');
  }

  // 9. Transform feedback
  if (shouldApplyState(state.transformFeedback, current.transformFeedback)) {
    applyTransformFeedbackState(gl, state.transformFeedback, current.transformFeedback);
    checkGLError(gl, 'transform feedback state');
  }

  // 10. Viewport and scissor
  if (shouldApplyState(state.viewport, current.viewport)) {
    applyViewportState(gl, state.viewport, current.viewport);
    checkGLError(gl, 'viewport state');
  }

  // 11. Capabilities (enable/disable)
  if (shouldApplyState(state.capabilities, current.capabilities)) {
    applyCapabilityState(gl, state.capabilities, current.capabilities);
    checkGLError(gl, 'capability state');
  }

  // 12. Clear values
  if (shouldApplyState(state.clear, current.clear)) {
    applyClearState(gl, state.clear, current.clear);
    checkGLError(gl, 'clear state');
  }

  // 13. Blend state
  if (shouldApplyState(state.blend, current.blend)) {
    applyBlendState(gl, state.blend, current.blend);
    checkGLError(gl, 'blend state');
  }

  // 14. Depth state
  if (shouldApplyState(state.depth, current.depth)) {
    applyDepthState(gl, state.depth, current.depth);
    checkGLError(gl, 'depth state');
  }

  // 15. Stencil state
  if (shouldApplyState(state.stencil, current.stencil)) {
    applyStencilState(gl, state.stencil, current.stencil);
    checkGLError(gl, 'stencil state');
  }

  // 16. Color write mask
  if (shouldApplyState(state.color, current.color)) {
    applyColorState(gl, state.color, current.color);
    checkGLError(gl, 'color state');
  }

  // 17. Culling state
  if (shouldApplyState(state.culling, current.culling)) {
    applyCullingState(gl, state.culling, current.culling);
    checkGLError(gl, 'culling state');
  }

  // 18. Line width
  if (shouldApplyState(state.line, current.line)) {
    applyLineState(gl, state.line, current.line);
    checkGLError(gl, 'line state');
  }

  // 19. Polygon offset
  if (shouldApplyState(state.polygonOffset, current.polygonOffset)) {
    applyPolygonOffsetState(gl, state.polygonOffset, current.polygonOffset);
    checkGLError(gl, 'polygon offset state');
  }

  // 20. Sample coverage
  if (shouldApplyState(state.sample, current.sample)) {
    applySampleState(gl, state.sample, current.sample);
    checkGLError(gl, 'sample state');
  }

  // 21. Pixel store parameters
  if (shouldApplyState(state.pixelStore, current.pixelStore)) {
    applyPixelStoreState(gl, state.pixelStore, current.pixelStore);
    checkGLError(gl, 'pixel store state');
  }

  // 22. Query objects
  if (state.queries && (!current.queries || !state.queries.equals(current.queries))) {
    applyQueryState(gl, state.queries);
    checkGLError(gl, 'query state');
  }
}

/**
 * Apply buffer binding state
 */
function applyBufferState(
  gl: WebGL2RenderingContext,
  buffers: WebGLBufferState | undefined,
  current: WebGLBufferState | undefined,
  validBuffers: Set<WebGLBuffer>
): void {
  // Helper to validate a buffer
  const validateBuffer = (buffer: WebGLBuffer | null, bufferName: string): boolean => {
    if (!buffer) return true;

    if (!validBuffers.has(buffer)) {
      console.warn(
        `[WebGLStateApply] Cannot bind ${bufferName}: buffer has been deleted. Skipping.`
      );
      return false;
    }
    return true;
  };

  if (isPropertyDefined(buffers, 'arrayBuffer')) {
    if (validateBuffer(buffers!.arrayBuffer, 'ARRAY_BUFFER')) {
      gl.bindBuffer(gl.ARRAY_BUFFER, buffers!.arrayBuffer);
    }
  } else if (isDefined(current?.arrayBuffer)) {
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }

  // NOTE: ELEMENT_ARRAY_BUFFER is NOT restored here!
  // Per OpenGL ES 3.0 spec section 2.10, ELEMENT_ARRAY_BUFFER binding is per-VAO state.
  // It is automatically restored when binding the VAO in applyVertexArrayState() above.

  if (isPropertyDefined(buffers, 'uniformBuffer')) {
    if (validateBuffer(buffers!.uniformBuffer, 'UNIFORM_BUFFER')) {
      gl.bindBuffer(gl.UNIFORM_BUFFER, buffers!.uniformBuffer);
    }
  } else if (isDefined(current?.uniformBuffer)) {
    gl.bindBuffer(gl.UNIFORM_BUFFER, null);
  }

  if (isPropertyDefined(buffers, 'transformFeedbackBuffer')) {
    if (validateBuffer(buffers!.transformFeedbackBuffer, 'TRANSFORM_FEEDBACK_BUFFER')) {
      gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buffers!.transformFeedbackBuffer);
    }
  } else if (isDefined(current?.transformFeedbackBuffer)) {
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);
  }

  if (isPropertyDefined(buffers, 'pixelPackBuffer')) {
    if (validateBuffer(buffers!.pixelPackBuffer, 'PIXEL_PACK_BUFFER')) {
      gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buffers!.pixelPackBuffer);
    }
  } else if (isDefined(current?.pixelPackBuffer)) {
    gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
  }

  if (isPropertyDefined(buffers, 'pixelUnpackBuffer')) {
    if (validateBuffer(buffers!.pixelUnpackBuffer, 'PIXEL_UNPACK_BUFFER')) {
      gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, buffers!.pixelUnpackBuffer);
    }
  } else if (isDefined(current?.pixelUnpackBuffer)) {
    gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
  }

  if (isPropertyDefined(buffers, 'copyReadBuffer')) {
    if (validateBuffer(buffers!.copyReadBuffer, 'COPY_READ_BUFFER')) {
      gl.bindBuffer(gl.COPY_READ_BUFFER, buffers!.copyReadBuffer);
    }
  } else if (isDefined(current?.copyReadBuffer)) {
    gl.bindBuffer(gl.COPY_READ_BUFFER, null);
  }

  if (isPropertyDefined(buffers, 'copyWriteBuffer')) {
    if (validateBuffer(buffers!.copyWriteBuffer, 'COPY_WRITE_BUFFER')) {
      gl.bindBuffer(gl.COPY_WRITE_BUFFER, buffers!.copyWriteBuffer);
    }
  } else if (isDefined(current?.copyWriteBuffer)) {
    gl.bindBuffer(gl.COPY_WRITE_BUFFER, null);
  }

  // Apply indexed buffer bindings
  if (buffers?.uniformBufferBindings) {
    for (let index = 0; index < GL_MAX_UNIFORM_BUFFER_BINDINGS; index++) {
      const bindingOrUndefined = buffers.uniformBufferBindings.get(index);
      if (!isDefined(bindingOrUndefined)) {
        continue;
      }

      const binding = bindingOrUndefined as WebGLIndexedBufferBinding;
      if (isDefined(binding.buffer)) {
        // Validate buffer exists
        if (!validateBuffer(binding.buffer, `UNIFORM_BUFFER at index ${index}`)) {
          continue;
        }

        const size = binding.size as number;
        const offset = binding.offset as number;

        // Per WebGLState.ts bindBufferBase logic: size=0 means entire buffer (bindBufferBase)
        // size>0 means specific range (bindBufferRange)
        if (size === 0) {
          // bindBufferBase was used originally
          gl.bindBufferBase(gl.UNIFORM_BUFFER, index, binding.buffer);
        } else {
          // bindBufferRange was used originally
          gl.bindBufferRange(gl.UNIFORM_BUFFER, index, binding.buffer, offset, size);
        }
      }
    }
  }

  if (buffers?.transformFeedbackBufferBindings) {
    for (let index = 0; index < GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS; index++) {
      const bindingOrUndefined = buffers.transformFeedbackBufferBindings.get(index);
      if (!isDefined(bindingOrUndefined)) {
        continue;
      }

      const binding = bindingOrUndefined as WebGLIndexedBufferBinding;
      if (isDefined(binding.buffer)) {
        // Validate buffer exists
        if (!validateBuffer(binding.buffer, `TRANSFORM_FEEDBACK_BUFFER at index ${index}`)) {
          continue;
        }

        const size = binding.size as number;
        const offset = binding.offset as number;

        // Per WebGLState.ts bindBufferBase logic: size=0 means entire buffer (bindBufferBase)
        // size>0 means specific range (bindBufferRange)
        if (size === 0) {
          // bindBufferBase was used originally
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, index, binding.buffer);
        } else {
          // bindBufferRange was used originally
          gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, index, binding.buffer, offset, size);
        }
      }
    }
  }
}

/**
 * Apply vertex array object state
 */
function applyVertexArrayState(
  gl: WebGL2RenderingContext,
  vertexArrays: WebGLVertexArrayState | undefined,
  current?: WebGLVertexArrayState
): void {
  // TODO: Currently only restores GL state for the currently bound VAO.
  // This means if you have multiple VAOs configured, save state, switch VAOs,
  // and restore, the non-current VAO states will not be restored in the tracker.
  // To fix this, we would need to:
  // 1. Copy all VAO states from saved state to current tracker's vaoStates Map
  // 2. Ensure this doesn't break existing behavior or cause performance issues
  // 3. Handle edge cases where VAOs are deleted between save and restore

  // Bind the VAO
  if (isPropertyDefined(vertexArrays, 'vertexArrayObject')) {
    const vaoToBind = vertexArrays!.vertexArrayObject;

    // Check if the VAO was deleted between save and restore
    // Since bindVertexArray now ensures all bound VAOs have vaoStates entries,
    // if the VAO is missing from current.vaoStates, it means it was deleted
    if (vaoToBind !== null && current && !current.vaoStates.has(vaoToBind)) {
      console.warn(
        '[WebGLStateApply] Cannot restore VAO state: the saved VAO has been deleted. ' +
          'Binding will revert to default VAO (null).'
      );
      gl.bindVertexArray(null);
      return;
    }

    gl.bindVertexArray(vaoToBind);
  } else if (isDefined(current?.vertexArrayObject)) {
    gl.bindVertexArray(null);
  }

  // Restore per-VAO state for the currently bound VAO
  // Per OpenGL ES 3.0 spec section 2.10: ELEMENT_ARRAY_BUFFER and vertex attributes are per-VAO state
  const vao =
    vertexArrays?.vertexArrayObject === GLUndefined ? null : vertexArrays?.vertexArrayObject;
  const vaoState = vertexArrays?.vaoStates.get(vao!);

  if (vaoState) {
    // Restore ELEMENT_ARRAY_BUFFER binding for this VAO
    if (isPropertyDefined(vaoState, 'elementArrayBuffer')) {
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vaoState.elementArrayBuffer);
    }

    // Restore vertex attribute state
    if (vaoState.attributes) {
      // Iterate through all possible vertex attribute indices
      for (let idx = 0; idx < GL_MAX_VERTEX_ATTRIBS; idx++) {
        const attribOrUndefined = vaoState.attributes.get(idx);

        // Skip undefined attributes (never been set)
        if (!isDefined(attribOrUndefined)) {
          continue;
        }

        const attrib = attribOrUndefined as WebGLVertexAttribState;

        // Check if we have a complete vertex attribute configuration
        const hasCompleteConfig =
          isDefined(attrib.size) &&
          isDefined(attrib.type) &&
          isDefined(attrib.stride) &&
          isDefined(attrib.offset);

        // Only restore this attribute if we have complete configuration OR if we're explicitly disabling it
        const shouldRestore = hasCompleteConfig || (isDefined(attrib.enabled) && !attrib.enabled);

        if (!shouldRestore) {
          // Skip this attribute - incomplete configuration and not explicitly disabled
          continue;
        }

        // Bind the buffer that was associated with this attribute if defined
        if (isDefined(attrib.buffer)) {
          gl.bindBuffer(gl.ARRAY_BUFFER, attrib.buffer);
        }

        // Restore vertexAttribPointer configuration if all required parameters are defined
        if (hasCompleteConfig) {
          // Restore the vertex attribute pointer
          if (isDefined(attrib.normalized)) {
            gl.vertexAttribPointer(
              idx,
              attrib.size as number,
              attrib.type as number,
              attrib.normalized as boolean,
              attrib.stride as number,
              attrib.offset as number
            );
          } else {
            // For vertexAttribIPointer (integer attributes)
            gl.vertexAttribIPointer(
              idx,
              attrib.size as number,
              attrib.type as number,
              attrib.stride as number,
              attrib.offset as number
            );
          }
        }

        // Handle enable/disable state
        if (isDefined(attrib.enabled)) {
          if (attrib.enabled) {
            // Only enable if we have complete configuration
            if (hasCompleteConfig) {
              gl.enableVertexAttribArray(idx);
            }
          } else {
            // Always allow disabling, even without complete configuration
            gl.disableVertexAttribArray(idx);
          }
        }

        if (isDefined(attrib.divisor)) {
          gl.vertexAttribDivisor(idx, attrib.divisor as number);
        }
      }
    } // end if (vaoState.attributes)
  } // end if (vaoState)
}

/**
 * Apply texture binding state
 */
function applyTextureState(
  gl: WebGL2RenderingContext,
  textures: WebGLTextureState | undefined,
  current?: WebGLTextureState
): void {
  // Set active texture unit first
  if (isPropertyDefined(textures, 'activeTexture')) {
    gl.activeTexture(textures!.activeTexture as number);
  } else if (isDefined(current?.activeTexture)) {
    gl.activeTexture(gl.TEXTURE0);
  }

  // Bind textures to their respective units
  if (textures?.textureUnits) {
    for (let unitIndex = 0; unitIndex < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS; unitIndex++) {
      const unitKey = `TEXTURE${unitIndex}`;
      const unitStateOrUndefined = textures.textureUnits.get(unitKey);

      if (!isDefined(unitStateOrUndefined)) {
        continue;
      }

      const unitState = unitStateOrUndefined as WebGLTextureUnitState;

      gl.activeTexture(gl.TEXTURE0 + unitIndex);

      if (isDefined(unitState.texture2D)) {
        gl.bindTexture(gl.TEXTURE_2D, unitState.texture2D);
      }
      if (isDefined(unitState.textureCubeMap)) {
        gl.bindTexture(gl.TEXTURE_CUBE_MAP, unitState.textureCubeMap);
      }
      if (isDefined(unitState.texture3D)) {
        gl.bindTexture(gl.TEXTURE_3D, unitState.texture3D);
      }
      if (isDefined(unitState.texture2DArray)) {
        gl.bindTexture(gl.TEXTURE_2D_ARRAY, unitState.texture2DArray);
      }
    }

    // Restore the active texture unit
    if (isPropertyDefined(textures, 'activeTexture')) {
      gl.activeTexture(textures!.activeTexture as number);
    }
  }
}

/**
 * Apply sampler binding state
 */
function applySamplerState(
  gl: WebGL2RenderingContext,
  samplers: WebGLSamplerState | undefined,
  current?: WebGLSamplerState
): void {
  if (samplers?.samplerBindings) {
    for (let unit = 0; unit < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS; unit++) {
      const sampler = samplers.samplerBindings[unit];
      if (isDefined(sampler)) {
        gl.bindSampler(unit, sampler);
      }
    }
  } else if (current?.samplerBindings) {
    // Reset all sampler bindings to null
    for (let unit = 0; unit < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS; unit++) {
      if (isDefined(current.samplerBindings[unit])) {
        gl.bindSampler(unit, null);
      }
    }
  }
}

/**
 * Apply program binding state
 */
function applyProgramState(
  gl: WebGL2RenderingContext,
  programs: WebGLProgramState | undefined,
  current?: WebGLProgramState
): void {
  if (isPropertyDefined(programs, 'currentProgram')) {
    gl.useProgram(programs!.currentProgram);
  } else if (isDefined(current?.currentProgram)) {
    gl.useProgram(null);
  }
}

/**
 * Apply framebuffer binding and attachment state
 */
function applyFramebufferState(
  gl: WebGL2RenderingContext,
  framebuffers: WebGLFramebufferState | undefined,
  current?: WebGLFramebufferState
): void {
  // Bind framebuffers first
  if (isPropertyDefined(framebuffers, 'framebuffer')) {
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers!.framebuffer);
  } else if (isDefined(current?.framebuffer)) {
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  }

  if (isPropertyDefined(framebuffers, 'drawFramebuffer')) {
    gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, framebuffers!.drawFramebuffer);
  } else if (isDefined(current?.drawFramebuffer)) {
    gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
  }

  if (isPropertyDefined(framebuffers, 'readFramebuffer')) {
    gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffers!.readFramebuffer);
  } else if (isDefined(current?.readFramebuffer)) {
    gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
  }

  // Apply default framebuffer state (only drawBuffers and readBuffer)
  // NOTE: We do NOT apply attachments to the default framebuffer (null) because:
  // 1. The default framebuffer is provided by the canvas/context
  // 2. You cannot call framebufferTexture2D/framebufferRenderbuffer on the default framebuffer
  // 3. Attempting to do so results in INVALID_OPERATION errors
  const isDefaultDrawFB = framebuffers?.drawFramebuffer === null;
  const hasDefaultFBState = framebuffers?.defaultFramebufferState;

  // Only apply drawBuffers and readBuffer for default framebuffer
  if (isDefaultDrawFB && hasDefaultFBState) {
    const fbState = framebuffers!.defaultFramebufferState;

    // Apply draw buffers (this IS valid for default framebuffer)
    if (isDefined(fbState.drawBuffers)) {
      gl.drawBuffers(fbState.drawBuffers as number[]);
    }

    // Apply read buffer (this IS valid for default framebuffer)
    if (isDefined(fbState.readBuffer)) {
      gl.readBuffer(fbState.readBuffer as number);
    }
  }
}

/**
 * Apply renderbuffer binding state
 */
function applyRenderbufferState(
  gl: WebGL2RenderingContext,
  renderbuffer: WebGLRenderbufferState | undefined,
  current?: WebGLRenderbufferState
): void {
  if (isPropertyDefined(renderbuffer, 'renderbuffer')) {
    gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer!.renderbuffer);
  } else if (isDefined(current?.renderbuffer)) {
    gl.bindRenderbuffer(gl.RENDERBUFFER, null);
  }
}

/**
 * Apply transform feedback state
 */
function applyTransformFeedbackState(
  gl: WebGL2RenderingContext,
  transformFeedback: WebGLTransformFeedbackState | undefined,
  current?: WebGLTransformFeedbackState
): void {
  if (isPropertyDefined(transformFeedback, 'transformFeedback')) {
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback!.transformFeedback);
  } else if (isDefined(current?.transformFeedback)) {
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
  }

  if (
    isPropertyDefined(transformFeedback, 'transformFeedbackActive') &&
    transformFeedback!.transformFeedbackActive
  ) {
    // Note: beginTransformFeedback requires a primitive mode parameter
    // which isn't tracked, so this is a simplified version
    // gl.beginTransformFeedback(primitiveMode);
  }

  if (
    isPropertyDefined(transformFeedback, 'transformFeedbackPaused') &&
    transformFeedback!.transformFeedbackPaused
  ) {
    gl.pauseTransformFeedback();
  }
}

/**
 * Apply viewport and scissor state
 */
function applyViewportState(
  gl: WebGL2RenderingContext,
  viewport: WebGLViewportState | undefined,
  current?: WebGLViewportState
): void {
  // Viewport - default: typically [0, 0, canvas.width, canvas.height]
  if (isPropertyDefined(viewport, 'viewport')) {
    const vp = viewport!.viewport as Int32Array;
    gl.viewport(vp[0], vp[1], vp[2], vp[3]);
  } else if (isDefined(current?.viewport)) {
    // Reset to canvas dimensions
    const canvas = gl.canvas;
    gl.viewport(0, 0, canvas.width, canvas.height);
  }

  // Scissor box - default: typically [0, 0, canvas.width, canvas.height]
  if (isPropertyDefined(viewport, 'scissorBox')) {
    const scissor = viewport!.scissorBox as Int32Array;
    gl.scissor(scissor[0], scissor[1], scissor[2], scissor[3]);
  } else if (isDefined(current?.scissorBox)) {
    // Reset to canvas dimensions
    const canvas = gl.canvas;
    gl.scissor(0, 0, canvas.width, canvas.height);
  }
}

/**
 * Apply capability enable/disable state
 * @param gl - The WebGL2RenderingContext
 * @param capabilities - The desired capability state (undefined resets all to defaults)
 * @param currentCapabilities - Optional current capability state. If provided and a capability
 *                              is currently enabled (true) but not defined in capabilities,
 *                              it will be explicitly disabled.
 */
function applyCapabilityState(
  gl: WebGL2RenderingContext,
  capabilities: WebGLCapabilityState | undefined,
  currentCapabilities?: WebGLCapabilityState
): void {
  // Helper to apply a capability state
  const applyCapability = (cap: keyof WebGLCapabilityState, glEnum: number) => {
    const desired = capabilities?.[cap];
    const current = currentCapabilities?.[cap];

    if (isDefined(desired)) {
      // If desired state is defined, apply it
      desired ? gl.enable(glEnum) : gl.disable(glEnum);
    } else if (isDefined(current) && current === true) {
      // If desired state is undefined but current is enabled, disable it
      gl.disable(glEnum);
    }
  };

  applyCapability('blend', gl.BLEND);
  applyCapability('cullFace', gl.CULL_FACE);
  applyCapability('depthTest', gl.DEPTH_TEST);
  applyCapability('dither', gl.DITHER);
  applyCapability('polygonOffsetFill', gl.POLYGON_OFFSET_FILL);
  applyCapability('sampleAlphaToCoverage', gl.SAMPLE_ALPHA_TO_COVERAGE);
  applyCapability('sampleCoverage', gl.SAMPLE_COVERAGE);
  applyCapability('scissorTest', gl.SCISSOR_TEST);
  applyCapability('stencilTest', gl.STENCIL_TEST);
  applyCapability('rasterDiscard', gl.RASTERIZER_DISCARD);
}

/**
 * Apply clear value state
 */
function applyClearState(
  gl: WebGL2RenderingContext,
  clear: WebGLClearState | undefined,
  current?: WebGLClearState
): void {
  // ColorClearValue - default: [0, 0, 0, 0]
  if (isPropertyDefined(clear, 'colorClearValue')) {
    const color = clear!.colorClearValue as Float32Array;
    gl.clearColor(color[0], color[1], color[2], color[3]);
  } else if (isDefined(current?.colorClearValue)) {
    gl.clearColor(0, 0, 0, 0);
  }

  // DepthClearValue - default: 1
  if (isPropertyDefined(clear, 'depthClearValue')) {
    gl.clearDepth(clear!.depthClearValue as number);
  } else if (isDefined(current?.depthClearValue)) {
    gl.clearDepth(1);
  }

  // StencilClearValue - default: 0
  if (isPropertyDefined(clear, 'stencilClearValue')) {
    gl.clearStencil(clear!.stencilClearValue as number);
  } else if (isDefined(current?.stencilClearValue)) {
    gl.clearStencil(0);
  }
}

/**
 * Apply blend state
 */
function applyBlendState(
  gl: WebGL2RenderingContext,
  blend: WebGLBlendState | undefined,
  current?: WebGLBlendState
): void {
  // BlendColor - default: [0, 0, 0, 0]
  if (isPropertyDefined(blend, 'blendColor')) {
    const color = blend!.blendColor as Float32Array;
    gl.blendColor(color[0], color[1], color[2], color[3]);
  } else if (isDefined(current?.blendColor)) {
    gl.blendColor(0, 0, 0, 0);
  }

  // BlendEquation - default: FUNC_ADD for both RGB and Alpha
  if (
    isPropertyDefined(blend, 'blendEquationRgb') &&
    isPropertyDefined(blend, 'blendEquationAlpha')
  ) {
    gl.blendEquationSeparate(
      blend!.blendEquationRgb as number,
      blend!.blendEquationAlpha as number
    );
  } else if (isDefined(current?.blendEquationRgb) && isDefined(current?.blendEquationAlpha)) {
    gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
  }

  // BlendFunc - default: src=ONE, dst=ZERO for both RGB and Alpha
  if (
    isPropertyDefined(blend, 'blendSrcRgb') &&
    isPropertyDefined(blend, 'blendDstRgb') &&
    isPropertyDefined(blend, 'blendSrcAlpha') &&
    isPropertyDefined(blend, 'blendDstAlpha')
  ) {
    gl.blendFuncSeparate(
      blend!.blendSrcRgb as number,
      blend!.blendDstRgb as number,
      blend!.blendSrcAlpha as number,
      blend!.blendDstAlpha as number
    );
  } else if (
    isDefined(current?.blendSrcRgb) &&
    isDefined(current?.blendDstRgb) &&
    isDefined(current?.blendSrcAlpha) &&
    isDefined(current?.blendDstAlpha)
  ) {
    gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO);
  }
}

/**
 * Apply depth state
 */
function applyDepthState(
  gl: WebGL2RenderingContext,
  depth: WebGLDepthState | undefined,
  current?: WebGLDepthState
): void {
  // DepthFunc - default: LESS
  if (isPropertyDefined(depth, 'depthFunc')) {
    gl.depthFunc(depth!.depthFunc as number);
  } else if (isDefined(current?.depthFunc)) {
    gl.depthFunc(gl.LESS);
  }

  // DepthWritemask - default: true
  if (isPropertyDefined(depth, 'depthWritemask')) {
    gl.depthMask(depth!.depthWritemask as boolean);
  } else if (isDefined(current?.depthWritemask)) {
    gl.depthMask(true);
  }

  // DepthRange - default: [0, 1]
  if (isPropertyDefined(depth, 'depthRange')) {
    const range = depth!.depthRange as Float32Array;
    gl.depthRange(range[0], range[1]);
  } else if (isDefined(current?.depthRange)) {
    gl.depthRange(0, 1);
  }
}

/**
 * Apply stencil state
 */
function applyStencilState(
  gl: WebGL2RenderingContext,
  stencil: WebGLStencilState | undefined,
  current?: WebGLStencilState
): void {
  // Front face stencil func - default: ALWAYS, ref=0, mask=0xFFFFFFFF
  if (
    isPropertyDefined(stencil, 'stencilFunc') &&
    isPropertyDefined(stencil, 'stencilRef') &&
    isPropertyDefined(stencil, 'stencilValueMask')
  ) {
    gl.stencilFuncSeparate(
      gl.FRONT,
      stencil!.stencilFunc as number,
      stencil!.stencilRef as number,
      stencil!.stencilValueMask as number
    );
  } else if (
    isDefined(current?.stencilFunc) &&
    isDefined(current?.stencilRef) &&
    isDefined(current?.stencilValueMask)
  ) {
    gl.stencilFuncSeparate(gl.FRONT, gl.ALWAYS, 0, 0xffffffff);
  }

  // Front face stencil writemask - default: 0xFFFFFFFF
  if (isPropertyDefined(stencil, 'stencilWritemask')) {
    gl.stencilMaskSeparate(gl.FRONT, stencil!.stencilWritemask as number);
  } else if (isDefined(current?.stencilWritemask)) {
    gl.stencilMaskSeparate(gl.FRONT, 0xffffffff);
  }

  // Front face stencil operations - default: KEEP for all
  if (
    isPropertyDefined(stencil, 'stencilFail') &&
    isPropertyDefined(stencil, 'stencilPassDepthFail') &&
    isPropertyDefined(stencil, 'stencilPassDepthPass')
  ) {
    gl.stencilOpSeparate(
      gl.FRONT,
      stencil!.stencilFail as number,
      stencil!.stencilPassDepthFail as number,
      stencil!.stencilPassDepthPass as number
    );
  } else if (
    isDefined(current?.stencilFail) &&
    isDefined(current?.stencilPassDepthFail) &&
    isDefined(current?.stencilPassDepthPass)
  ) {
    gl.stencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.KEEP);
  }

  // Back face stencil func - default: ALWAYS, ref=0, mask=0xFFFFFFFF
  if (
    isPropertyDefined(stencil, 'stencilBackFunc') &&
    isPropertyDefined(stencil, 'stencilBackRef') &&
    isPropertyDefined(stencil, 'stencilBackValueMask')
  ) {
    gl.stencilFuncSeparate(
      gl.BACK,
      stencil!.stencilBackFunc as number,
      stencil!.stencilBackRef as number,
      stencil!.stencilBackValueMask as number
    );
  } else if (
    isDefined(current?.stencilBackFunc) &&
    isDefined(current?.stencilBackRef) &&
    isDefined(current?.stencilBackValueMask)
  ) {
    gl.stencilFuncSeparate(gl.BACK, gl.ALWAYS, 0, 0xffffffff);
  }

  // Back face stencil writemask - default: 0xFFFFFFFF
  if (isPropertyDefined(stencil, 'stencilBackWritemask')) {
    gl.stencilMaskSeparate(gl.BACK, stencil!.stencilBackWritemask as number);
  } else if (isDefined(current?.stencilBackWritemask)) {
    gl.stencilMaskSeparate(gl.BACK, 0xffffffff);
  }

  // Back face stencil operations - default: KEEP for all
  if (
    isPropertyDefined(stencil, 'stencilBackFail') &&
    isPropertyDefined(stencil, 'stencilBackPassDepthFail') &&
    isPropertyDefined(stencil, 'stencilBackPassDepthPass')
  ) {
    gl.stencilOpSeparate(
      gl.BACK,
      stencil!.stencilBackFail as number,
      stencil!.stencilBackPassDepthFail as number,
      stencil!.stencilBackPassDepthPass as number
    );
  } else if (
    isDefined(current?.stencilBackFail) &&
    isDefined(current?.stencilBackPassDepthFail) &&
    isDefined(current?.stencilBackPassDepthPass)
  ) {
    gl.stencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.KEEP);
  }
}

/**
 * Apply color write mask state
 */
function applyColorState(
  gl: WebGL2RenderingContext,
  color: WebGLColorState | undefined,
  current?: WebGLColorState
): void {
  // ColorWritemask - default: [true, true, true, true]
  if (isPropertyDefined(color, 'colorWritemask')) {
    const mask = color!.colorWritemask as boolean[];
    gl.colorMask(mask[0], mask[1], mask[2], mask[3]);
  } else if (isDefined(current?.colorWritemask)) {
    gl.colorMask(true, true, true, true);
  }
}

/**
 * Apply culling state
 */
function applyCullingState(
  gl: WebGL2RenderingContext,
  culling: WebGLCullingState | undefined,
  current?: WebGLCullingState
): void {
  // CullFaceMode - default: BACK
  if (isPropertyDefined(culling, 'cullFaceMode')) {
    gl.cullFace(culling!.cullFaceMode as number);
  } else if (isDefined(current?.cullFaceMode)) {
    gl.cullFace(gl.BACK);
  }

  // FrontFace - default: CCW (counter-clockwise)
  if (isPropertyDefined(culling, 'frontFace')) {
    gl.frontFace(culling!.frontFace as number);
  } else if (isDefined(current?.frontFace)) {
    gl.frontFace(gl.CCW);
  }
}

/**
 * Apply line width state
 */
function applyLineState(
  gl: WebGL2RenderingContext,
  line: WebGLLineState | undefined,
  current?: WebGLLineState
): void {
  // LineWidth - default: 1
  if (isPropertyDefined(line, 'lineWidth')) {
    gl.lineWidth(line!.lineWidth as number);
  } else if (isDefined(current?.lineWidth)) {
    gl.lineWidth(1);
  }
}

/**
 * Apply polygon offset state
 */
function applyPolygonOffsetState(
  gl: WebGL2RenderingContext,
  polygonOffset: WebGLPolygonOffsetState | undefined,
  current?: WebGLPolygonOffsetState
): void {
  // PolygonOffset - default: factor=0, units=0
  if (
    isPropertyDefined(polygonOffset, 'polygonOffsetFactor') &&
    isPropertyDefined(polygonOffset, 'polygonOffsetUnits')
  ) {
    gl.polygonOffset(
      polygonOffset!.polygonOffsetFactor as number,
      polygonOffset!.polygonOffsetUnits as number
    );
  } else if (isDefined(current?.polygonOffsetFactor) && isDefined(current?.polygonOffsetUnits)) {
    gl.polygonOffset(0, 0);
  }
}

/**
 * Apply sample coverage state
 */
function applySampleState(
  gl: WebGL2RenderingContext,
  sample: WebGLSampleState | undefined,
  current?: WebGLSampleState
): void {
  // SampleCoverage - default: value=1, invert=false
  if (
    isPropertyDefined(sample, 'sampleCoverageValue') &&
    isPropertyDefined(sample, 'sampleCoverageInvert')
  ) {
    gl.sampleCoverage(
      sample!.sampleCoverageValue as number,
      sample!.sampleCoverageInvert as boolean
    );
  } else if (isDefined(current?.sampleCoverageValue) && isDefined(current?.sampleCoverageInvert)) {
    gl.sampleCoverage(1, false);
  }
}

/**
 * Apply pixel store parameters
 */
function applyPixelStoreState(
  gl: WebGL2RenderingContext,
  pixelStore: WebGLPixelStoreState | undefined,
  current?: WebGLPixelStoreState
): void {
  // PackAlignment - default: 4
  if (isPropertyDefined(pixelStore, 'packAlignment')) {
    gl.pixelStorei(gl.PACK_ALIGNMENT, pixelStore!.packAlignment as number);
  } else if (isDefined(current?.packAlignment)) {
    gl.pixelStorei(gl.PACK_ALIGNMENT, 4);
  }

  // UnpackAlignment - default: 4
  if (isPropertyDefined(pixelStore, 'unpackAlignment')) {
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, pixelStore!.unpackAlignment as number);
  } else if (isDefined(current?.unpackAlignment)) {
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
  }

  // UnpackFlipY - default: false
  if (isPropertyDefined(pixelStore, 'unpackFlipY')) {
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, pixelStore!.unpackFlipY ? 1 : 0);
  } else if (isDefined(current?.unpackFlipY)) {
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
  }

  // UnpackPremultiplyAlpha - default: false
  if (isPropertyDefined(pixelStore, 'unpackPremultiplyAlpha')) {
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, pixelStore!.unpackPremultiplyAlpha ? 1 : 0);
  } else if (isDefined(current?.unpackPremultiplyAlpha)) {
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
  }

  // PackRowLength - default: 0
  if (isPropertyDefined(pixelStore, 'packRowLength')) {
    gl.pixelStorei(gl.PACK_ROW_LENGTH, pixelStore!.packRowLength as number);
  } else if (isDefined(current?.packRowLength)) {
    gl.pixelStorei(gl.PACK_ROW_LENGTH, 0);
  }

  // PackSkipPixels - default: 0
  if (isPropertyDefined(pixelStore, 'packSkipPixels')) {
    gl.pixelStorei(gl.PACK_SKIP_PIXELS, pixelStore!.packSkipPixels as number);
  } else if (isDefined(current?.packSkipPixels)) {
    gl.pixelStorei(gl.PACK_SKIP_PIXELS, 0);
  }

  // PackSkipRows - default: 0
  if (isPropertyDefined(pixelStore, 'packSkipRows')) {
    gl.pixelStorei(gl.PACK_SKIP_ROWS, pixelStore!.packSkipRows as number);
  } else if (isDefined(current?.packSkipRows)) {
    gl.pixelStorei(gl.PACK_SKIP_ROWS, 0);
  }

  // UnpackRowLength - default: 0
  if (isPropertyDefined(pixelStore, 'unpackRowLength')) {
    gl.pixelStorei(gl.UNPACK_ROW_LENGTH, pixelStore!.unpackRowLength as number);
  } else if (isDefined(current?.unpackRowLength)) {
    gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
  }

  // UnpackImageHeight - default: 0
  if (isPropertyDefined(pixelStore, 'unpackImageHeight')) {
    gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, pixelStore!.unpackImageHeight as number);
  } else if (isDefined(current?.unpackImageHeight)) {
    gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, 0);
  }

  // UnpackSkipPixels - default: 0
  if (isPropertyDefined(pixelStore, 'unpackSkipPixels')) {
    gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, pixelStore!.unpackSkipPixels as number);
  } else if (isDefined(current?.unpackSkipPixels)) {
    gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0);
  }

  // UnpackSkipRows - default: 0
  if (isPropertyDefined(pixelStore, 'unpackSkipRows')) {
    gl.pixelStorei(gl.UNPACK_SKIP_ROWS, pixelStore!.unpackSkipRows as number);
  } else if (isDefined(current?.unpackSkipRows)) {
    gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0);
  }

  // UnpackSkipImages - default: 0
  if (isPropertyDefined(pixelStore, 'unpackSkipImages')) {
    gl.pixelStorei(gl.UNPACK_SKIP_IMAGES, pixelStore!.unpackSkipImages as number);
  } else if (isDefined(current?.unpackSkipImages)) {
    gl.pixelStorei(gl.UNPACK_SKIP_IMAGES, 0);
  }
}

/**
 * Apply query state
 */
function applyQueryState(gl: WebGL2RenderingContext, queries: WebGLQueryState): void {
  // Note: Query state includes active queries, but we can't directly set them
  // as beginQuery requires starting a new query operation.
  // This is here for completeness but may need special handling.

  if (isDefined(queries.currentAnySamplesPassed) && queries.currentAnySamplesPassed) {
    console.warn(
      '[WebGLStateApply] Cannot restore active query state for ANY_SAMPLES_PASSED - queries cannot be directly restored'
    );
    // gl.beginQuery(gl.ANY_SAMPLES_PASSED, queries.currentAnySamplesPassed);
  }
  if (
    isDefined(queries.currentAnySamplesPassedConservative) &&
    queries.currentAnySamplesPassedConservative
  ) {
    console.warn(
      '[WebGLStateApply] Cannot restore active query state for ANY_SAMPLES_PASSED_CONSERVATIVE - queries cannot be directly restored'
    );
    // gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, queries.currentAnySamplesPassedConservative);
  }
  if (
    isDefined(queries.currentTransformFeedbackPrimitivesWritten) &&
    queries.currentTransformFeedbackPrimitivesWritten
  ) {
    console.warn(
      '[WebGLStateApply] Cannot restore active query state for TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN - queries cannot be directly restored'
    );
    // gl.beginQuery(gl.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, queries.currentTransformFeedbackPrimitivesWritten);
  }
}
