part of '_gpu.dart';

// ---------------------------------------------------------------------------
// Render-target value types (mirroring flutter_gpu).
// ---------------------------------------------------------------------------

base class ColorAttachment {
  ColorAttachment({
    this.loadAction = LoadAction.clear,
    this.storeAction = StoreAction.store,
    vm.Vector4? clearValue,
    required this.texture,
    this.mipLevel = 0,
    this.slice = 0,
    this.resolveTexture,
  }) : clearValue = clearValue ?? vm.Vector4.zero();

  LoadAction loadAction;
  StoreAction storeAction;
  vm.Vector4 clearValue;
  Texture texture;

  /// The mip level of [texture] to render into.
  int mipLevel;

  /// The slice of [texture] to render into. Cubemap textures are not
  /// supported on the web backend, so this must be 0.
  // TODO(rendertarget): cubemap slices on the web backend.
  int slice;

  Texture? resolveTexture;
}

base class DepthStencilAttachment {
  DepthStencilAttachment({
    this.depthLoadAction = LoadAction.clear,
    this.depthStoreAction = StoreAction.dontCare,
    this.depthClearValue = 0.0,
    this.stencilLoadAction = LoadAction.clear,
    this.stencilStoreAction = StoreAction.dontCare,
    this.stencilClearValue = 0,
    required this.texture,
    this.mipLevel = 0,
    this.slice = 0,
  });

  LoadAction depthLoadAction;
  StoreAction depthStoreAction;
  double depthClearValue;
  LoadAction stencilLoadAction;
  StoreAction stencilStoreAction;
  int stencilClearValue;
  Texture texture;

  /// The mip level of [texture] to render into.
  int mipLevel;

  /// The slice of [texture] to render into. Cubemap textures are not
  /// supported on the web backend, so this must be 0.
  int slice;
}

base class StencilConfig {
  StencilConfig({
    this.compareFunction = CompareFunction.always,
    this.stencilFailureOperation = StencilOperation.keep,
    this.depthFailureOperation = StencilOperation.keep,
    this.depthStencilPassOperation = StencilOperation.keep,
    this.readMask = 0xFFFFFFFF,
    this.writeMask = 0xFFFFFFFF,
  });

  CompareFunction compareFunction;
  StencilOperation stencilFailureOperation;
  StencilOperation depthFailureOperation;
  StencilOperation depthStencilPassOperation;
  int readMask;
  int writeMask;
}

enum StencilFace { both, front, back }

base class ColorBlendEquation {
  ColorBlendEquation({
    this.colorBlendOperation = BlendOperation.add,
    this.sourceColorBlendFactor = BlendFactor.one,
    this.destinationColorBlendFactor = BlendFactor.oneMinusSourceAlpha,
    this.alphaBlendOperation = BlendOperation.add,
    this.sourceAlphaBlendFactor = BlendFactor.one,
    this.destinationAlphaBlendFactor = BlendFactor.oneMinusSourceAlpha,
  });

  BlendOperation colorBlendOperation;
  BlendFactor sourceColorBlendFactor;
  BlendFactor destinationColorBlendFactor;
  BlendOperation alphaBlendOperation;
  BlendFactor sourceAlphaBlendFactor;
  BlendFactor destinationAlphaBlendFactor;
}

base class SamplerOptions {
  SamplerOptions({
    this.minFilter = MinMagFilter.nearest,
    this.magFilter = MinMagFilter.nearest,
    this.mipFilter = MipFilter.nearest,
    this.widthAddressMode = SamplerAddressMode.clampToEdge,
    this.heightAddressMode = SamplerAddressMode.clampToEdge,
    this.maxAnisotropy = 1,
  });

  MinMagFilter minFilter;
  MinMagFilter magFilter;
  MipFilter mipFilter;
  SamplerAddressMode widthAddressMode;
  SamplerAddressMode heightAddressMode;

  /// The maximum anisotropy clamp used when sampling. The default value of 1
  /// disables anisotropic filtering. Mirrors `package:flutter_gpu`; applied via
  /// `EXT_texture_filter_anisotropic` and clamped to the device maximum.
  int maxAnisotropy;
}

base class DepthRange {
  DepthRange({this.zNear = 0.0, this.zFar = 1.0});
  double zNear;
  double zFar;
}

base class Scissor {
  Scissor({this.x = 0, this.y = 0, this.width = 0, this.height = 0});
  int x, y, width, height;
}

base class Viewport {
  Viewport({
    this.x = 0,
    this.y = 0,
    this.width = 0,
    this.height = 0,
    DepthRange? depthRange,
  }) : depthRange = depthRange ?? DepthRange();

  int x, y, width, height;
  DepthRange depthRange;
}

base class RenderTarget {
  const RenderTarget({
    this.colorAttachments = const <ColorAttachment>[],
    this.depthStencilAttachment,
  });

  RenderTarget.singleColor(
    ColorAttachment colorAttachment, {
    DepthStencilAttachment? depthStencilAttachment,
  }) : this(
         colorAttachments: [colorAttachment],
         depthStencilAttachment: depthStencilAttachment,
       );

  final List<ColorAttachment> colorAttachments;
  final DepthStencilAttachment? depthStencilAttachment;
}

// ---------------------------------------------------------------------------
// RenderPass: records bind / state / draw calls and immediately issues them
// against the GpuContext's GL2 context. Phase 1 is "draw a triangle" so most
// state setters are stubbed.
// ---------------------------------------------------------------------------

base class RenderPass {
  RenderPass._(this._gpuContext, this._target) {
    _bindFramebuffer();
    _applyLoadActions();
    // Reset the fixed-function state that GL holds globally but Impeller
    // scopes per pass. Cull mode and winding order otherwise leak from the
    // previous pass's last draw into a pass that doesn't set them (e.g. the
    // full-screen tonemap blit, which inherits whatever the scene pass left
    // bound). A mirrored (negative-determinant) node leaves the winding
    // flipped, which would then back-face cull the blit and blank the frame.
    // Mirrors Impeller's GLES backend, which re-initialises this state at the
    // start of every pass encode.
    final gl = _gpuContext._gl;
    gl.disable(web.WebGL2RenderingContext.CULL_FACE);
    gl.frontFace(web.WebGL2RenderingContext.CCW);
  }

  final GpuContext _gpuContext;
  final RenderTarget _target;

  RenderPipeline? _boundPipeline;
  web.WebGLVertexArrayObject? _vao;

  /// Vertex-stream bindings recorded since the last [clearBindings], applied
  /// through the VAO cache when the draw is issued: (view, slot, whether the
  /// stream is instance-rate).
  final List<(BufferView, int, bool)> _pendingVertexBindings = [];

  /// Uniform-block binding points, and texture units mapped to the GL target
  /// bound there, occupied since the last [clearBindings]. Recorded so the
  /// clear can release exactly what it handed out.
  final Set<int> _boundUniformBlocks = {};
  final Map<int, int> _boundTextureUnits = {};
  PrimitiveType _primitiveType = PrimitiveType.triangle;
  BufferView? _inlineVertexBufferView;

  BufferView? _indexBufferView;
  IndexType _indexType = IndexType.int32;

  web.WebGLFramebuffer? _fbo;
  bool _finished = false;

  void _ensureVao() {
    final gl = _gpuContext._gl;
    _vao ??= gl.createVertexArray();
    gl.bindVertexArray(_vao);
  }

  // ---- framebuffer setup ---------------------------------------------------

  void _bindFramebuffer() {
    final gl = _gpuContext._gl;

    if (_target.colorAttachments.isEmpty) {
      throw Exception('RenderTarget must have at least one color attachment');
    }
    final colorAttachment = _target.colorAttachments.first;
    final color = colorAttachment.texture;
    final mipLevel = colorAttachment.mipLevel;
    final slice = colorAttachment.slice;
    final depth = _target.depthStencilAttachment?.texture;
    // A color slice selects a cube face; depth slices/mips are not supported.
    if (slice != 0 && color.textureType != TextureType.textureCube) {
      throw UnsupportedError(
        'Rendering to a 2D texture slice is not supported on the web backend',
      );
    }
    if ((_target.depthStencilAttachment?.slice ?? 0) != 0) {
      throw UnsupportedError(
        'Rendering to a depth texture slice is not supported on the web backend',
      );
    }
    if ((_target.depthStencilAttachment?.mipLevel ?? 0) != 0) {
      throw UnsupportedError(
        'Rendering to a depth mip level is not supported on the web backend',
      );
    }

    // Configured framebuffers (including the completeness check, a
    // synchronous GPU round trip) are cached per attachment combination.
    final fbo = _gpuContext._framebufferFor(
      color,
      mipLevel,
      slice,
      depth,
      () => _createFramebuffer(gl, color, mipLevel, slice, depth),
    );
    _fbo = fbo;
    gl.bindFramebuffer(web.WebGL2RenderingContext.FRAMEBUFFER, fbo);
    gl.viewport(
      0,
      0,
      (color.width >> mipLevel).clamp(1, color.width).toInt(),
      (color.height >> mipLevel).clamp(1, color.height).toInt(),
    );
  }

  static web.WebGLFramebuffer _createFramebuffer(
    web.WebGL2RenderingContext gl,
    Texture color,
    int mipLevel,
    int slice,
    Texture? depth,
  ) {
    final fbo = gl.createFramebuffer();
    if (fbo == null) {
      throw StateError('Failed to create WebGL framebuffer');
    }
    gl.bindFramebuffer(web.WebGL2RenderingContext.FRAMEBUFFER, fbo);

    if (color.sampleCount == 1) {
      // For a cube the attachment target is the face; for 2D it is TEXTURE_2D.
      gl.framebufferTexture2D(
        web.WebGL2RenderingContext.FRAMEBUFFER,
        web.WebGL2RenderingContext.COLOR_ATTACHMENT0,
        color.glSliceTarget(slice),
        color.glTexture,
        mipLevel,
      );
    } else {
      assert(
        mipLevel == 0,
        'Multisampled attachments cannot target a mip level',
      );
      gl.framebufferRenderbuffer(
        web.WebGL2RenderingContext.FRAMEBUFFER,
        web.WebGL2RenderingContext.COLOR_ATTACHMENT0,
        web.WebGL2RenderingContext.RENDERBUFFER,
        color.glRenderbuffer,
      );
    }

    if (depth != null) {
      if (depth.sampleCount == 1) {
        gl.framebufferTexture2D(
          web.WebGL2RenderingContext.FRAMEBUFFER,
          web.WebGL2RenderingContext.DEPTH_STENCIL_ATTACHMENT,
          web.WebGL2RenderingContext.TEXTURE_2D,
          depth.glTexture,
          0,
        );
      } else {
        gl.framebufferRenderbuffer(
          web.WebGL2RenderingContext.FRAMEBUFFER,
          web.WebGL2RenderingContext.DEPTH_STENCIL_ATTACHMENT,
          web.WebGL2RenderingContext.RENDERBUFFER,
          depth.glRenderbuffer,
        );
      }
    }

    final status = gl.checkFramebufferStatus(
      web.WebGL2RenderingContext.FRAMEBUFFER,
    );
    if (status != web.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE) {
      throw Exception(
        'Framebuffer incomplete (status 0x${status.toRadixString(16)})',
      );
    }
    return fbo;
  }

  void _applyLoadActions() {
    final gl = _gpuContext._gl;
    int clearMask = 0;

    final color = _target.colorAttachments.first;
    if (color.loadAction == LoadAction.clear) {
      gl.clearColor(
        color.clearValue.x,
        color.clearValue.y,
        color.clearValue.z,
        color.clearValue.w,
      );
      clearMask |= web.WebGL2RenderingContext.COLOR_BUFFER_BIT;
    }
    final depth = _target.depthStencilAttachment;
    if (depth != null) {
      if (depth.depthLoadAction == LoadAction.clear) {
        // glClear(DEPTH) respects depthMask, which a prior draw may have
        // turned off; force it on so the clear actually writes.
        gl.depthMask(true);
        gl.clearDepth(depth.depthClearValue);
        clearMask |= web.WebGL2RenderingContext.DEPTH_BUFFER_BIT;
      }
      if (depth.stencilLoadAction == LoadAction.clear) {
        gl.clearStencil(depth.stencilClearValue);
        clearMask |= web.WebGL2RenderingContext.STENCIL_BUFFER_BIT;
      }
    }
    if (clearMask != 0) {
      gl.clear(clearMask);
    }
  }

  // ---- public API ----------------------------------------------------------

  void bindPipeline(RenderPipeline pipeline) {
    final gl = _gpuContext._gl;
    _boundPipeline = pipeline;
    gl.useProgram(pipeline._program);
    // Give every active uniform block a valid (zeroed) buffer up front, so a
    // block the caller never binds does not raise a used-but-unbound uniform
    // buffer error on this backend. Explicit bindUniform calls override it.
    pipeline._bindDefaultUniformBlocks();
  }

  void bindVertexBuffer(BufferView bufferView, {int slot = 0}) {
    final pipeline = _boundPipeline;
    if (pipeline == null) {
      throw StateError('bindVertexBuffer called before bindPipeline');
    }

    final layout = pipeline.vertexLayout;
    if (layout == null && slot != 0) {
      throw RangeError.value(
        slot,
        'slot',
        'Slots other than 0 need an explicit pipeline VertexLayout',
      );
    }

    if (layout == null && pipeline.vertexShader.vertexInputs.isEmpty) {
      // Inline pipeline (no reflected attributes): configured at draw time
      // from the vertex count; not part of the cached-VAO path.
      _inlineVertexBufferView = bufferView;
      return;
    }
    _inlineVertexBufferView = null;

    var instanceRate = false;
    if (layout != null) {
      if (slot < 0 || slot >= layout.buffers.length) {
        throw RangeError.value(
          slot,
          'slot',
          'Pipeline VertexLayout declares ${layout.buffers.length} buffers',
        );
      }
      instanceRate = layout.buffers[slot].stepMode == VertexStepMode.instance;
    }
    // Deferred: applied (through the VAO cache) when the draw is issued.
    _pendingVertexBindings.add((bufferView, slot, instanceRate));
  }

  /// Applies one recorded vertex-stream binding to the currently bound VAO:
  /// binds the stream's GL buffer and points/enables its attributes.
  void _applyVertexBinding(BufferView bufferView, int slot) {
    final gl = _gpuContext._gl;
    final pipeline = _boundPipeline!;
    bufferView.buffer._bindForTarget(web.WebGL2RenderingContext.ARRAY_BUFFER);

    final layout = pipeline.vertexLayout;
    if (layout != null) {
      // Explicit layout: the buffer's slot describes its stride, step mode,
      // and named attributes; the shader's reflection only supplies the
      // attribute locations.
      final buffer = layout.buffers[slot];
      final divisor = buffer.stepMode == VertexStepMode.instance ? 1 : 0;
      for (final attribute in buffer.attributes) {
        final input = pipeline.vertexShader.vertexInputByName(attribute.name);
        if (input == null) {
          throw StateError(
            'Vertex shader has no input named "${attribute.name}"',
          );
        }
        if (attribute.format.name.startsWith('uint') ||
            attribute.format.name.startsWith('sint')) {
          throw UnimplementedError(
            'Integer vertex formats are not implemented in the WebGL2 '
            'backend',
          );
        }
        gl.enableVertexAttribArray(input.location);
        gl.vertexAttribPointer(
          input.location,
          attribute.format.componentCount,
          web.WebGL2RenderingContext.FLOAT,
          false,
          buffer.strideInBytes,
          bufferView.offsetInBytes + attribute.offsetInBytes,
        );
        gl.vertexAttribDivisor(input.location, divisor);
      }
      return;
    }

    final inputs = pipeline.vertexShader.vertexInputs;
    final stride = pipeline.vertexShader.vertexStride;
    for (final input in inputs) {
      gl.enableVertexAttribArray(input.location);
      gl.vertexAttribPointer(
        input.location,
        input.componentCount,
        web.WebGL2RenderingContext.FLOAT,
        false,
        stride,
        bufferView.offsetInBytes + input.offsetInBytes,
      );
      // Divisor state lives in the VAO and may be left over from an
      // instanced layout; reset it for vertex-rate inputs.
      gl.vertexAttribDivisor(input.location, 0);
    }
  }

  /// Binds the vertex (and, when [needsIndex] is set, index) state for the
  /// draw being issued, through a cached VAO.
  ///
  /// Vertex-attribute specification is the dominant per-draw GL traffic, and
  /// on the web every GL call is validated in the browser's GPU process, so
  /// re-specifying stable geometry streams per draw is expensive out of all
  /// proportion to the Dart-side cost. Geometry streams are stable per
  /// (pipeline, buffers, offsets), so their full attribute state is captured
  /// in a VAO once and re-bound with a single call thereafter.
  ///
  /// Instance-rate streams are excluded from the cache key and re-pointed on
  /// the bound VAO every draw: they ride the per-frame bump allocator, so
  /// their offset changes per draw and every instanced draw re-points them
  /// anyway.
  void _applyVertexState({required bool needsIndex}) {
    if (_inlineVertexBufferView != null) {
      _ensureVao();
      return;
    }
    final gl = _gpuContext._gl;
    final pipeline = _boundPipeline!;
    final key = StringBuffer()..write(identityHashCode(pipeline));
    for (final (view, slot, instanceRate) in _pendingVertexBindings) {
      if (instanceRate) continue;
      key
        ..write('|')
        ..write(identityHashCode(view.buffer))
        ..write(':')
        ..write(view.offsetInBytes)
        ..write(':')
        ..write(slot);
    }
    final indexView = needsIndex ? _indexBufferView : null;
    if (indexView != null) {
      key
        ..write('#')
        ..write(identityHashCode(indexView.buffer))
        ..write(':')
        ..write(indexView.offsetInBytes);
    }
    final cacheKey = key.toString();
    final cache = _gpuContext._vaoCache;
    // Remove-and-reinsert keeps the map in least-recently-used order.
    var vao = cache.remove(cacheKey);
    if (vao != null) {
      cache[cacheKey] = vao;
      gl.bindVertexArray(vao);
    } else {
      vao = gl.createVertexArray();
      if (vao == null) {
        throw StateError('Failed to create WebGL vertex array');
      }
      gl.bindVertexArray(vao);
      for (final (view, slot, instanceRate) in _pendingVertexBindings) {
        if (instanceRate) continue;
        _applyVertexBinding(view, slot);
      }
      // ELEMENT_ARRAY_BUFFER binding is VAO state; capture it with the rest.
      indexView?.buffer._bindForTarget(
        web.WebGL2RenderingContext.ELEMENT_ARRAY_BUFFER,
      );
      cache[cacheKey] = vao;
      if (cache.length > GpuContext._kMaxCachedVaos) {
        final oldest = cache.keys.first;
        gl.deleteVertexArray(cache.remove(oldest)!);
      }
    }
    for (final (view, slot, instanceRate) in _pendingVertexBindings) {
      if (instanceRate) _applyVertexBinding(view, slot);
    }
  }

  void bindIndexBuffer(BufferView bufferView, IndexType indexType) {
    // Deferred: ELEMENT_ARRAY_BUFFER binding is VAO state, applied (through
    // the VAO cache) when the draw is issued.
    _indexBufferView = bufferView;
    _indexType = indexType;
  }

  void bindUniform(UniformSlot slot, BufferView bufferView) {
    final pipeline = _boundPipeline;
    if (pipeline == null) {
      throw StateError('bindUniform called before bindPipeline');
    }
    final struct = slot.shader._uniformStructs[slot.uniformName];
    if (struct == null) return;

    final gl = _gpuContext._gl;

    // GLSL ES 3.00 bundles expose the struct as a std140 uniform block;
    // attach the emplaced bytes directly as a buffer range. The emplaced
    // layout matches std140 (it is what the reflection metadata describes),
    // and HostBuffer offsets are 256-aligned, satisfying WebGL2's
    // UNIFORM_BUFFER_OFFSET_ALIGNMENT.
    final blockBinding = pipeline._structBlockBindings[struct.name];
    if (blockBinding != null) {
      final glBuffer = bufferView.buffer._bindForTarget(
        web.WebGL2RenderingContext.UNIFORM_BUFFER,
      );
      // Bind at least the driver-reported block data size; the emplaced
      // length alone can be smaller than the driver's padded size, and a
      // too-small range makes the draw a no-op. DeviceBuffer pads its GL
      // data store so the extended range stays inside the buffer.
      var lengthInBytes = bufferView.lengthInBytes;
      final minBindSize = pipeline._structBlockBindSizes[struct.name];
      if (minBindSize != null && minBindSize > lengthInBytes) {
        lengthInBytes = minBindSize;
      }
      gl.bindBufferRange(
        web.WebGL2RenderingContext.UNIFORM_BUFFER,
        blockBinding,
        glBuffer,
        bufferView.offsetInBytes,
        lengthInBytes,
      );
      _boundUniformBlocks.add(blockBinding);
      return;
    }

    final floats = bufferView.buffer._stagingFloats;
    final base = bufferView.offsetInBytes;
    final locations = pipeline._structLocations[struct.name];
    for (var i = 0; i < struct.members.length; i++) {
      final loc = locations?[i];
      if (loc == null) continue; // optimized out by the linker
      final member = struct.members[i];
      _setUniformMember(gl, loc, member, floats, base + member.offsetInBytes);
    }
  }

  // Scratch for the rare matrix repack; grows to the largest repacked member
  // and is reused across draws.
  static Float32List _matrixScratch = Float32List(64);

  void _setUniformMember(
    web.WebGL2RenderingContext gl,
    web.WebGLUniformLocation loc,
    _UniformMember member,
    Float32List floats,
    int byteOffset,
  ) {
    final floatOffset = byteOffset >> 2;
    if (member.columns > 1) {
      // Matrix, possibly an array. In std140 each matrix column is aligned
      // to 16 bytes (a mat3 column is a vec3 padded to vec4); GL's
      // uniformMatrix*fv wants tightly-packed columns. The matrix count
      // comes from the total size, not arrayElements (which Impeller
      // overloads to mean column count for a standalone matrix).
      const columnStride = 16;
      final cols = member.columns;
      final rows = member.vecSize;
      final count = member.totalSizeInBytes ~/ (cols * columnStride);
      if (rows * 4 == columnStride) {
        // mat4 columns are already tight in std140; upload straight from the
        // staging view with no copy (the overwhelmingly common case).
        gl.uniformMatrix4fv(loc, false, floats.toJS, floatOffset, count * 16);
        return;
      }
      final tightLength = count * cols * rows;
      if (_matrixScratch.length < tightLength) {
        _matrixScratch = Float32List(tightLength);
      }
      final tight = _matrixScratch;
      var w = 0;
      for (var m = 0; m < count; m++) {
        for (var c = 0; c < cols; c++) {
          final col = floatOffset + (m * cols + c) * (columnStride >> 2);
          for (var r = 0; r < rows; r++) {
            tight[w++] = floats[col + r];
          }
        }
      }
      switch (cols) {
        case 2:
          gl.uniformMatrix2fv(loc, false, tight.toJS, 0, tightLength);
        case 3:
          gl.uniformMatrix3fv(loc, false, tight.toJS, 0, tightLength);
      }
    } else {
      // Vector / scalar, possibly an array. vec4 arrays are tightly packed
      // (16-byte elements); vec3/vec2/scalar arrays would have std140
      // padding, but flutter_scene's uniforms don't use those.
      final count = member.arrayElements == 0 ? 1 : member.arrayElements;
      final length = member.vecSize * count;
      switch (member.vecSize) {
        case 1:
          gl.uniform1fv(loc, floats.toJS, floatOffset, length);
        case 2:
          gl.uniform2fv(loc, floats.toJS, floatOffset, length);
        case 3:
          gl.uniform3fv(loc, floats.toJS, floatOffset, length);
        case 4:
          gl.uniform4fv(loc, floats.toJS, floatOffset, length);
      }
    }
  }

  void bindTexture(
    UniformSlot slot,
    Texture texture, {
    SamplerOptions? sampler,
  }) {
    final pipeline = _boundPipeline;
    if (pipeline == null) {
      throw StateError('bindTexture called before bindPipeline');
    }
    final unit = pipeline._samplerUnits[slot.uniformName];
    if (unit == null) return;

    final gl = _gpuContext._gl;
    final target = texture.glTarget;
    gl.activeTexture(web.WebGL2RenderingContext.TEXTURE0 + unit);
    gl.bindTexture(target, texture.glTexture);
    _boundTextureUnits[unit] = target;
    if (sampler != null) {
      // Sampler parameters are per-texture-object GL state; skip the ones
      // already applied to this texture. Per-draw texParameteri calls are
      // validated in the browser's GPU process and add up across draws.
      final minFilter = _glMinFilter(sampler, texture);
      if (texture._lastMinFilter != minFilter) {
        texture._lastMinFilter = minFilter;
        gl.texParameteri(
          target,
          web.WebGL2RenderingContext.TEXTURE_MIN_FILTER,
          minFilter,
        );
      }
      final magFilter = sampler.magFilter == MinMagFilter.nearest
          ? web.WebGL2RenderingContext.NEAREST
          : web.WebGL2RenderingContext.LINEAR;
      if (texture._lastMagFilter != magFilter) {
        texture._lastMagFilter = magFilter;
        gl.texParameteri(
          target,
          web.WebGL2RenderingContext.TEXTURE_MAG_FILTER,
          magFilter,
        );
      }
      final wrapS = _glAddressMode(sampler.widthAddressMode);
      if (texture._lastWrapS != wrapS) {
        texture._lastWrapS = wrapS;
        gl.texParameteri(
          target,
          web.WebGL2RenderingContext.TEXTURE_WRAP_S,
          wrapS,
        );
      }
      final wrapT = _glAddressMode(sampler.heightAddressMode);
      if (texture._lastWrapT != wrapT) {
        texture._lastWrapT = wrapT;
        gl.texParameteri(
          target,
          web.WebGL2RenderingContext.TEXTURE_WRAP_T,
          wrapT,
        );
      }
      final maxAniso = _gpuContext.maxSupportedAnisotropy;
      if (sampler.maxAnisotropy > 1 && maxAniso > 1) {
        // EXT_texture_filter_anisotropic: TEXTURE_MAX_ANISOTROPY_EXT = 0x84FE.
        final anisotropy = sampler.maxAnisotropy.clamp(1, maxAniso).toDouble();
        if (texture._lastAnisotropy != anisotropy) {
          texture._lastAnisotropy = anisotropy;
          gl.texParameterf(target, 0x84FE, anisotropy);
        }
      }
    }
  }

  // The GL minification filter for [sampler]. Mipmap modes apply only when
  // the texture actually has mip levels; a mipmap MIN_FILTER on a
  // single-level texture is incomplete in GL and samples black.
  static int _glMinFilter(SamplerOptions sampler, Texture texture) {
    final nearest = sampler.minFilter == MinMagFilter.nearest;
    if (texture.mipLevelCount <= 1) {
      return nearest
          ? web.WebGL2RenderingContext.NEAREST
          : web.WebGL2RenderingContext.LINEAR;
    }
    return switch ((nearest, sampler.mipFilter)) {
      (true, MipFilter.nearest) =>
        web.WebGL2RenderingContext.NEAREST_MIPMAP_NEAREST,
      (true, MipFilter.linear) =>
        web.WebGL2RenderingContext.NEAREST_MIPMAP_LINEAR,
      (false, MipFilter.nearest) =>
        web.WebGL2RenderingContext.LINEAR_MIPMAP_NEAREST,
      (false, MipFilter.linear) =>
        web.WebGL2RenderingContext.LINEAR_MIPMAP_LINEAR,
    };
  }

  static int _glAddressMode(SamplerAddressMode mode) {
    switch (mode) {
      case SamplerAddressMode.clampToEdge:
        return web.WebGL2RenderingContext.CLAMP_TO_EDGE;
      case SamplerAddressMode.repeat:
        return web.WebGL2RenderingContext.REPEAT;
      case SamplerAddressMode.mirror:
        return web.WebGL2RenderingContext.MIRRORED_REPEAT;
    }
  }

  void clearBindings() {
    // Drops every per-draw resource binding, leaving the bound pipeline in
    // place (matching flutter_gpu, where the pipeline persists until the next
    // bindPipeline; the encoder skips rebinding a pipeline it already bound,
    // so nulling it here would leave later draws without one).
    //
    // Releasing the uniform blocks and texture units matters beyond tidiness.
    // They are GL context state rather than per-draw descriptors, so leaving
    // them attached lets a draw that should have rebound a slot read the
    // previous draw's resource and render correctly, which hides bind
    // lifetime bugs here that break on the native backends.
    final gl = _gpuContext._gl;
    if (_vao != null) {
      gl.bindVertexArray(null);
    }
    for (final binding in _boundUniformBlocks) {
      gl.bindBufferBase(
        web.WebGL2RenderingContext.UNIFORM_BUFFER,
        binding,
        null,
      );
    }
    _boundUniformBlocks.clear();
    for (final entry in _boundTextureUnits.entries) {
      gl.activeTexture(web.WebGL2RenderingContext.TEXTURE0 + entry.key);
      gl.bindTexture(entry.value, null);
    }
    _boundTextureUnits.clear();
    _inlineVertexBufferView = null;
    _indexBufferView = null;
    _pendingVertexBindings.clear();
  }

  void setColorBlendEnable(bool enable, {int colorAttachmentIndex = 0}) {
    final gl = _gpuContext._gl;
    if (enable) {
      gl.enable(web.WebGL2RenderingContext.BLEND);
    } else {
      gl.disable(web.WebGL2RenderingContext.BLEND);
    }
  }

  void setColorBlendEquation(
    ColorBlendEquation equation, {
    int colorAttachmentIndex = 0,
  }) {
    final gl = _gpuContext._gl;
    gl.blendEquationSeparate(
      _glBlendOp(equation.colorBlendOperation),
      _glBlendOp(equation.alphaBlendOperation),
    );
    gl.blendFuncSeparate(
      _glBlendFactor(equation.sourceColorBlendFactor),
      _glBlendFactor(equation.destinationColorBlendFactor),
      _glBlendFactor(equation.sourceAlphaBlendFactor),
      _glBlendFactor(equation.destinationAlphaBlendFactor),
    );
  }

  void setDepthWriteEnable(bool enable) {
    _gpuContext._gl.depthMask(enable);
  }

  void setDepthCompareOperation(CompareFunction compareFunction) {
    final gl = _gpuContext._gl;
    if (compareFunction == CompareFunction.always) {
      gl.disable(web.WebGL2RenderingContext.DEPTH_TEST);
    } else {
      gl.enable(web.WebGL2RenderingContext.DEPTH_TEST);
      gl.depthFunc(_glCompare(compareFunction));
    }
  }

  void setStencilReference(int referenceValue) {
    /* not implemented; flutter_scene does not use stencil */
  }
  void setStencilConfig(
    StencilConfig configuration, {
    StencilFace targetFace = StencilFace.both,
  }) {
    /* not implemented; flutter_scene does not use stencil */
  }

  void setCullMode(CullMode cullMode) {
    final gl = _gpuContext._gl;
    switch (cullMode) {
      case CullMode.none:
        gl.disable(web.WebGL2RenderingContext.CULL_FACE);
      case CullMode.frontFace:
        gl.enable(web.WebGL2RenderingContext.CULL_FACE);
        gl.cullFace(web.WebGL2RenderingContext.FRONT);
      case CullMode.backFace:
        gl.enable(web.WebGL2RenderingContext.CULL_FACE);
        gl.cullFace(web.WebGL2RenderingContext.BACK);
    }
  }

  void setPolygonMode(PolygonMode polygonMode) {
    /* not implemented; WebGL2 has no glPolygonMode */
  }

  void setPrimitiveType(PrimitiveType primitiveType) {
    _primitiveType = primitiveType;
  }

  void setWindingOrder(WindingOrder windingOrder) {
    // Inverted relative to the requested order: the generated GLES vertex
    // shaders multiply gl_Position.y by `_impeller_y_flip = -1`, which mirrors
    // triangle winding while storing render targets top-down.
    _gpuContext._gl.frontFace(
      windingOrder == WindingOrder.clockwise
          ? web.WebGL2RenderingContext.CCW
          : web.WebGL2RenderingContext.CW,
    );
  }

  static int _glCompare(CompareFunction f) {
    switch (f) {
      case CompareFunction.never:
        return web.WebGL2RenderingContext.NEVER;
      case CompareFunction.always:
        return web.WebGL2RenderingContext.ALWAYS;
      case CompareFunction.less:
        return web.WebGL2RenderingContext.LESS;
      case CompareFunction.equal:
        return web.WebGL2RenderingContext.EQUAL;
      case CompareFunction.lessEqual:
        return web.WebGL2RenderingContext.LEQUAL;
      case CompareFunction.greater:
        return web.WebGL2RenderingContext.GREATER;
      case CompareFunction.notEqual:
        return web.WebGL2RenderingContext.NOTEQUAL;
      case CompareFunction.greaterEqual:
        return web.WebGL2RenderingContext.GEQUAL;
    }
  }

  static int _glBlendOp(BlendOperation op) {
    switch (op) {
      case BlendOperation.add:
        return web.WebGL2RenderingContext.FUNC_ADD;
      case BlendOperation.subtract:
        return web.WebGL2RenderingContext.FUNC_SUBTRACT;
      case BlendOperation.reverseSubtract:
        return web.WebGL2RenderingContext.FUNC_REVERSE_SUBTRACT;
    }
  }

  static int _glBlendFactor(BlendFactor f) {
    switch (f) {
      case BlendFactor.zero:
        return web.WebGL2RenderingContext.ZERO;
      case BlendFactor.one:
        return web.WebGL2RenderingContext.ONE;
      case BlendFactor.sourceColor:
        return web.WebGL2RenderingContext.SRC_COLOR;
      case BlendFactor.oneMinusSourceColor:
        return web.WebGL2RenderingContext.ONE_MINUS_SRC_COLOR;
      case BlendFactor.sourceAlpha:
        return web.WebGL2RenderingContext.SRC_ALPHA;
      case BlendFactor.oneMinusSourceAlpha:
        return web.WebGL2RenderingContext.ONE_MINUS_SRC_ALPHA;
      case BlendFactor.destinationColor:
        return web.WebGL2RenderingContext.DST_COLOR;
      case BlendFactor.oneMinusDestinationColor:
        return web.WebGL2RenderingContext.ONE_MINUS_DST_COLOR;
      case BlendFactor.destinationAlpha:
        return web.WebGL2RenderingContext.DST_ALPHA;
      case BlendFactor.oneMinusDestinationAlpha:
        return web.WebGL2RenderingContext.ONE_MINUS_DST_ALPHA;
      case BlendFactor.sourceAlphaSaturated:
        return web.WebGL2RenderingContext.SRC_ALPHA_SATURATE;
      case BlendFactor.blendColor:
        return web.WebGL2RenderingContext.CONSTANT_COLOR;
      case BlendFactor.oneMinusBlendColor:
        return web.WebGL2RenderingContext.ONE_MINUS_CONSTANT_COLOR;
      case BlendFactor.blendAlpha:
        return web.WebGL2RenderingContext.CONSTANT_ALPHA;
      case BlendFactor.oneMinusBlendAlpha:
        return web.WebGL2RenderingContext.ONE_MINUS_CONSTANT_ALPHA;
    }
  }

  void setViewport(Viewport viewport) {
    final gl = _gpuContext._gl;
    gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
    gl.depthRange(viewport.depthRange.zNear, viewport.depthRange.zFar);
  }

  void _configureInlineVertexBuffer(int vertexCount) {
    final pipeline = _boundPipeline;
    final bufferView = _inlineVertexBufferView;
    if (pipeline == null || bufferView == null) return;
    final gl = _gpuContext._gl;
    final location = gl.getAttribLocation(pipeline._program, 'position');
    if (location < 0) {
      throw Exception(
        'RenderPipeline has no `position` attribute and no reflected '
        'vertex inputs. Use a ShaderLibrary built from a bundle for '
        'pipelines with non-trivial vertex layouts.',
      );
    }
    final perVertex = bufferView.lengthInBytes ~/ vertexCount;
    final components = perVertex ~/ 4;
    bufferView.buffer._bindForTarget(web.WebGL2RenderingContext.ARRAY_BUFFER);
    gl.enableVertexAttribArray(location);
    gl.vertexAttribPointer(
      location,
      components,
      web.WebGL2RenderingContext.FLOAT,
      false,
      perVertex,
      bufferView.offsetInBytes,
    );
  }

  void draw(int vertexCount, {int instanceCount = 1}) {
    final gl = _gpuContext._gl;
    if (_boundPipeline == null) {
      throw StateError('draw called before bindPipeline');
    }
    if (vertexCount == 0 || instanceCount == 0) return;
    _applyVertexState(needsIndex: false);
    _configureInlineVertexBuffer(vertexCount);
    if (instanceCount != 1) {
      gl.drawArraysInstanced(
        _glPrimitiveType(_primitiveType),
        0,
        vertexCount,
        instanceCount,
      );
    } else {
      gl.drawArrays(_glPrimitiveType(_primitiveType), 0, vertexCount);
    }
  }

  void drawIndexed(int indexCount, {int instanceCount = 1}) {
    final gl = _gpuContext._gl;
    if (_boundPipeline == null) {
      throw StateError('drawIndexed called before bindPipeline');
    }
    if (_inlineVertexBufferView != null) {
      throw StateError('Indexed inline pipelines require reflected attributes');
    }
    if (indexCount == 0 || instanceCount == 0) return;
    final indexView = _indexBufferView;
    if (indexView == null) {
      throw StateError('drawIndexed called before bindIndexBuffer');
    }
    _applyVertexState(needsIndex: true);
    final glIndexType = _indexType == IndexType.int16
        ? web.WebGL2RenderingContext.UNSIGNED_SHORT
        : web.WebGL2RenderingContext.UNSIGNED_INT;
    if (instanceCount != 1) {
      gl.drawElementsInstanced(
        _glPrimitiveType(_primitiveType),
        indexCount,
        glIndexType,
        indexView.offsetInBytes,
        instanceCount,
      );
    } else {
      gl.drawElements(
        _glPrimitiveType(_primitiveType),
        indexCount,
        glIndexType,
        indexView.offsetInBytes,
      );
    }
  }

  /// Called by CommandBuffer when the pass ends (a new pass begins, or the
  /// buffer is submitted). Resolves MSAA color attachments into their
  /// resolve textures.
  void _finish() {
    if (_finished) return;
    _finished = true;
    final gl = _gpuContext._gl;
    for (final att in _target.colorAttachments) {
      final resolve = att.resolveTexture;
      final needsResolve =
          att.texture.sampleCount > 1 &&
          resolve != null &&
          (att.storeAction == StoreAction.multisampleResolve ||
              att.storeAction == StoreAction.storeAndMultisampleResolve);
      if (needsResolve) {
        _resolveColor(gl, att.texture, resolve);
      }
    }
  }

  void _resolveColor(
    web.WebGL2RenderingContext gl,
    Texture msaa,
    Texture resolve,
  ) {
    final resolveFbo = gl.createFramebuffer();
    gl.bindFramebuffer(web.WebGL2RenderingContext.READ_FRAMEBUFFER, _fbo);
    gl.bindFramebuffer(web.WebGL2RenderingContext.DRAW_FRAMEBUFFER, resolveFbo);
    gl.framebufferTexture2D(
      web.WebGL2RenderingContext.DRAW_FRAMEBUFFER,
      web.WebGL2RenderingContext.COLOR_ATTACHMENT0,
      web.WebGL2RenderingContext.TEXTURE_2D,
      resolve.glTexture,
      0,
    );
    gl.blitFramebuffer(
      0,
      0,
      msaa.width,
      msaa.height,
      0,
      0,
      resolve.width,
      resolve.height,
      web.WebGL2RenderingContext.COLOR_BUFFER_BIT,
      web.WebGL2RenderingContext.NEAREST,
    );
    gl.bindFramebuffer(web.WebGL2RenderingContext.FRAMEBUFFER, null);
    gl.deleteFramebuffer(resolveFbo);
  }

  static int _glPrimitiveType(PrimitiveType p) {
    switch (p) {
      case PrimitiveType.triangle:
        return web.WebGL2RenderingContext.TRIANGLES;
      case PrimitiveType.triangleStrip:
        return web.WebGL2RenderingContext.TRIANGLE_STRIP;
      case PrimitiveType.line:
        return web.WebGL2RenderingContext.LINES;
      case PrimitiveType.lineStrip:
        return web.WebGL2RenderingContext.LINE_STRIP;
      case PrimitiveType.point:
        return web.WebGL2RenderingContext.POINTS;
    }
  }
}
