/**
 * Utilities for parsing flat styles for WebGL renderers
 * @module ol/render/webgl/style
 */
import {assert} from '../../asserts.js';
import {
  BooleanType,
  ColorType,
  NumberArrayType,
  NumberType,
  SizeType,
  StringType,
  computeGeometryType,
} from '../../expr/expression.js';
import {
  FEATURE_ID_PROPERTY_NAME,
  GEOMETRY_TYPE_PROPERTY_NAME,
  getStringNumberEquivalent,
  newCompilationContext,
  stringToGlsl,
} from '../../expr/gpu.js';
import {ShaderBuilder} from './ShaderBuilder.js';
import {
  applyContextToBuilder,
  expressionToGlsl,
  generateAttributesFromContext,
  generateUniformsFromContext,
  getGlslSizeFromType,
  getGlslTypeFromType,
} from './compileUtil.js';

/**
 * see https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
 * @param {Object|string} input The hash input, either an object or string
 * @return {string} Hash (if the object cannot be serialized, it is based on `getUid`)
 */
export function computeHash(input) {
  const hash = JSON.stringify(input)
    .split('')
    .reduce((prev, curr) => (prev << 5) - prev + curr.charCodeAt(0), 0);
  return (hash >>> 0).toString();
}

/**
 * @param {import("../../style/flat.js").FlatStyle} style Style
 * @param {ShaderBuilder} builder Shader builder
 * @param {import("../../expr/gpu.js").CompilationContext} vertContext Vertex shader compilation context
 * @param {'shape-'|'circle-'|'icon-'} prefix Properties prefix
 */
function parseCommonSymbolProperties(style, builder, vertContext, prefix) {
  if (`${prefix}radius` in style && prefix !== 'icon-') {
    let radius = expressionToGlsl(
      vertContext,
      style[`${prefix}radius`],
      NumberType,
    );
    if (`${prefix}radius2` in style) {
      const radius2 = expressionToGlsl(
        vertContext,
        style[`${prefix}radius2`],
        NumberType,
      );
      radius = `max(${radius}, ${radius2})`;
    }
    if (`${prefix}stroke-width` in style) {
      radius = `(${radius} + ${expressionToGlsl(
        vertContext,
        style[`${prefix}stroke-width`],
        NumberType,
      )} * 0.5)`;
    }
    builder.setSymbolSizeExpression(`vec2(${radius} * 2. + 0.5)`); // adding some padding for antialiasing
  }
  if (`${prefix}scale` in style) {
    const scale = expressionToGlsl(
      vertContext,
      style[`${prefix}scale`],
      SizeType,
    );
    builder.setSymbolSizeExpression(
      `${builder.getSymbolSizeExpression()} * ${scale}`,
    );
  }
  if (`${prefix}displacement` in style) {
    builder.setSymbolOffsetExpression(
      expressionToGlsl(
        vertContext,
        style[`${prefix}displacement`],
        NumberArrayType,
      ),
    );
  }
  if (`${prefix}rotation` in style) {
    builder.setSymbolRotationExpression(
      expressionToGlsl(vertContext, style[`${prefix}rotation`], NumberType),
    );
  }
  if (`${prefix}rotate-with-view` in style) {
    builder.setSymbolRotateWithView(!!style[`${prefix}rotate-with-view`]);
  }
}

/**
 * @param {string} distanceField The distance field expression
 * @param {string|null} fillColor The fill color expression; null if no fill
 * @param {string|null} strokeColor The stroke color expression; null if no stroke
 * @param {string|null} strokeWidth The stroke width expression; null if no stroke
 * @param {string|null} opacity The opacity expression; null if no stroke
 * @return {string} The final color expression, based on the distance field and given params
 */
function getColorFromDistanceField(
  distanceField,
  fillColor,
  strokeColor,
  strokeWidth,
  opacity,
) {
  let color = 'vec4(0.)';
  if (fillColor !== null) {
    color = fillColor;
  }
  if (strokeColor !== null && strokeWidth !== null) {
    const strokeFillRatio = `smoothstep(-${strokeWidth} + 0.63, -${strokeWidth} - 0.58, ${distanceField})`;
    color = `mix(${strokeColor}, ${color}, ${strokeFillRatio})`;
  }
  const shapeOpacity = `(1.0 - smoothstep(-0.63, 0.58, ${distanceField}))`;
  let result = `${color} * vec4(1.0, 1.0, 1.0, ${shapeOpacity})`;
  if (opacity !== null) {
    result = `${result} * vec4(1.0, 1.0, 1.0, ${opacity})`;
  }
  return result;
}

/**
 * This will parse an image property provided by `<prefix>-src`
 * The image size expression in GLSL will be returned
 * @param {import("../../style/flat.js").FlatStyle} style Style
 * @param {ShaderBuilder} builder Shader builder
 * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms
 * @param {'icon-'|'fill-pattern-'|'stroke-pattern-'} prefix Property prefix
 * @param {string} textureId A identifier that will be used in the generated uniforms: `sample2d u_texture<id>` and `vec2 u_texture<id>_size`
 * @return {string} The image size expression
 */
function parseImageProperties(style, builder, uniforms, prefix, textureId) {
  const image = new Image();
  image.crossOrigin =
    style[`${prefix}cross-origin`] === undefined
      ? 'anonymous'
      : style[`${prefix}cross-origin`];
  assert(
    typeof style[`${prefix}src`] === 'string',
    `WebGL layers do not support expressions for the ${prefix}src style property`,
  );
  image.src = /** @type {string} */ (style[`${prefix}src`]);

  // the size is provided asynchronously using a uniform
  uniforms[`u_texture${textureId}_size`] = () => {
    return image.complete ? [image.width, image.height] : [0, 0];
  };
  builder.addUniform(`u_texture${textureId}_size`, 'vec2');
  const size = `u_texture${textureId}_size`;

  uniforms[`u_texture${textureId}`] = image;
  builder.addUniform(`u_texture${textureId}`, 'sampler2D');
  return size;
}

/**
 * This will parse an image's offset properties provided by `<prefix>-offset`, `<prefix>-offset-origin` and `<prefix>-size`
 * @param {import("../../style/flat.js").FlatStyle} style Style
 * @param {'icon-'|'fill-pattern-'|'stroke-pattern-'} prefix Property prefix
 * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context (vertex or fragment)
 * @param {string} imageSize Pixel size of the full image as a GLSL expression
 * @param {string} sampleSize Pixel size of the sample in the image as a GLSL expression
 * @return {string} The offset expression
 */
function parseImageOffsetProperties(
  style,
  prefix,
  context,
  imageSize,
  sampleSize,
) {
  let offsetExpression = expressionToGlsl(
    context,
    style[`${prefix}offset`],
    SizeType,
  );
  if (`${prefix}offset-origin` in style) {
    switch (style[`${prefix}offset-origin`]) {
      case 'top-right':
        offsetExpression = `vec2(${imageSize}.x, 0.) + ${sampleSize} * vec2(-1., 0.) + ${offsetExpression} * vec2(-1., 1.)`;
        break;
      case 'bottom-left':
        offsetExpression = `vec2(0., ${imageSize}.y) + ${sampleSize} * vec2(0., -1.) + ${offsetExpression} * vec2(1., -1.)`;
        break;
      case 'bottom-right':
        offsetExpression = `${imageSize} - ${sampleSize} - ${offsetExpression}`;
        break;
      default: // pass
    }
  }
  return offsetExpression;
}

/**
 * @param {import("../../style/flat.js").FlatStyle} style Style
 * @param {ShaderBuilder} builder Shader builder
 * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms
 * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context
 */
function parseCircleProperties(style, builder, uniforms, context) {
  // this function takes in screen coordinates in pixels and returns the signed distance field
  // (0 on the boundary, negative inside the circle, positive outside, values in pixels)
  context.functions['circleDistanceField'] =
    `float circleDistanceField(vec2 point, float radius) {
  return length(point) - radius;
}`;

  parseCommonSymbolProperties(style, builder, context, 'circle-');

  // OPACITY
  let opacity = null;
  if ('circle-opacity' in style) {
    opacity = expressionToGlsl(context, style['circle-opacity'], NumberType);
  }

  // SCALE
  let currentPoint = 'coordsPx';
  if ('circle-scale' in style) {
    const scale = expressionToGlsl(context, style['circle-scale'], SizeType);
    currentPoint = `coordsPx / ${scale}`;
  }

  // FILL COLOR
  let fillColor = null;
  if ('circle-fill-color' in style) {
    fillColor = expressionToGlsl(
      context,
      style['circle-fill-color'],
      ColorType,
    );
  }

  // STROKE COLOR
  let strokeColor = null;
  if ('circle-stroke-color' in style) {
    strokeColor = expressionToGlsl(
      context,
      style['circle-stroke-color'],
      ColorType,
    );
  }

  // RADIUS
  let radius = expressionToGlsl(context, style['circle-radius'], NumberType);

  // STROKE WIDTH
  let strokeWidth = null;
  if ('circle-stroke-width' in style) {
    strokeWidth = expressionToGlsl(
      context,
      style['circle-stroke-width'],
      NumberType,
    );
    radius = `(${radius} + ${strokeWidth} * 0.5)`;
  }

  // FINAL COLOR
  const distanceField = `circleDistanceField(${currentPoint}, ${radius})`;
  const colorExpression = getColorFromDistanceField(
    distanceField,
    fillColor,
    strokeColor,
    strokeWidth,
    opacity,
  );
  builder.setSymbolColorExpression(colorExpression);
}

/**
 * @param {import("../../style/flat.js").FlatStyle} style Style
 * @param {ShaderBuilder} builder Shader builder
 * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms
 * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context
 */
function parseShapeProperties(style, builder, uniforms, context) {
  context.functions['round'] = `float round(float v) {
  return sign(v) * floor(abs(v) + 0.5);
}`;

  // these functions take in screen coordinates in pixels and returns the signed distance field
  // (0 on the boundary, negative inside the polygon, positive outside, values in pixels)
  // inspired by https://github.com/zranger1/PixelblazePatterns/blob/master/Toolkit/sdf2d.md#n-sided-regular-polygon
  context.functions['starDistanceField'] =
    `float starDistanceField(vec2 point, float numPoints, float radius, float radius2, float angle) {
  float startAngle = -PI * 0.5 + angle; // tip starts upwards and rotates clockwise with angle
  float c = cos(startAngle);
  float s = sin(startAngle);
  vec2 pointRotated = vec2(c * point.x - s * point.y, s * point.x + c * point.y);
  float alpha = TWO_PI / numPoints; // the angle of one sector
  float beta = atan(pointRotated.y, pointRotated.x);
  float gamma = round(beta / alpha) * alpha; // angle in sector
  c = cos(-gamma);
  s = sin(-gamma);
  vec2 inSector = vec2(c * pointRotated.x - s * pointRotated.y, abs(s * pointRotated.x + c * pointRotated.y));
  vec2 tipToPoint = inSector + vec2(-radius, 0.);
  vec2 edgeNormal = vec2(radius2 * sin(alpha * 0.5), -radius2 * cos(alpha * 0.5) + radius);
  return dot(normalize(edgeNormal), tipToPoint);
}`;
  context.functions['regularDistanceField'] =
    `float regularDistanceField(vec2 point, float numPoints, float radius, float angle) {
  float startAngle = -PI * 0.5 + angle; // tip starts upwards and rotates clockwise with angle
  float c = cos(startAngle);
  float s = sin(startAngle);
  vec2 pointRotated = vec2(c * point.x - s * point.y, s * point.x + c * point.y);
  float alpha = TWO_PI / numPoints; // the angle of one sector
  float radiusIn = radius * cos(PI / numPoints);
  float beta = atan(pointRotated.y, pointRotated.x);
  float gamma = round((beta - alpha * 0.5) / alpha) * alpha + alpha * 0.5; // angle in sector from mid
  c = cos(-gamma);
  s = sin(-gamma);
  vec2 inSector = vec2(c * pointRotated.x - s * pointRotated.y, abs(s * pointRotated.x + c * pointRotated.y));
  return inSector.x - radiusIn;
}`;

  parseCommonSymbolProperties(style, builder, context, 'shape-');

  // OPACITY
  let opacity = null;
  if ('shape-opacity' in style) {
    opacity = expressionToGlsl(context, style['shape-opacity'], NumberType);
  }

  // SCALE
  let currentPoint = 'coordsPx';
  if ('shape-scale' in style) {
    const scale = expressionToGlsl(context, style['shape-scale'], SizeType);
    currentPoint = `coordsPx / ${scale}`;
  }

  // FILL COLOR
  let fillColor = null;
  if ('shape-fill-color' in style) {
    fillColor = expressionToGlsl(context, style['shape-fill-color'], ColorType);
  }

  // STROKE COLOR
  let strokeColor = null;
  if ('shape-stroke-color' in style) {
    strokeColor = expressionToGlsl(
      context,
      style['shape-stroke-color'],
      ColorType,
    );
  }

  // STROKE WIDTH
  let strokeWidth = null;
  if ('shape-stroke-width' in style) {
    strokeWidth = expressionToGlsl(
      context,
      style['shape-stroke-width'],
      NumberType,
    );
  }

  // SHAPE TYPE
  const numPoints = expressionToGlsl(
    context,
    style['shape-points'],
    NumberType,
  );
  let angle = '0.';
  if ('shape-angle' in style) {
    angle = expressionToGlsl(context, style['shape-angle'], NumberType);
  }
  let shapeField;
  let radius = expressionToGlsl(context, style['shape-radius'], NumberType);
  if (strokeWidth !== null) {
    radius = `${radius} + ${strokeWidth} * 0.5`;
  }
  if ('shape-radius2' in style) {
    let radius2 = expressionToGlsl(context, style['shape-radius2'], NumberType);
    if (strokeWidth !== null) {
      radius2 = `${radius2} + ${strokeWidth} * 0.5`;
    }
    shapeField = `starDistanceField(${currentPoint}, ${numPoints}, ${radius}, ${radius2}, ${angle})`;
  } else {
    shapeField = `regularDistanceField(${currentPoint}, ${numPoints}, ${radius}, ${angle})`;
  }

  // FINAL COLOR
  const colorExpression = getColorFromDistanceField(
    shapeField,
    fillColor,
    strokeColor,
    strokeWidth,
    opacity,
  );
  builder.setSymbolColorExpression(colorExpression);
}

/**
 * @param {import("../../style/flat.js").FlatStyle} style Style
 * @param {ShaderBuilder} builder Shader builder
 * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms
 * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context
 */
function parseIconProperties(style, builder, uniforms, context) {
  // COLOR
  let color = 'vec4(1.0)';
  if ('icon-color' in style) {
    color = expressionToGlsl(context, style['icon-color'], ColorType);
  }

  // OPACITY
  if ('icon-opacity' in style) {
    color = `${color} * vec4(1.0, 1.0, 1.0, ${expressionToGlsl(
      context,
      style['icon-opacity'],
      NumberType,
    )})`;
  }

  // IMAGE & SIZE
  const textureId = computeHash(style['icon-src']);
  const sizeExpression = parseImageProperties(
    style,
    builder,
    uniforms,
    'icon-',
    textureId,
  );
  builder
    .setSymbolColorExpression(
      `${color} * texture2D(u_texture${textureId}, v_texCoord)`,
    )
    .setSymbolSizeExpression(sizeExpression);

  // override size if width/height were specified
  if ('icon-width' in style && 'icon-height' in style) {
    builder.setSymbolSizeExpression(
      `vec2(${expressionToGlsl(
        context,
        style['icon-width'],
        NumberType,
      )}, ${expressionToGlsl(context, style['icon-height'], NumberType)})`,
    );
  }

  // tex coord
  if ('icon-offset' in style && 'icon-size' in style) {
    const sampleSize = expressionToGlsl(
      context,
      style['icon-size'],
      NumberArrayType,
    );
    const fullsize = builder.getSymbolSizeExpression();
    builder.setSymbolSizeExpression(sampleSize);
    const offset = parseImageOffsetProperties(
      style,
      'icon-',
      context,
      'v_quadSizePx',
      sampleSize,
    );
    builder.setTextureCoordinateExpression(
      `(vec4((${offset}).xyxy) + vec4(0., 0., ${sampleSize})) / (${fullsize}).xyxy`,
    );
  }

  parseCommonSymbolProperties(style, builder, context, 'icon-');

  if ('icon-anchor' in style) {
    const anchor = expressionToGlsl(
      context,
      style['icon-anchor'],
      NumberArrayType,
    );
    let scale = `1.0`;
    if (`icon-scale` in style) {
      scale = expressionToGlsl(context, style[`icon-scale`], SizeType);
    }
    let shiftPx;
    if (
      style['icon-anchor-x-units'] === 'pixels' &&
      style['icon-anchor-y-units'] === 'pixels'
    ) {
      shiftPx = `${anchor} * ${scale}`;
    } else if (style['icon-anchor-x-units'] === 'pixels') {
      shiftPx = `${anchor} * vec2(vec2(${scale}).x, v_quadSizePx.y)`;
    } else if (style['icon-anchor-y-units'] === 'pixels') {
      shiftPx = `${anchor} * vec2(v_quadSizePx.x, vec2(${scale}).x)`;
    } else {
      shiftPx = `${anchor} * v_quadSizePx`;
    }
    // default origin is top-left
    let offsetPx = `v_quadSizePx * vec2(0.5, -0.5) + ${shiftPx} * vec2(-1., 1.)`;
    if ('icon-anchor-origin' in style) {
      switch (style['icon-anchor-origin']) {
        case 'top-right':
          offsetPx = `v_quadSizePx * -0.5 + ${shiftPx}`;
          break;
        case 'bottom-left':
          offsetPx = `v_quadSizePx * 0.5 - ${shiftPx}`;
          break;
        case 'bottom-right':
          offsetPx = `v_quadSizePx * vec2(-0.5, 0.5) + ${shiftPx} * vec2(1., -1.)`;
          break;
        default: // pass
      }
    }
    builder.setSymbolOffsetExpression(
      `${builder.getSymbolOffsetExpression()} + ${offsetPx}`,
    );
  }
}

/**
 * @param {import("../../style/flat.js").FlatStyle} style Style
 * @param {ShaderBuilder} builder Shader Builder
 * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms
 * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context
 */
function parseStrokeProperties(style, builder, uniforms, context) {
  if ('stroke-color' in style) {
    builder.setStrokeColorExpression(
      expressionToGlsl(context, style['stroke-color'], ColorType),
    );
  }
  if ('stroke-pattern-src' in style) {
    const textureId = computeHash(style['stroke-pattern-src']);
    const sizeExpression = parseImageProperties(
      style,
      builder,
      uniforms,
      'stroke-pattern-',
      textureId,
    );
    let sampleSizeExpression = sizeExpression;
    let offsetExpression = 'vec2(0.)';
    if ('stroke-pattern-offset' in style && 'stroke-pattern-size' in style) {
      sampleSizeExpression = expressionToGlsl(
        context,
        style[`stroke-pattern-size`],
        NumberArrayType,
      );
      offsetExpression = parseImageOffsetProperties(
        style,
        'stroke-pattern-',
        context,
        sizeExpression,
        sampleSizeExpression,
      );
    }
    let spacingExpression = '0.';
    if ('stroke-pattern-spacing' in style) {
      spacingExpression = expressionToGlsl(
        context,
        style['stroke-pattern-spacing'],
        NumberType,
      );
    }
    let startOffsetExpression = '0.';
    if ('stroke-pattern-start-offset' in style) {
      startOffsetExpression = expressionToGlsl(
        context,
        style['stroke-pattern-start-offset'],
        NumberType,
      );
    }
    context.functions['sampleStrokePattern'] =
      `vec4 sampleStrokePattern(sampler2D texture, vec2 textureSize, vec2 textureOffset, vec2 sampleSize, float spacingPx, float startOffsetPx, float currentLengthPx, float currentRadiusRatio, float lineWidth) {
  float currentLengthScaled = (currentLengthPx - startOffsetPx) * sampleSize.y / lineWidth;
  float spacingScaled = spacingPx * sampleSize.y / lineWidth;
  float uCoordPx = mod(currentLengthScaled, (sampleSize.x + spacingScaled));
  float isInsideOfPattern = step(uCoordPx, sampleSize.x);
  float vCoordPx = (-currentRadiusRatio * 0.5 + 0.5) * sampleSize.y;
  // make sure that we're not sampling too close to the borders to avoid interpolation with outside pixels
  uCoordPx = clamp(uCoordPx, 0.5, sampleSize.x - 0.5);
  vCoordPx = clamp(vCoordPx, 0.5, sampleSize.y - 0.5);
  vec2 texCoord = (vec2(uCoordPx, vCoordPx) + textureOffset) / textureSize;
  return texture2D(texture, texCoord) * vec4(1.0, 1.0, 1.0, isInsideOfPattern);
}`;
    const textureName = `u_texture${textureId}`;
    let tintExpression = '1.';
    if ('stroke-color' in style) {
      tintExpression = builder.getStrokeColorExpression();
    }
    builder.setStrokeColorExpression(
      `${tintExpression} * sampleStrokePattern(${textureName}, ${sizeExpression}, ${offsetExpression}, ${sampleSizeExpression}, ${spacingExpression}, ${startOffsetExpression}, currentLengthPx, currentRadiusRatio, v_width)`,
    );

    context.functions['computeStrokePatternLength'] =
      `float computeStrokePatternLength(vec2 sampleSize, float spacingPx, float lineWidth) {
  float patternLengthPx = sampleSize.x / sampleSize.y * lineWidth;
  return patternLengthPx + spacingPx;
}`;

    // apply a stroke pattern length to avoid visual artifacts
    builder.setStrokePatternLengthExpression(
      `computeStrokePatternLength(${sampleSizeExpression}, ${spacingExpression}, v_width)`,
    );
  }

  if ('stroke-width' in style) {
    builder.setStrokeWidthExpression(
      expressionToGlsl(context, style['stroke-width'], NumberType),
    );
  }

  if ('stroke-offset' in style) {
    builder.setStrokeOffsetExpression(
      expressionToGlsl(context, style['stroke-offset'], NumberType),
    );
  }

  if ('stroke-line-cap' in style) {
    builder.setStrokeCapExpression(
      expressionToGlsl(context, style['stroke-line-cap'], StringType),
    );
  }

  if ('stroke-line-join' in style) {
    builder.setStrokeJoinExpression(
      expressionToGlsl(context, style['stroke-line-join'], StringType),
    );
  }

  if ('stroke-miter-limit' in style) {
    builder.setStrokeMiterLimitExpression(
      expressionToGlsl(context, style['stroke-miter-limit'], NumberType),
    );
  }

  if ('stroke-line-dash' in style) {
    context.functions['getSingleDashDistance'] =
      `float getSingleDashDistance(float distance, float radius, float dashOffset, float dashLength, float dashLengthTotal, float capType, float lineWidth) {
  float localDistance = mod(distance, dashLengthTotal);
  float distanceSegment = abs(localDistance - dashOffset - dashLength * 0.5) - dashLength * 0.5;
  distanceSegment = min(distanceSegment, dashLengthTotal - localDistance);
  if (capType == ${stringToGlsl('square')}) {
    distanceSegment -= lineWidth * 0.5;
  } else if (capType == ${stringToGlsl('round')}) {
    distanceSegment = min(distanceSegment, sqrt(distanceSegment * distanceSegment + radius * radius) - lineWidth * 0.5);
  }
  return distanceSegment;
}`;

    let dashPattern = style['stroke-line-dash'].map((v) =>
      expressionToGlsl(context, v, NumberType),
    );
    // if pattern has odd length, concatenate it with itself to be even
    if (dashPattern.length % 2 === 1) {
      dashPattern = [...dashPattern, ...dashPattern];
    }

    let offsetExpression = '0.';
    if ('stroke-line-dash-offset' in style) {
      offsetExpression = expressionToGlsl(
        context,
        style['stroke-line-dash-offset'],
        NumberType,
      );
    }

    // define a function for this dash specifically
    const uniqueDashKey = computeHash(style['stroke-line-dash']);
    const dashFunctionName = `dashDistanceField_${uniqueDashKey}`;

    const dashLengthsParamsDef = dashPattern
      .map((v, i) => `float dashLength${i}`)
      .join(', ');
    const totalLengthDef = dashPattern
      .map((v, i) => `dashLength${i}`)
      .join(' + ');
    let currentDashOffset = '0.';
    let distanceExpression = `getSingleDashDistance(distance, radius, ${currentDashOffset}, dashLength0, totalDashLength, capType, lineWidth)`;
    for (let i = 2; i < dashPattern.length; i += 2) {
      currentDashOffset = `${currentDashOffset} + dashLength${
        i - 2
      } + dashLength${i - 1}`;
      distanceExpression = `min(${distanceExpression}, getSingleDashDistance(distance, radius, ${currentDashOffset}, dashLength${i}, totalDashLength, capType, lineWidth))`;
    }

    context.functions[dashFunctionName] =
      `float ${dashFunctionName}(float distance, float radius, float capType, float lineWidth, ${dashLengthsParamsDef}) {
  float totalDashLength = ${totalLengthDef};
  return ${distanceExpression};
}`;
    const dashLengthsCalls = dashPattern.map((v, i) => `${v}`).join(', ');
    builder.setStrokeDistanceFieldExpression(
      `${dashFunctionName}(currentLengthPx + ${offsetExpression}, currentRadiusPx, capType, v_width, ${dashLengthsCalls})`,
    );

    // apply a stroke pattern length to avoid visual artifacts
    let patternLength = dashPattern.join(' + ');
    if (builder.getStrokePatternLengthExpression()) {
      context.functions['combinePatternLengths'] =
        `float combinePatternLengths(float patternLength1, float patternLength2) {
  return patternLength1 * patternLength2;
}`;
      patternLength = `combinePatternLengths(${builder.getStrokePatternLengthExpression()}, ${patternLength})`;
    }
    builder.setStrokePatternLengthExpression(patternLength);
  }
}

/**
 * @param {import("../../style/flat.js").FlatStyle} style Style
 * @param {ShaderBuilder} builder Shader Builder
 * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms
 * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context
 */
function parseFillProperties(style, builder, uniforms, context) {
  if ('fill-color' in style) {
    builder.setFillColorExpression(
      expressionToGlsl(context, style['fill-color'], ColorType),
    );
  }
  if ('fill-pattern-src' in style) {
    const textureId = computeHash(style['fill-pattern-src']);
    const sizeExpression = parseImageProperties(
      style,
      builder,
      uniforms,
      'fill-pattern-',
      textureId,
    );
    let sampleSizeExpression = sizeExpression;
    let offsetExpression = 'vec2(0.)';
    if ('fill-pattern-offset' in style && 'fill-pattern-size' in style) {
      sampleSizeExpression = expressionToGlsl(
        context,
        style[`fill-pattern-size`],
        NumberArrayType,
      );
      offsetExpression = parseImageOffsetProperties(
        style,
        'fill-pattern-',
        context,
        sizeExpression,
        sampleSizeExpression,
      );
    }
    context.functions['sampleFillPattern'] =
      `vec4 sampleFillPattern(sampler2D texture, vec2 textureSize, vec2 textureOffset, vec2 sampleSize, vec2 pxOrigin, vec2 pxPosition) {
  float scaleRatio = pow(2., mod(u_zoom + 0.5, 1.) - 0.5);
  vec2 pxRelativePos = pxPosition - pxOrigin;
  // rotate the relative position from origin by the current view rotation
  pxRelativePos = vec2(pxRelativePos.x * cos(u_rotation) - pxRelativePos.y * sin(u_rotation), pxRelativePos.x * sin(u_rotation) + pxRelativePos.y * cos(u_rotation));
  // sample position is computed according to the sample offset & size
  vec2 samplePos = mod(pxRelativePos / scaleRatio, sampleSize);
  // also make sure that we're not sampling too close to the borders to avoid interpolation with outside pixels
  samplePos = clamp(samplePos, vec2(0.5), sampleSize - vec2(0.5));
  samplePos.y = sampleSize.y - samplePos.y; // invert y axis so that images appear upright
  return texture2D(texture, (samplePos + textureOffset) / textureSize);
}`;
    const textureName = `u_texture${textureId}`;
    let tintExpression = '1.';
    if ('fill-color' in style) {
      tintExpression = builder.getFillColorExpression();
    }
    builder.setFillColorExpression(
      `${tintExpression} * sampleFillPattern(${textureName}, ${sizeExpression}, ${offsetExpression}, ${sampleSizeExpression}, pxOrigin, pxPos)`,
    );
  }
}

/**
 * @typedef {Object} StyleParseResult
 * @property {ShaderBuilder} builder Shader builder pre-configured according to a given style
 * @property {import("./VectorStyleRenderer.js").UniformDefinitions} uniforms Uniform definitions
 * @property {import("./VectorStyleRenderer.js").AttributeDefinitions} attributes Attribute definitions
 */

/**
 * Parses a {@link import("../../style/flat.js").FlatStyle} object and returns a {@link ShaderBuilder}
 * object that has been configured according to the given style, as well as `attributes` and `uniforms`
 * arrays to be fed to the `WebGLPointsRenderer` class.
 *
 * Also returns `uniforms` and `attributes` properties as expected by the
 * {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}.
 *
 * @param {import("../../style/flat.js").FlatStyle} style Flat style.
 * @param {import('../../style/flat.js').StyleVariables} [variables] Style variables.
 * @param {import("../../expr/expression.js").EncodedExpression} [filter] Filter (if any)
 * @return {StyleParseResult} Result containing shader params, attributes and uniforms.
 */
export function parseLiteralStyle(style, variables, filter) {
  const context = newCompilationContext();

  const builder = new ShaderBuilder();

  /** @type {Object<string,import("../../webgl/Helper").UniformValue>} */
  const uniforms = {};

  if ('icon-src' in style) {
    parseIconProperties(style, builder, uniforms, context);
  } else if ('shape-points' in style) {
    parseShapeProperties(style, builder, uniforms, context);
  } else if ('circle-radius' in style) {
    parseCircleProperties(style, builder, uniforms, context);
  }
  parseStrokeProperties(style, builder, uniforms, context);
  parseFillProperties(style, builder, uniforms, context);

  // note that the style filter may have already been applied earlier when building the rendering instructions
  // this is still needed in case a filter cannot be evaluated statically beforehand (e.g. depending on time)
  if (filter) {
    const parsedFilter = expressionToGlsl(context, filter, BooleanType);
    builder.setFragmentDiscardExpression(`!${parsedFilter}`);
  }

  /**
   * @type {import('./VectorStyleRenderer.js').AttributeDefinitions}
   */
  const attributes = {};

  // Define attributes for special inputs
  function defineSpecialInput(contextPropName, glslPropName, type, callback) {
    if (!context[contextPropName]) {
      return;
    }
    const glslType = getGlslTypeFromType(type);
    const attrSize = getGlslSizeFromType(type);
    builder.addAttribute(`a_${glslPropName}`, glslType);

    attributes[glslPropName] = {
      size: attrSize,
      callback,
    };
  }
  defineSpecialInput(
    'geometryType',
    GEOMETRY_TYPE_PROPERTY_NAME,
    StringType,
    (feature) =>
      getStringNumberEquivalent(computeGeometryType(feature.getGeometry())),
  );
  defineSpecialInput(
    'featureId',
    FEATURE_ID_PROPERTY_NAME,
    StringType | NumberType,
    (feature) => {
      const id = feature.getId() ?? null;
      return typeof id === 'string' ? getStringNumberEquivalent(id) : id;
    },
  );

  applyContextToBuilder(builder, context);

  return {
    builder,
    attributes: {...attributes, ...generateAttributesFromContext(context)},
    uniforms: {
      ...uniforms,
      ...generateUniformsFromContext(context, variables),
    },
  };
}
