import type { Cubism2BaseContextConstructor, Cubism2BaseContextInstance } from './baseContext'
import type {
  Cubism2BaseDataConstructor,
  Cubism2BaseDataInstance,
  Cubism2BaseDataReader,
} from './baseData'
import type {
  Cubism2InterpolationConstructor,
  Cubism2InterpolationModelContextLike,
  Cubism2InterpolationParamBindingSetLike,
} from './interpolation'
import type {
  Cubism2ParamBindingSetConstructor,
  Cubism2ParamBindingSetInstance,
} from './paramBinding'

type MutableNumberArray = ArrayLike<number> & {
  [index: number]: number
}

type GridPointTableList = ArrayLike<ArrayLike<number>>

interface GridPoint2D {
  x: number
  y: number
}

interface GridExtrapolationBasis {
  columnVectorX: number
  columnVectorY: number
  originX: number
  originY: number
  rowVectorX: number
  rowVectorY: number
}

/**
 * Computes the interleaved point offset for a grid column and row.
 * @param column Grid column index in the authored point lattice.
 * @param row Grid row index in the authored point lattice.
 * @param strideColumnCount Number of point columns, including the terminal edge column.
 * @returns Interleaved array offset for the point's x coordinate.
 */
function getGridPointOffset(column: number, row: number, strideColumnCount: number): number {
  return 2 * (column + row * strideColumnCount)
}

/**
 * Reads one x/y control point from the interleaved Cubism2 grid buffer.
 * @param gridPoints Interleaved control points generated by the grid interpolation step.
 * @param column Grid column to read.
 * @param row Grid row to read.
 * @param strideColumnCount Number of point columns, including the terminal edge column.
 * @returns The requested grid control point.
 */
function readGridPoint(
  gridPoints: ArrayLike<number>,
  column: number,
  row: number,
  strideColumnCount: number,
): GridPoint2D {
  const pointOffset = getGridPointOffset(column, row, strideColumnCount)
  return {
    x: gridPoints[pointOffset]!,
    y: gridPoints[pointOffset + 1]!,
  }
}

/**
 * Builds the SDK2 basis vectors used when points leave the authored 0..1 grid area.
 * @param gridPoints Interleaved control point lattice.
 * @param gridColumnCount Number of horizontal grid cells in the authored lattice.
 * @param gridRowCount Number of vertical grid cells in the authored lattice.
 * @returns Origin and basis vectors for near-field and far-field extrapolation.
 */
function createGridExtrapolationBasis(
  gridPoints: ArrayLike<number>,
  gridColumnCount: number,
  gridRowCount: number,
): GridExtrapolationBasis {
  const strideColumnCount = gridColumnCount + 1
  const topLeft = readGridPoint(gridPoints, 0, 0, strideColumnCount)
  const topRight = readGridPoint(gridPoints, gridColumnCount, 0, strideColumnCount)
  const bottomLeft = readGridPoint(gridPoints, 0, gridRowCount, strideColumnCount)
  const bottomRight = readGridPoint(gridPoints, gridColumnCount, gridRowCount, strideColumnCount)
  const centerX = 0.25 * (topLeft.x + topRight.x + bottomLeft.x + bottomRight.x)
  const centerY = 0.25 * (topLeft.y + topRight.y + bottomLeft.y + bottomRight.y)
  const diagonalVectorX = bottomRight.x - topLeft.x
  const diagonalVectorY = bottomRight.y - topLeft.y
  const antiDiagonalVectorX = topRight.x - bottomLeft.x
  const antiDiagonalVectorY = topRight.y - bottomLeft.y
  const columnVectorX = (diagonalVectorX + antiDiagonalVectorX) * 0.5
  const columnVectorY = (diagonalVectorY + antiDiagonalVectorY) * 0.5
  const rowVectorX = (diagonalVectorX - antiDiagonalVectorX) * 0.5
  const rowVectorY = (diagonalVectorY - antiDiagonalVectorY) * 0.5

  return {
    columnVectorX,
    columnVectorY,
    originX: centerX - 0.5 * (columnVectorX + rowVectorX),
    originY: centerY - 0.5 * (columnVectorY + rowVectorY),
    rowVectorX,
    rowVectorY,
  }
}

/**
 * Writes the lower half of a split grid quad using barycentric-like SDK2 weights.
 * @param outputPoints Interleaved output buffer receiving the transformed point.
 * @param pointOffset Offset of the x coordinate to write.
 * @param originX First triangle vertex x.
 * @param originY First triangle vertex y.
 * @param horizontalX Horizontal triangle vertex x.
 * @param horizontalY Horizontal triangle vertex y.
 * @param verticalX Vertical triangle vertex x.
 * @param verticalY Vertical triangle vertex y.
 * @param localX Local x weight inside the split quad.
 * @param localY Local y weight inside the split quad.
 * @returns Nothing; `outputPoints` is mutated in place.
 */
function writeLowerTrianglePoint(
  outputPoints: MutableNumberArray,
  pointOffset: number,
  originX: number,
  originY: number,
  horizontalX: number,
  horizontalY: number,
  verticalX: number,
  verticalY: number,
  localX: number,
  localY: number,
): void {
  outputPoints[pointOffset] =
    originX + (horizontalX - originX) * localX + (verticalX - originX) * localY
  outputPoints[pointOffset + 1] =
    originY + (horizontalY - originY) * localX + (verticalY - originY) * localY
}

/**
 * Writes the upper half of a split grid quad using the legacy SDK2 complement weights.
 * @param outputPoints Interleaved output buffer receiving the transformed point.
 * @param pointOffset Offset of the x coordinate to write.
 * @param diagonalX Opposite triangle vertex x.
 * @param diagonalY Opposite triangle vertex y.
 * @param verticalX Vertical triangle vertex x.
 * @param verticalY Vertical triangle vertex y.
 * @param horizontalX Horizontal triangle vertex x.
 * @param horizontalY Horizontal triangle vertex y.
 * @param localX Local x weight inside the split quad.
 * @param localY Local y weight inside the split quad.
 * @returns Nothing; `outputPoints` is mutated in place.
 */
function writeUpperTrianglePoint(
  outputPoints: MutableNumberArray,
  pointOffset: number,
  diagonalX: number,
  diagonalY: number,
  verticalX: number,
  verticalY: number,
  horizontalX: number,
  horizontalY: number,
  localX: number,
  localY: number,
): void {
  outputPoints[pointOffset] =
    diagonalX + (verticalX - diagonalX) * (1 - localX) + (horizontalX - diagonalX) * (1 - localY)
  outputPoints[pointOffset + 1] =
    diagonalY + (verticalY - diagonalY) * (1 - localX) + (horizontalY - diagonalY) * (1 - localY)
}

/**
 * Dispatches SDK2 split-quad interpolation to the lower or upper triangle branch.
 * @param outputPoints Interleaved output buffer receiving the transformed point.
 * @param pointOffset Offset of the x coordinate to write.
 * @param originX Lower-origin vertex x.
 * @param originY Lower-origin vertex y.
 * @param horizontalX Horizontal-neighbor vertex x.
 * @param horizontalY Horizontal-neighbor vertex y.
 * @param verticalX Vertical-neighbor vertex x.
 * @param verticalY Vertical-neighbor vertex y.
 * @param diagonalX Opposite diagonal vertex x.
 * @param diagonalY Opposite diagonal vertex y.
 * @param localX Local x weight inside the split quad.
 * @param localY Local y weight inside the split quad.
 * @param useInclusiveLowerTriangle Whether `localX + localY === 1` belongs to the lower triangle.
 * @returns Nothing; `outputPoints` is mutated in place.
 */
function writeSplitQuadPoint(
  outputPoints: MutableNumberArray,
  pointOffset: number,
  originX: number,
  originY: number,
  horizontalX: number,
  horizontalY: number,
  verticalX: number,
  verticalY: number,
  diagonalX: number,
  diagonalY: number,
  localX: number,
  localY: number,
  useInclusiveLowerTriangle: boolean,
): void {
  const lowerTriangle = useInclusiveLowerTriangle ? localX + localY <= 1 : localX + localY < 1
  if (lowerTriangle) {
    writeLowerTrianglePoint(
      outputPoints,
      pointOffset,
      originX,
      originY,
      horizontalX,
      horizontalY,
      verticalX,
      verticalY,
      localX,
      localY,
    )
  } else {
    writeUpperTrianglePoint(
      outputPoints,
      pointOffset,
      diagonalX,
      diagonalY,
      verticalX,
      verticalY,
      horizontalX,
      horizontalY,
      localX,
      localY,
    )
  }
}

export interface Cubism2GridBaseDataReader extends Cubism2BaseDataReader {
  readInt32: () => number
}

export interface Cubism2GridModelContextLike extends Cubism2InterpolationModelContextLike {
  getBaseContext: (baseDataIndex: number) => Cubism2BaseContextInstance | null
  getBaseData: (baseDataIndex: number) => Cubism2GridTargetBaseDataLike | null
  getBaseDataIndex: (targetBaseDataId: unknown) => number
}

export interface Cubism2GridTargetBaseDataLike {
  transformPoints: (
    modelContext: unknown,
    targetBaseContext: unknown,
    sourcePoints: MutableNumberArray,
    outputPoints: MutableNumberArray,
    pointCount: number,
    pointOffset: number,
    pointStride: number,
  ) => void
}

export interface Cubism2GridContextInstance extends Cubism2BaseContextInstance {
  localPoints: Float32Array | null
  targetBaseDataIndex: number
  targetSpacePoints: Float32Array | null
}

export interface Cubism2GridBaseDataInstance extends Cubism2BaseDataInstance {
  applyRuntimeContext: (modelContext: unknown, gridContext: unknown) => void
  createRuntimeContext: (modelContext: unknown) => Cubism2GridContextInstance
  getGridPointCount: () => number
  gridColumnCount: number
  gridPointValues: GridPointTableList | null
  gridRowCount: number
  initializeParamBindingSet: () => void
  paramBindingSet: Cubism2ParamBindingSetInstance | null
  readGridBaseData: (reader: Cubism2BaseDataReader) => void
  transformPoints: (
    modelContext: unknown,
    sourceContext: unknown,
    sourcePoints: unknown,
    outputPoints: unknown,
    pointCount: number,
    pointOffset: number,
    pointStride: number,
  ) => void
  transformPointsSdk1: (
    modelContext: unknown,
    sourceContext: unknown,
    sourcePoints: unknown,
    outputPoints: unknown,
    pointCount: number,
    pointOffset: number,
    pointStride: number,
  ) => void
  updateRuntimeContext: (modelContext: unknown, gridContext: unknown) => void
}

export interface Cubism2GridBaseDataPayload {
  gridColumnCount: number
  gridPointValues: GridPointTableList
  gridRowCount: number
  paramBindingSet: Cubism2ParamBindingSetInstance
}

export interface Cubism2GridBaseDataConstructor {
  new (): Cubism2GridBaseDataInstance
  paramDirtyFlagScratch: boolean[]
  prototype: Cubism2GridBaseDataInstance
  transformPointsSdk2: (
    sourcePoints: ArrayLike<number>,
    outputPoints: MutableNumberArray,
    pointCount: number,
    pointOffset: number,
    pointStride: number,
    gridPoints: ArrayLike<number>,
    gridColumnCount: number,
    gridRowCount: number,
  ) => void
}

export interface Cubism2GridContextConstructor {
  new (baseData: Cubism2GridBaseDataInstance): Cubism2GridContextInstance
  prototype: Cubism2GridContextInstance
}

export interface Cubism2GridBaseDataConstructors {
  Cubism2GridBaseData: Cubism2GridBaseDataConstructor
  Cubism2GridContext: Cubism2GridContextConstructor
}

export interface CreateCubism2GridBaseDataOptions {
  Cubism2BaseContext: Cubism2BaseContextConstructor
  Cubism2BaseData: Cubism2BaseDataConstructor
  Cubism2Interpolation: Pick<Cubism2InterpolationConstructor, 'interpolatePoints'>
  Cubism2ParamBindingSet: Cubism2ParamBindingSetConstructor
  Live2D: {
    isVerboseLoggingEnabled: () => boolean
    shouldClampSdk1GridPointsToUnitRange: boolean
  }
  System: {
    err: {
      printf: (message: string, ...args: unknown[]) => void
    }
  }
  UtDebug: {
    logWithLegacyPrefix: (message: string, ...args: unknown[]) => void
  }
  isBootstrapping: () => boolean
}

/**
 * Reads the type-65 grid payload after the shared base-data ID header.
 * @param reader Cubism2 binary reader positioned after the shared base-data header.
 * @returns Row/column dimensions plus the authored param binding and grid point tables.
 */
function readCubism2GridBaseDataPayload(
  reader: Cubism2GridBaseDataReader,
): Cubism2GridBaseDataPayload {
  const gridRowCount = reader.readInt32()
  const gridColumnCount = reader.readInt32()
  const paramBindingSet = reader.readObject() as Cubism2ParamBindingSetInstance
  const gridPointValues = reader.readObject() as GridPointTableList

  return {
    gridColumnCount,
    gridPointValues,
    gridRowCount,
    paramBindingSet,
  }
}

/**
 * Applies a decoded type-65 grid payload to a grid base-data object.
 * @param gridBaseData Target type-65 record that owns grid dimensions and interpolation tables.
 * @param payload Payload returned by `readCubism2GridBaseDataPayload`.
 * @returns Nothing; mutates the grid base data in the same field order as min.js.
 */
function applyCubism2GridBaseDataPayload(
  gridBaseData: Cubism2GridBaseDataInstance,
  payload: Cubism2GridBaseDataPayload,
): void {
  gridBaseData.gridRowCount = payload.gridRowCount
  gridBaseData.gridColumnCount = payload.gridColumnCount
  gridBaseData.paramBindingSet = payload.paramBindingSet
  gridBaseData.gridPointValues = payload.gridPointValues
}

/**
 * Creates Cubism2 type-65 grid base-data constructors bound to compatibility dependencies.
 * @param options Runtime constructors, flags, and diagnostics required by legacy grid logic.
 * @returns Grid base-data and grid-context constructors.
 */
export function createCubism2GridBaseData(
  options: CreateCubism2GridBaseDataOptions,
): Cubism2GridBaseDataConstructors {
  const {
    Cubism2BaseContext,
    Cubism2BaseData,
    Cubism2Interpolation,
    Cubism2ParamBindingSet,
    Live2D,
    System,
    UtDebug,
    isBootstrapping,
  } = options

  /**
   * Stores one type-65 Cubism2 grid base-data record used to deform points from a param grid.
   * @returns Nothing; reader hooks populate grid dimensions, bindings, and authored grid values.
   */
  function Cubism2GridBaseData(this: Cubism2GridBaseDataInstance): void {
    if (isBootstrapping()) {
      return
    }
    Cubism2BaseData.prototype.constructor.call(this)
    this.gridColumnCount = 0
    this.gridRowCount = 0
    this.paramBindingSet = null
    this.gridPointValues = null
  }

  const GridBaseData = Cubism2GridBaseData as unknown as Cubism2GridBaseDataConstructor
  GridBaseData.prototype = new Cubism2BaseData() as unknown as Cubism2GridBaseDataInstance
  GridBaseData.paramDirtyFlagScratch = []

  /**
   * Allocates the parameter binding set before type-65 grid records append their axes.
   * @returns Nothing; the binding set is stored for later parameter interpolation.
   */
  GridBaseData.prototype.initializeParamBindingSet = function (): void {
    this.paramBindingSet = new Cubism2ParamBindingSet()
    this.paramBindingSet.initBindingList()
  }

  /**
   * Reads a type-65 grid base-data payload from the Cubism2 binary stream.
   * @param reader Cubism2 binary reader positioned at the grid payload.
   * @returns Nothing; shared base header, grid dimensions, bindings, points, and opacity are stored.
   */
  GridBaseData.prototype.readGridBaseData = function (reader: Cubism2BaseDataReader): void {
    const gridReader = reader as Cubism2GridBaseDataReader
    Cubism2BaseData.prototype.readBaseData.call(this, reader)
    applyCubism2GridBaseDataPayload(this, readCubism2GridBaseDataPayload(gridReader))
    Cubism2BaseData.prototype.readV2Opacity.call(this, reader)
  }

  /**
   * Creates the runtime point buffers that hold interpolated grid points and optional target-space points.
   * @param modelContext Model context that owns the base context list; unused by this constructor path.
   * @returns Runtime grid context with local and optional target-space point buffers sized to the grid.
   */
  GridBaseData.prototype.createRuntimeContext = function (
    modelContext: unknown,
  ): Cubism2GridContextInstance {
    void modelContext
    const gridContext = new GridContext(this)
    const pointCount = this.getGridPointCount()
    if (gridContext.localPoints != null) {
      gridContext.localPoints = null
    }
    gridContext.localPoints = new Float32Array(pointCount * 2)
    if (gridContext.targetSpacePoints != null) {
      gridContext.targetSpacePoints = null
    }
    if (this.hasTargetBaseData()) {
      gridContext.targetSpacePoints = new Float32Array(pointCount * 2)
    } else {
      gridContext.targetSpacePoints = null
    }
    return gridContext
  }

  /**
   * Interpolates authored grid points for the current model parameters and updates opacity.
   * @param modelContext Runtime model context that supplies current parameter values and scratch buffers.
   * @param gridContext Runtime grid context whose local point buffer receives interpolated points.
   * @returns Nothing; dirty flags, local points, and interpolated opacity are mutated in place.
   */
  GridBaseData.prototype.updateRuntimeContext = function (
    modelContext: unknown,
    gridContext: unknown,
  ): void {
    const runtimeModelContext = modelContext as Cubism2GridModelContextLike
    const runtimeGridContext = gridContext as Cubism2GridContextInstance
    const paramBindingSet = this
      .paramBindingSet as unknown as Cubism2InterpolationParamBindingSetLike & {
      hasChangedParams: (modelContext: unknown) => boolean
    }
    if (!paramBindingSet.hasChangedParams(runtimeModelContext)) {
      return
    }
    const pointCount = this.getGridPointCount()
    const dirtyFlagRef = GridBaseData.paramDirtyFlagScratch
    dirtyFlagRef[0] = false
    Cubism2Interpolation.interpolatePoints(
      runtimeModelContext,
      paramBindingSet,
      dirtyFlagRef,
      pointCount,
      this.gridPointValues,
      runtimeGridContext.localPoints,
      0,
      2,
    )
    runtimeGridContext.setTransformFlag(dirtyFlagRef[0]!)
    this.interpolateOpacity(
      runtimeModelContext,
      this.paramBindingSet,
      runtimeGridContext,
      dirtyFlagRef,
    )
  }

  /**
   * Propagates the grid through its target base data when this grid depends on another base.
   * @param modelContext Runtime model context used to resolve target base data and context.
   * @param gridContext Runtime grid context whose total opacity/scale and target points are updated.
   * @returns Nothing; context activity and target-space buffers are updated in place.
   */
  GridBaseData.prototype.applyRuntimeContext = function (
    modelContext: unknown,
    gridContext: unknown,
  ): void {
    const runtimeModelContext = modelContext as Cubism2GridModelContextLike
    const runtimeGridContext = gridContext as Cubism2GridContextInstance
    runtimeGridContext.setActive(true)
    if (!this.hasTargetBaseData()) {
      runtimeGridContext.setTotalOpacity(runtimeGridContext.getInterpolatedOpacity())
    } else {
      const targetBaseDataId = this.getTargetBaseDataID()
      if (runtimeGridContext.targetBaseDataIndex == Cubism2BaseData.UNRESOLVED_BASE_DATA_INDEX) {
        runtimeGridContext.targetBaseDataIndex =
          runtimeModelContext.getBaseDataIndex(targetBaseDataId)
      }
      if (runtimeGridContext.targetBaseDataIndex < 0) {
        if (Live2D.isVerboseLoggingEnabled()) {
          UtDebug.logWithLegacyPrefix('Target base data was not found: %s', targetBaseDataId)
        }
        runtimeGridContext.setActive(false)
      } else {
        const targetBaseData = runtimeModelContext.getBaseData(
          runtimeGridContext.targetBaseDataIndex,
        )
        const targetBaseContext = runtimeModelContext.getBaseContext(
          runtimeGridContext.targetBaseDataIndex,
        )
        if (
          targetBaseData != null &&
          targetBaseContext != null &&
          targetBaseContext.isRenderable()
        ) {
          const totalScale = targetBaseContext.getTotalScale()
          runtimeGridContext.setTotalScaleNotForClient(totalScale)
          const totalOpacity = targetBaseContext.getTotalOpacity()
          runtimeGridContext.setTotalOpacity(
            totalOpacity * runtimeGridContext.getInterpolatedOpacity(),
          )
          targetBaseData.transformPoints(
            runtimeModelContext,
            targetBaseContext,
            runtimeGridContext.localPoints!,
            runtimeGridContext.targetSpacePoints!,
            this.getGridPointCount(),
            0,
            2,
          )
          runtimeGridContext.setActive(true)
        } else {
          runtimeGridContext.setActive(false)
        }
      }
    }
  }

  /**
   * Applies this grid's point mapping to an arbitrary point buffer.
   * @param modelContext Runtime model context; retained for the legacy SDK1 branch signature.
   * @param sourceContext Base context whose local or target-space grid points drive the mapping.
   * @param sourcePoints Input points to transform.
   * @param outputPoints Output points receiving transformed coordinates.
   * @param pointCount Number of interleaved points to transform.
   * @param pointOffset First element offset in the interleaved point buffers.
   * @param pointStride Stride between points in the interleaved point buffers.
   * @returns Nothing; output points are mutated in place.
   */
  GridBaseData.prototype.transformPoints = function (
    modelContext: unknown,
    sourceContext: unknown,
    sourcePoints: unknown,
    outputPoints: unknown,
    pointCount: number,
    pointOffset: number,
    pointStride: number,
  ): void {
    const runtimeSourceContext = sourceContext as Cubism2GridContextInstance
    if (true) {
      const gridPoints =
        runtimeSourceContext.targetSpacePoints != null
          ? runtimeSourceContext.targetSpacePoints
          : runtimeSourceContext.localPoints
      GridBaseData.transformPointsSdk2(
        sourcePoints as ArrayLike<number>,
        outputPoints as MutableNumberArray,
        pointCount,
        pointOffset,
        pointStride,
        gridPoints!,
        this.gridColumnCount,
        this.gridRowCount,
      )
    } else {
      this.transformPointsSdk1(
        modelContext,
        sourceContext,
        sourcePoints,
        outputPoints,
        pointCount,
        pointOffset,
        pointStride,
      )
    }
  }

  /**
   * Maps normalized source points through the SDK2 grid interpolation/extrapolation algorithm.
   * @param sourcePoints Interleaved source point coordinates to map.
   * @param outputPoints Interleaved output point coordinates written by this method.
   * @param pointCount Number of points to transform.
   * @param pointOffset First element offset in the interleaved buffers.
   * @param pointStride Stride between points in the interleaved buffers.
   * @param gridPoints Interleaved grid control points produced by parameter interpolation.
   * @param gridColumnCount Number of horizontal grid cells.
   * @param gridRowCount Number of vertical grid cells.
   * @returns Nothing; the output buffer is mutated in place.
   */
  GridBaseData.transformPointsSdk2 = function (
    sourcePoints: ArrayLike<number>,
    outputPoints: MutableNumberArray,
    pointCount: number,
    pointOffset: number,
    pointStride: number,
    gridPoints: ArrayLike<number>,
    gridColumnCount: number,
    gridRowCount: number,
  ): void {
    const exclusivePointOffset = pointCount * pointStride
    const strideColumnCount = gridColumnCount + 1
    let extrapolationBasis: GridExtrapolationBasis | null = null

    for (
      let pointCursor = pointOffset;
      pointCursor < exclusivePointOffset;
      pointCursor += pointStride
    ) {
      const normalizedX = sourcePoints[pointCursor]!
      const normalizedY = sourcePoints[pointCursor + 1]!
      const gridX = normalizedX * gridColumnCount
      const gridY = normalizedY * gridRowCount

      if (gridX < 0 || gridY < 0 || gridColumnCount <= gridX || gridRowCount <= gridY) {
        if (extrapolationBasis == null) {
          extrapolationBasis = createGridExtrapolationBasis(
            gridPoints,
            gridColumnCount,
            gridRowCount,
          )
        }

        const { columnVectorX, columnVectorY, originX, originY, rowVectorX, rowVectorY } =
          extrapolationBasis

        if (-2 < normalizedX && normalizedX < 3 && -2 < normalizedY && normalizedY < 3) {
          if (normalizedX <= 0) {
            if (normalizedY <= 0) {
              const diagonalPoint = readGridPoint(gridPoints, 0, 0, strideColumnCount)
              const verticalX = originX - 2 * columnVectorX
              const verticalY = originY - 2 * columnVectorY
              const horizontalX = originX - 2 * rowVectorX
              const horizontalY = originY - 2 * rowVectorY
              const originCornerX = originX - 2 * columnVectorX - 2 * rowVectorX
              const originCornerY = originY - 2 * columnVectorY - 2 * rowVectorY
              const localX = 0.5 * (normalizedX - -2)
              const localY = 0.5 * (normalizedY - -2)
              writeSplitQuadPoint(
                outputPoints,
                pointCursor,
                originCornerX,
                originCornerY,
                horizontalX,
                horizontalY,
                verticalX,
                verticalY,
                diagonalPoint.x,
                diagonalPoint.y,
                localX,
                localY,
                true,
              )
            } else if (normalizedY >= 1) {
              const horizontalPoint = readGridPoint(gridPoints, 0, gridRowCount, strideColumnCount)
              const originCornerX = originX - 2 * columnVectorX + rowVectorX
              const originCornerY = originY - 2 * columnVectorY + rowVectorY
              const diagonalX = originX + 3 * rowVectorX
              const diagonalY = originY + 3 * rowVectorY
              const verticalX = originX - 2 * columnVectorX + 3 * rowVectorX
              const verticalY = originY - 2 * columnVectorY + 3 * rowVectorY
              const localX = 0.5 * (normalizedX - -2)
              const localY = 0.5 * (normalizedY - 1)
              writeSplitQuadPoint(
                outputPoints,
                pointCursor,
                originCornerX,
                originCornerY,
                horizontalPoint.x,
                horizontalPoint.y,
                verticalX,
                verticalY,
                diagonalX,
                diagonalY,
                localX,
                localY,
                true,
              )
            } else {
              let rowIndex = gridY | 0
              if (rowIndex == gridRowCount) {
                rowIndex = gridRowCount - 1
              }
              const localX = 0.5 * (normalizedX - -2)
              const localY = gridY - rowIndex
              const rowRatio = rowIndex / gridRowCount
              const nextRowRatio = (rowIndex + 1) / gridRowCount
              const horizontalPoint = readGridPoint(gridPoints, 0, rowIndex, strideColumnCount)
              const diagonalPoint = readGridPoint(gridPoints, 0, rowIndex + 1, strideColumnCount)
              const originCornerX = originX - 2 * columnVectorX + rowRatio * rowVectorX
              const originCornerY = originY - 2 * columnVectorY + rowRatio * rowVectorY
              const verticalX = originX - 2 * columnVectorX + nextRowRatio * rowVectorX
              const verticalY = originY - 2 * columnVectorY + nextRowRatio * rowVectorY
              writeSplitQuadPoint(
                outputPoints,
                pointCursor,
                originCornerX,
                originCornerY,
                horizontalPoint.x,
                horizontalPoint.y,
                verticalX,
                verticalY,
                diagonalPoint.x,
                diagonalPoint.y,
                localX,
                localY,
                true,
              )
            }
          } else if (1 <= normalizedX) {
            if (normalizedY <= 0) {
              const verticalPoint = readGridPoint(gridPoints, gridColumnCount, 0, strideColumnCount)
              const diagonalX = originX + 3 * columnVectorX
              const diagonalY = originY + 3 * columnVectorY
              const originCornerX = originX + columnVectorX - 2 * rowVectorX
              const originCornerY = originY + columnVectorY - 2 * rowVectorY
              const horizontalX = originX + 3 * columnVectorX - 2 * rowVectorX
              const horizontalY = originY + 3 * columnVectorY - 2 * rowVectorY
              const localX = 0.5 * (normalizedX - 1)
              const localY = 0.5 * (normalizedY - -2)
              writeSplitQuadPoint(
                outputPoints,
                pointCursor,
                originCornerX,
                originCornerY,
                horizontalX,
                horizontalY,
                verticalPoint.x,
                verticalPoint.y,
                diagonalX,
                diagonalY,
                localX,
                localY,
                true,
              )
            } else if (normalizedY >= 1) {
              const originPoint = readGridPoint(
                gridPoints,
                gridColumnCount,
                gridRowCount,
                strideColumnCount,
              )
              const horizontalX = originX + 3 * columnVectorX + rowVectorX
              const horizontalY = originY + 3 * columnVectorY + rowVectorY
              const verticalX = originX + columnVectorX + 3 * rowVectorX
              const verticalY = originY + columnVectorY + 3 * rowVectorY
              const diagonalX = originX + 3 * columnVectorX + 3 * rowVectorX
              const diagonalY = originY + 3 * columnVectorY + 3 * rowVectorY
              const localX = 0.5 * (normalizedX - 1)
              const localY = 0.5 * (normalizedY - 1)
              writeSplitQuadPoint(
                outputPoints,
                pointCursor,
                originPoint.x,
                originPoint.y,
                horizontalX,
                horizontalY,
                verticalX,
                verticalY,
                diagonalX,
                diagonalY,
                localX,
                localY,
                true,
              )
            } else {
              let rowIndex = gridY | 0
              if (rowIndex == gridRowCount) {
                rowIndex = gridRowCount - 1
              }
              const localX = 0.5 * (normalizedX - 1)
              const localY = gridY - rowIndex
              const rowRatio = rowIndex / gridRowCount
              const nextRowRatio = (rowIndex + 1) / gridRowCount
              const originPoint = readGridPoint(
                gridPoints,
                gridColumnCount,
                rowIndex,
                strideColumnCount,
              )
              const verticalPoint = readGridPoint(
                gridPoints,
                gridColumnCount,
                rowIndex + 1,
                strideColumnCount,
              )
              const horizontalX = originX + 3 * columnVectorX + rowRatio * rowVectorX
              const horizontalY = originY + 3 * columnVectorY + rowRatio * rowVectorY
              const diagonalX = originX + 3 * columnVectorX + nextRowRatio * rowVectorX
              const diagonalY = originY + 3 * columnVectorY + nextRowRatio * rowVectorY
              writeSplitQuadPoint(
                outputPoints,
                pointCursor,
                originPoint.x,
                originPoint.y,
                horizontalX,
                horizontalY,
                verticalPoint.x,
                verticalPoint.y,
                diagonalX,
                diagonalY,
                localX,
                localY,
                true,
              )
            }
          } else if (normalizedY <= 0) {
            let columnIndex = gridX | 0
            if (columnIndex == gridColumnCount) {
              columnIndex = gridColumnCount - 1
            }
            const localX = gridX - columnIndex
            const localY = 0.5 * (normalizedY - -2)
            const columnRatio = columnIndex / gridColumnCount
            const nextColumnRatio = (columnIndex + 1) / gridColumnCount
            const verticalPoint = readGridPoint(gridPoints, columnIndex, 0, strideColumnCount)
            const diagonalPoint = readGridPoint(gridPoints, columnIndex + 1, 0, strideColumnCount)
            const originCornerX = originX + columnRatio * columnVectorX - 2 * rowVectorX
            const originCornerY = originY + columnRatio * columnVectorY - 2 * rowVectorY
            const horizontalX = originX + nextColumnRatio * columnVectorX - 2 * rowVectorX
            const horizontalY = originY + nextColumnRatio * columnVectorY - 2 * rowVectorY
            writeSplitQuadPoint(
              outputPoints,
              pointCursor,
              originCornerX,
              originCornerY,
              horizontalX,
              horizontalY,
              verticalPoint.x,
              verticalPoint.y,
              diagonalPoint.x,
              diagonalPoint.y,
              localX,
              localY,
              true,
            )
          } else if (normalizedY >= 1) {
            let columnIndex = gridX | 0
            if (columnIndex == gridColumnCount) {
              columnIndex = gridColumnCount - 1
            }
            const localX = gridX - columnIndex
            const localY = 0.5 * (normalizedY - 1)
            const columnRatio = columnIndex / gridColumnCount
            const nextColumnRatio = (columnIndex + 1) / gridColumnCount
            const originPoint = readGridPoint(
              gridPoints,
              columnIndex,
              gridRowCount,
              strideColumnCount,
            )
            const horizontalPoint = readGridPoint(
              gridPoints,
              columnIndex + 1,
              gridRowCount,
              strideColumnCount,
            )
            const verticalX = originX + columnRatio * columnVectorX + 3 * rowVectorX
            const verticalY = originY + columnRatio * columnVectorY + 3 * rowVectorY
            const diagonalX = originX + nextColumnRatio * columnVectorX + 3 * rowVectorX
            const diagonalY = originY + nextColumnRatio * columnVectorY + 3 * rowVectorY
            writeSplitQuadPoint(
              outputPoints,
              pointCursor,
              originPoint.x,
              originPoint.y,
              horizontalPoint.x,
              horizontalPoint.y,
              verticalX,
              verticalY,
              diagonalX,
              diagonalY,
              localX,
              localY,
              true,
            )
          } else {
            System.err.printf(
              'Unexpected grid extrapolation coordinates: %.4f, %.4f\n',
              normalizedX,
              normalizedY,
            )
          }
        } else {
          outputPoints[pointCursor] =
            originX + normalizedX * columnVectorX + normalizedY * rowVectorX
          outputPoints[pointCursor + 1] =
            originY + normalizedX * columnVectorY + normalizedY * rowVectorY
        }
      } else {
        const cellLocalX = gridX - (gridX | 0)
        const cellLocalY = gridY - (gridY | 0)
        const basePointOffset = getGridPointOffset(gridX | 0, gridY | 0, strideColumnCount)
        writeSplitQuadPoint(
          outputPoints,
          pointCursor,
          gridPoints[basePointOffset]!,
          gridPoints[basePointOffset + 1]!,
          gridPoints[basePointOffset + 2]!,
          gridPoints[basePointOffset + 3]!,
          gridPoints[basePointOffset + 2 * strideColumnCount]!,
          gridPoints[basePointOffset + 2 * strideColumnCount + 1]!,
          gridPoints[basePointOffset + 2 * strideColumnCount + 2]!,
          gridPoints[basePointOffset + 2 * strideColumnCount + 3]!,
          cellLocalX,
          cellLocalY,
          false,
        )
      }
    }
  }

  /**
   * Applies the older SDK1 grid interpolation branch retained for compatibility with legacy flags.
   * @param modelContext Runtime model context retained for signature parity with SDK2 point transforms.
   * @param sourceContext Base context whose local or target-space points drive the mapping.
   * @param sourcePoints Input interleaved point buffer.
   * @param outputPoints Output interleaved point buffer.
   * @param pointCount Number of points to transform.
   * @param pointOffset First element offset in the interleaved point buffers.
   * @param pointStride Stride between points in the interleaved point buffers.
   * @returns Nothing; the output point buffer is mutated in place.
   */
  GridBaseData.prototype.transformPointsSdk1 = function (
    modelContext: unknown,
    sourceContext: unknown,
    sourcePoints: unknown,
    outputPoints: unknown,
    pointCount: number,
    pointOffset: number,
    pointStride: number,
  ): void {
    void modelContext
    var runtimeSourceContext = sourceContext as Cubism2GridContextInstance
    var inputPoints = sourcePoints as ArrayLike<number>
    var outputPointBuffer = outputPoints as MutableNumberArray
    var gridX, gridY
    var gridColumnCount = this.gridColumnCount
    var gridRowCount = this.gridRowCount
    var exclusivePointOffset = pointCount * pointStride
    var cellColumnIndex, cellRowIndex
    var basePointOffset
    var cellLocalX, cellLocalY
    var gridPoints =
      runtimeSourceContext.targetSpacePoints != null
        ? runtimeSourceContext.targetSpacePoints
        : runtimeSourceContext.localPoints!
    for (
      var pointCursor = pointOffset;
      pointCursor < exclusivePointOffset;
      pointCursor += pointStride
    ) {
      if (Live2D.shouldClampSdk1GridPointsToUnitRange) {
        gridX = inputPoints[pointCursor]!
        gridY = inputPoints[pointCursor + 1]!
        if (gridX < 0) {
          gridX = 0
        } else {
          if (gridX > 1) {
            gridX = 1
          }
        }
        if (gridY < 0) {
          gridY = 0
        } else {
          if (gridY > 1) {
            gridY = 1
          }
        }
        gridX *= gridColumnCount
        gridY *= gridRowCount
        cellColumnIndex = gridX | 0
        cellRowIndex = gridY | 0
        if (cellColumnIndex > gridColumnCount - 1) {
          cellColumnIndex = gridColumnCount - 1
        }
        if (cellRowIndex > gridRowCount - 1) {
          cellRowIndex = gridRowCount - 1
        }
        cellLocalX = gridX - cellColumnIndex
        cellLocalY = gridY - cellRowIndex
        basePointOffset = 2 * (cellColumnIndex + cellRowIndex * (gridColumnCount + 1))
      } else {
        gridX = inputPoints[pointCursor]! * gridColumnCount
        gridY = inputPoints[pointCursor + 1]! * gridRowCount
        cellLocalX = gridX - (gridX | 0)
        cellLocalY = gridY - (gridY | 0)
        basePointOffset = 2 * ((gridX | 0) + (gridY | 0) * (gridColumnCount + 1))
      }
      if (cellLocalX + cellLocalY < 1) {
        outputPointBuffer[pointCursor] =
          gridPoints[basePointOffset]! * (1 - cellLocalX - cellLocalY) +
          gridPoints[basePointOffset + 2]! * cellLocalX +
          gridPoints[basePointOffset + 2 * (gridColumnCount + 1)]! * cellLocalY
        outputPointBuffer[pointCursor + 1] =
          gridPoints[basePointOffset + 1]! * (1 - cellLocalX - cellLocalY) +
          gridPoints[basePointOffset + 3]! * cellLocalX +
          gridPoints[basePointOffset + 2 * (gridColumnCount + 1) + 1]! * cellLocalY
      } else {
        outputPointBuffer[pointCursor] =
          gridPoints[basePointOffset + 2 * (gridColumnCount + 1) + 2]! *
            (cellLocalX - 1 + cellLocalY) +
          gridPoints[basePointOffset + 2 * (gridColumnCount + 1)]! * (1 - cellLocalX) +
          gridPoints[basePointOffset + 2]! * (1 - cellLocalY)
        outputPointBuffer[pointCursor + 1] =
          gridPoints[basePointOffset + 2 * (gridColumnCount + 1) + 3]! *
            (cellLocalX - 1 + cellLocalY) +
          gridPoints[basePointOffset + 2 * (gridColumnCount + 1) + 1]! * (1 - cellLocalX) +
          gridPoints[basePointOffset + 3]! * (1 - cellLocalY)
      }
    }
  }

  /**
   * Counts all grid control points including the final row and column.
   * @returns Number of interleaved grid control points stored in the runtime buffers.
   */
  GridBaseData.prototype.getGridPointCount = function (): number {
    return (this.gridColumnCount + 1) * (this.gridRowCount + 1)
  }

  /**
   * Returns the Cubism2 base-data discriminator for type-65 grid records.
   * @returns Grid base-data type marker used by target-base dependency logic.
   */
  GridBaseData.prototype.getType = function (): number {
    return Cubism2BaseData.TYPE_GRID
  }

  /**
   * Initializes a grid base-data runtime context derived from the shared Cubism2 base context.
   * @param baseData Grid base-data definition that owns this runtime context.
   * @returns Nothing; the context stores target-base indexes and transformed point buffers.
   */
  function Cubism2GridContext(
    this: Cubism2GridContextInstance,
    baseData: Cubism2GridBaseDataInstance,
  ): void {
    Cubism2BaseContext.prototype.constructor.call(this, baseData)
    this.targetBaseDataIndex = Cubism2BaseData.UNRESOLVED_BASE_DATA_INDEX
    this.localPoints = null
    this.targetSpacePoints = null
  }

  const GridContext = Cubism2GridContext as unknown as Cubism2GridContextConstructor
  GridContext.prototype = new Cubism2BaseContext() as Cubism2GridContextInstance

  return {
    Cubism2GridBaseData: GridBaseData,
    Cubism2GridContext: GridContext,
  }
}
