/**
 * @module ol/render/canvas/style
 */

import {NO_COLOR} from '../../color.js';
import {buildExpression, newEvaluationContext} from '../../expr/cpu.js';
import {
  BooleanType,
  ColorType,
  NumberArrayType,
  NumberType,
  StringType,
  computeGeometryType,
  newParsingContext,
} from '../../expr/expression.js';
import {isEmpty} from '../../obj.js';
import {toSize} from '../../size.js';
import Circle from '../../style/Circle.js';
import Fill from '../../style/Fill.js';
import Icon from '../../style/Icon.js';
import RegularShape from '../../style/RegularShape.js';
import Stroke from '../../style/Stroke.js';
import Style from '../../style/Style.js';
import Text from '../../style/Text.js';

/**
 * @fileoverview This module includes functions to build styles for the canvas renderer.  Building
 * is composed of two steps: parsing and compiling.  The parsing step takes an encoded expression
 * and returns an instance of one of the expression classes.  The compiling step takes the
 * expression instance and returns a function that can be evaluated to return a literal value.  The
 * evaluator function should do as little allocation and work as possible.
 */

/**
 * @typedef {import("../../style/flat.js").FlatStyle} FlatStyle
 */

/**
 * @typedef {import("../../expr/expression.js").EncodedExpression} EncodedExpression
 */

/**
 * @typedef {import("../../expr/expression.js").ParsingContext} ParsingContext
 */

/**
 * @typedef {import("../../expr/expression.js").CallExpression} CallExpression
 */

/**
 * @typedef {import("../../expr/cpu.js").EvaluationContext} EvaluationContext
 */

/**
 * @typedef {import("../../expr/cpu.js").ExpressionEvaluator} ExpressionEvaluator
 */

/**
 * @param {EvaluationContext} context The evaluation context.
 * @return {boolean} Always true.
 */
function always(context) {
  return true;
}

/**
 * This function adapts a rule evaluator to the existing style function interface.
 * After we have deprecated the style function, we can use the compiled rules directly
 * and pass a more complete evaluation context (variables, zoom, time, etc.).
 *
 * @param {Array<import('../../style/flat.js').Rule>} rules The rules.
 * @return {import('../../style/Style.js').StyleFunction} A style function.
 */
export function rulesToStyleFunction(rules) {
  const parsingContext = newParsingContext();
  const evaluator = buildRuleSet(rules, parsingContext);
  const evaluationContext = newEvaluationContext();
  return function (feature, resolution) {
    evaluationContext.properties = feature.getPropertiesInternal();
    evaluationContext.resolution = resolution;
    if (parsingContext.featureId) {
      const id = feature.getId();
      if (id !== undefined) {
        evaluationContext.featureId = id;
      } else {
        evaluationContext.featureId = null;
      }
    }
    if (parsingContext.geometryType) {
      evaluationContext.geometryType = computeGeometryType(
        feature.getGeometry(),
      );
    }
    return evaluator(evaluationContext);
  };
}

/**
 * This function adapts a style evaluator to the existing style function interface.
 * After we have deprecated the style function, we can use the compiled rules directly
 * and pass a more complete evaluation context (variables, zoom, time, etc.).
 *
 * @param {Array<import('../../style/flat.js').FlatStyle>} flatStyles The flat styles.
 * @return {import('../../style/Style.js').StyleFunction} A style function.
 */
export function flatStylesToStyleFunction(flatStyles) {
  const parsingContext = newParsingContext();
  const length = flatStyles.length;

  /**
   * @type {Array<StyleEvaluator>}
   */
  const evaluators = new Array(length);
  for (let i = 0; i < length; ++i) {
    evaluators[i] = buildStyle(flatStyles[i], parsingContext);
  }
  const evaluationContext = newEvaluationContext();

  /**
   * @type {Array<Style>}
   */
  const styles = new Array(length);

  return function (feature, resolution) {
    evaluationContext.properties = feature.getPropertiesInternal();
    evaluationContext.resolution = resolution;
    if (parsingContext.featureId) {
      const id = feature.getId();
      if (id !== undefined) {
        evaluationContext.featureId = id;
      } else {
        evaluationContext.featureId = null;
      }
    }
    let nonNullCount = 0;
    for (let i = 0; i < length; ++i) {
      const style = evaluators[i](evaluationContext);
      if (style) {
        styles[nonNullCount] = style;
        nonNullCount += 1;
      }
    }
    styles.length = nonNullCount;
    return styles;
  };
}

/**
 * @typedef {function(EvaluationContext):Array<Style>} RuleSetEvaluator
 */

/**
 * @typedef {Object} CompiledRule
 * @property {ExpressionEvaluator} filter The compiled filter evaluator.
 * @property {Array<StyleEvaluator>} styles The list of compiled style evaluators.
 */

/**
 * @param {Array<import('../../style/flat.js').Rule>} rules The rules.
 * @param {ParsingContext} context The parsing context.
 * @return {RuleSetEvaluator} The evaluator function.
 */
export function buildRuleSet(rules, context) {
  const length = rules.length;

  /**
   * @type {Array<CompiledRule>}
   */
  const compiledRules = new Array(length);

  for (let i = 0; i < length; ++i) {
    const rule = rules[i];
    const filter =
      'filter' in rule
        ? buildExpression(rule.filter, BooleanType, context)
        : always;

    /**
     * @type {Array<StyleEvaluator>}
     */
    let styles;
    if (Array.isArray(rule.style)) {
      const styleLength = rule.style.length;
      styles = new Array(styleLength);
      for (let j = 0; j < styleLength; ++j) {
        styles[j] = buildStyle(rule.style[j], context);
      }
    } else {
      styles = [buildStyle(rule.style, context)];
    }

    compiledRules[i] = {filter, styles};
  }

  return function (context) {
    /**
     * @type {Array<Style>}
     */
    const styles = [];

    let someMatched = false;
    for (let i = 0; i < length; ++i) {
      const filterEvaluator = compiledRules[i].filter;
      if (!filterEvaluator(context)) {
        continue;
      }
      if (rules[i].else && someMatched) {
        continue;
      }
      someMatched = true;
      for (const styleEvaluator of compiledRules[i].styles) {
        const style = styleEvaluator(context);
        if (!style) {
          continue;
        }
        styles.push(style);
      }
    }

    return styles;
  };
}

/**
 * @typedef {function(EvaluationContext):Style|null} StyleEvaluator
 */

/**
 * @param {FlatStyle} flatStyle A flat style literal.
 * @param {ParsingContext} context The parsing context.
 * @return {StyleEvaluator} A function that evaluates to a style.  The style returned by
 * this function will be reused between invocations.
 */
export function buildStyle(flatStyle, context) {
  const evaluateFill = buildFill(flatStyle, '', context);
  const evaluateStroke = buildStroke(flatStyle, '', context);
  const evaluateText = buildText(flatStyle, context);
  const evaluateImage = buildImage(flatStyle, context);
  const evaluateZIndex = numberEvaluator(flatStyle, 'z-index', context);

  if (
    !evaluateFill &&
    !evaluateStroke &&
    !evaluateText &&
    !evaluateImage &&
    !isEmpty(flatStyle)
  ) {
    // assume this is a user error
    // would be nice to check the properties and suggest "did you mean..."
    throw new Error(
      'No fill, stroke, point, or text symbolizer properties in style: ' +
        JSON.stringify(flatStyle),
    );
  }

  const style = new Style();
  return function (context) {
    let empty = true;
    if (evaluateFill) {
      const fill = evaluateFill(context);
      if (fill) {
        empty = false;
      }
      style.setFill(fill);
    }
    if (evaluateStroke) {
      const stroke = evaluateStroke(context);
      if (stroke) {
        empty = false;
      }
      style.setStroke(stroke);
    }
    if (evaluateText) {
      const text = evaluateText(context);
      if (text) {
        empty = false;
      }
      style.setText(text);
    }
    if (evaluateImage) {
      const image = evaluateImage(context);
      if (image) {
        empty = false;
      }
      style.setImage(image);
    }
    if (evaluateZIndex) {
      style.setZIndex(evaluateZIndex(context));
    }
    if (empty) {
      return null;
    }
    return style;
  };
}

/**
 * @typedef {function(EvaluationContext):Fill|null} FillEvaluator
 */

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} prefix The property prefix.
 * @param {ParsingContext} context The parsing context.
 * @return {FillEvaluator?} A function that evaluates to a fill.
 */
function buildFill(flatStyle, prefix, context) {
  let evaluateColor;
  if (prefix + 'fill-pattern-src' in flatStyle) {
    evaluateColor = patternEvaluator(flatStyle, prefix + 'fill-', context);
  } else {
    if (flatStyle[prefix + 'fill-color'] === 'none') {
      // avoids hit detection
      return (context) => null;
    }

    evaluateColor = colorLikeEvaluator(
      flatStyle,
      prefix + 'fill-color',
      context,
    );
  }
  if (!evaluateColor) {
    return null;
  }

  const fill = new Fill();
  return function (context) {
    const color = evaluateColor(context);
    if (color === NO_COLOR) {
      return null;
    }
    fill.setColor(color);
    return fill;
  };
}

/**
 * @typedef {function(EvaluationContext):Stroke|null} StrokeEvaluator
 */

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} prefix The property prefix.
 * @param {ParsingContext} context The parsing context.
 * @return {StrokeEvaluator?} A function the evaluates to a stroke.
 */
function buildStroke(flatStyle, prefix, context) {
  const evaluateWidth = numberEvaluator(
    flatStyle,
    prefix + 'stroke-width',
    context,
  );

  const evaluateColor = colorLikeEvaluator(
    flatStyle,
    prefix + 'stroke-color',
    context,
  );

  if (!evaluateWidth && !evaluateColor) {
    return null;
  }

  const evaluateLineCap = stringEvaluator(
    flatStyle,
    prefix + 'stroke-line-cap',
    context,
  );

  const evaluateLineJoin = stringEvaluator(
    flatStyle,
    prefix + 'stroke-line-join',
    context,
  );

  const evaluateLineDash = numberArrayEvaluator(
    flatStyle,
    prefix + 'stroke-line-dash',
    context,
  );

  const evaluateLineDashOffset = numberEvaluator(
    flatStyle,
    prefix + 'stroke-line-dash-offset',
    context,
  );

  const evaluateMiterLimit = numberEvaluator(
    flatStyle,
    prefix + 'stroke-miter-limit',
    context,
  );

  const stroke = new Stroke();
  return function (context) {
    if (evaluateColor) {
      const color = evaluateColor(context);
      if (color === NO_COLOR) {
        return null;
      }
      stroke.setColor(color);
    }

    if (evaluateWidth) {
      stroke.setWidth(evaluateWidth(context));
    }

    if (evaluateLineCap) {
      const lineCap = evaluateLineCap(context);
      if (lineCap !== 'butt' && lineCap !== 'round' && lineCap !== 'square') {
        throw new Error('Expected butt, round, or square line cap');
      }
      stroke.setLineCap(lineCap);
    }

    if (evaluateLineJoin) {
      const lineJoin = evaluateLineJoin(context);
      if (
        lineJoin !== 'bevel' &&
        lineJoin !== 'round' &&
        lineJoin !== 'miter'
      ) {
        throw new Error('Expected bevel, round, or miter line join');
      }
      stroke.setLineJoin(lineJoin);
    }

    if (evaluateLineDash) {
      stroke.setLineDash(evaluateLineDash(context));
    }

    if (evaluateLineDashOffset) {
      stroke.setLineDashOffset(evaluateLineDashOffset(context));
    }

    if (evaluateMiterLimit) {
      stroke.setMiterLimit(evaluateMiterLimit(context));
    }

    return stroke;
  };
}

/**
 * @typedef {function(EvaluationContext):Text} TextEvaluator
 */

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {ParsingContext} context The parsing context.
 * @return {TextEvaluator?} A function that evaluates to a text symbolizer.
 */
function buildText(flatStyle, context) {
  const prefix = 'text-';

  // Currently, an Array<string> may be used for rich text support.  This doesn't
  // work with our expression syntax where arrays of strings are interpreted as
  // call expressions.  To support rich text, we could add a 'strings' operator
  // where all the following arguments would be string values.
  const evaluateValue = stringEvaluator(flatStyle, prefix + 'value', context);
  if (!evaluateValue) {
    return null;
  }

  const evaluateFill = buildFill(flatStyle, prefix, context);

  const evaluateBackgroundFill = buildFill(
    flatStyle,
    prefix + 'background-',
    context,
  );

  const evaluateStroke = buildStroke(flatStyle, prefix, context);

  const evaluateBackgroundStroke = buildStroke(
    flatStyle,
    prefix + 'background-',
    context,
  );

  const evaluateFont = stringEvaluator(flatStyle, prefix + 'font', context);

  const evaluateMaxAngle = numberEvaluator(
    flatStyle,
    prefix + 'max-angle',
    context,
  );

  const evaluateOffsetX = numberEvaluator(
    flatStyle,
    prefix + 'offset-x',
    context,
  );

  const evaluateOffsetY = numberEvaluator(
    flatStyle,
    prefix + 'offset-y',
    context,
  );

  const evaluateOverflow = booleanEvaluator(
    flatStyle,
    prefix + 'overflow',
    context,
  );

  const evaluatePlacement = stringEvaluator(
    flatStyle,
    prefix + 'placement',
    context,
  );

  const evaluateRepeat = numberEvaluator(flatStyle, prefix + 'repeat', context);

  const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);

  const evaluateRotateWithView = booleanEvaluator(
    flatStyle,
    prefix + 'rotate-with-view',
    context,
  );

  const evaluateRotation = numberEvaluator(
    flatStyle,
    prefix + 'rotation',
    context,
  );

  const evaluateAlign = stringEvaluator(flatStyle, prefix + 'align', context);

  const evaluateJustify = stringEvaluator(
    flatStyle,
    prefix + 'justify',
    context,
  );

  const evaluateBaseline = stringEvaluator(
    flatStyle,
    prefix + 'baseline',
    context,
  );

  const evaluateKeepUpright = booleanEvaluator(
    flatStyle,
    prefix + 'keep-upright',
    context,
  );

  const evaluatePadding = numberArrayEvaluator(
    flatStyle,
    prefix + 'padding',
    context,
  );

  // The following properties are not currently settable
  const declutterMode = optionalDeclutterMode(
    flatStyle,
    prefix + 'declutter-mode',
  );

  const text = new Text({declutterMode});

  return function (context) {
    text.setText(evaluateValue(context));

    if (evaluateFill) {
      text.setFill(evaluateFill(context));
    }

    if (evaluateBackgroundFill) {
      text.setBackgroundFill(evaluateBackgroundFill(context));
    }

    if (evaluateStroke) {
      text.setStroke(evaluateStroke(context));
    }

    if (evaluateBackgroundStroke) {
      text.setBackgroundStroke(evaluateBackgroundStroke(context));
    }

    if (evaluateFont) {
      text.setFont(evaluateFont(context));
    }

    if (evaluateMaxAngle) {
      text.setMaxAngle(evaluateMaxAngle(context));
    }

    if (evaluateOffsetX) {
      text.setOffsetX(evaluateOffsetX(context));
    }

    if (evaluateOffsetY) {
      text.setOffsetY(evaluateOffsetY(context));
    }

    if (evaluateOverflow) {
      text.setOverflow(evaluateOverflow(context));
    }

    if (evaluatePlacement) {
      const placement = evaluatePlacement(context);
      if (placement !== 'point' && placement !== 'line') {
        throw new Error('Expected point or line for text-placement');
      }
      text.setPlacement(placement);
    }

    if (evaluateRepeat) {
      text.setRepeat(evaluateRepeat(context));
    }

    if (evaluateScale) {
      text.setScale(evaluateScale(context));
    }

    if (evaluateRotateWithView) {
      text.setRotateWithView(evaluateRotateWithView(context));
    }

    if (evaluateRotation) {
      text.setRotation(evaluateRotation(context));
    }

    if (evaluateAlign) {
      const textAlign = evaluateAlign(context);
      if (
        textAlign !== 'left' &&
        textAlign !== 'center' &&
        textAlign !== 'right' &&
        textAlign !== 'end' &&
        textAlign !== 'start'
      ) {
        throw new Error(
          'Expected left, right, center, start, or end for text-align',
        );
      }
      text.setTextAlign(textAlign);
    }

    if (evaluateJustify) {
      const justify = evaluateJustify(context);
      if (justify !== 'left' && justify !== 'right' && justify !== 'center') {
        throw new Error('Expected left, right, or center for text-justify');
      }
      text.setJustify(justify);
    }

    if (evaluateBaseline) {
      const textBaseline = evaluateBaseline(context);
      if (
        textBaseline !== 'bottom' &&
        textBaseline !== 'top' &&
        textBaseline !== 'middle' &&
        textBaseline !== 'alphabetic' &&
        textBaseline !== 'hanging'
      ) {
        throw new Error(
          'Expected bottom, top, middle, alphabetic, or hanging for text-baseline',
        );
      }
      text.setTextBaseline(textBaseline);
    }

    if (evaluatePadding) {
      text.setPadding(evaluatePadding(context));
    }

    if (evaluateKeepUpright) {
      text.setKeepUpright(evaluateKeepUpright(context));
    }

    return text;
  };
}

/**
 * @typedef {function(EvaluationContext):import("../../style/Image.js").default} ImageEvaluator
 */

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {ParsingContext} context The parsing context.
 * @return {ImageEvaluator?} A function that evaluates to an image symbolizer.
 */
function buildImage(flatStyle, context) {
  if ('icon-src' in flatStyle) {
    return buildIcon(flatStyle, context);
  }

  if ('shape-points' in flatStyle) {
    return buildShape(flatStyle, context);
  }

  if ('circle-radius' in flatStyle) {
    return buildCircle(flatStyle, context);
  }

  return null;
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {ParsingContext} context The parsing context.
 * @return {ImageEvaluator} A function that evaluates to an image symbolizer.
 */
function buildIcon(flatStyle, context) {
  const prefix = 'icon-';

  // required property
  const srcName = prefix + 'src';
  const src = requireString(flatStyle[srcName], srcName);

  // settable properties
  const evaluateAnchor = coordinateEvaluator(
    flatStyle,
    prefix + 'anchor',
    context,
  );

  const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);

  const evaluateOpacity = numberEvaluator(
    flatStyle,
    prefix + 'opacity',
    context,
  );

  const evaluateDisplacement = coordinateEvaluator(
    flatStyle,
    prefix + 'displacement',
    context,
  );

  const evaluateRotation = numberEvaluator(
    flatStyle,
    prefix + 'rotation',
    context,
  );

  const evaluateRotateWithView = booleanEvaluator(
    flatStyle,
    prefix + 'rotate-with-view',
    context,
  );

  // the remaining symbolizer properties are not currently settable
  const anchorOrigin = optionalIconOrigin(flatStyle, prefix + 'anchor-origin');
  const anchorXUnits = optionalIconAnchorUnits(
    flatStyle,
    prefix + 'anchor-x-units',
  );
  const anchorYUnits = optionalIconAnchorUnits(
    flatStyle,
    prefix + 'anchor-y-units',
  );
  const color = optionalColorLike(flatStyle, prefix + 'color');
  const crossOrigin = optionalString(flatStyle, prefix + 'cross-origin');
  const offset = optionalNumberArray(flatStyle, prefix + 'offset');
  const offsetOrigin = optionalIconOrigin(flatStyle, prefix + 'offset-origin');
  const width = optionalNumber(flatStyle, prefix + 'width');
  const height = optionalNumber(flatStyle, prefix + 'height');
  const size = optionalSize(flatStyle, prefix + 'size');
  const declutterMode = optionalDeclutterMode(
    flatStyle,
    prefix + 'declutter-mode',
  );

  const icon = new Icon({
    src,
    anchorOrigin,
    anchorXUnits,
    anchorYUnits,
    color,
    crossOrigin,
    offset,
    offsetOrigin,
    height,
    width,
    size,
    declutterMode,
  });

  return function (context) {
    if (evaluateOpacity) {
      icon.setOpacity(evaluateOpacity(context));
    }

    if (evaluateDisplacement) {
      icon.setDisplacement(evaluateDisplacement(context));
    }

    if (evaluateRotation) {
      icon.setRotation(evaluateRotation(context));
    }

    if (evaluateRotateWithView) {
      icon.setRotateWithView(evaluateRotateWithView(context));
    }

    if (evaluateScale) {
      icon.setScale(evaluateScale(context));
    }

    if (evaluateAnchor) {
      icon.setAnchor(evaluateAnchor(context));
    }
    return icon;
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {ParsingContext} context The parsing context.
 * @return {ImageEvaluator} A function that evaluates to an icon symbolizer.
 */
function buildShape(flatStyle, context) {
  const prefix = 'shape-';

  // required property
  const pointsName = prefix + 'points';
  const radiusName = prefix + 'radius';
  const points = requireNumber(flatStyle[pointsName], pointsName);
  const radius = requireNumber(flatStyle[radiusName], radiusName);

  // settable properties
  const evaluateFill = buildFill(flatStyle, prefix, context);
  const evaluateStroke = buildStroke(flatStyle, prefix, context);
  const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);
  const evaluateDisplacement = coordinateEvaluator(
    flatStyle,
    prefix + 'displacement',
    context,
  );
  const evaluateRotation = numberEvaluator(
    flatStyle,
    prefix + 'rotation',
    context,
  );
  const evaluateRotateWithView = booleanEvaluator(
    flatStyle,
    prefix + 'rotate-with-view',
    context,
  );

  // the remaining properties are not currently settable
  const radius2 = optionalNumber(flatStyle, prefix + 'radius2');
  const angle = optionalNumber(flatStyle, prefix + 'angle');
  const declutterMode = optionalDeclutterMode(
    flatStyle,
    prefix + 'declutter-mode',
  );

  const shape = new RegularShape({
    points,
    radius,
    radius2,
    angle,
    declutterMode,
  });

  return function (context) {
    if (evaluateFill) {
      shape.setFill(evaluateFill(context));
    }
    if (evaluateStroke) {
      shape.setStroke(evaluateStroke(context));
    }
    if (evaluateDisplacement) {
      shape.setDisplacement(evaluateDisplacement(context));
    }
    if (evaluateRotation) {
      shape.setRotation(evaluateRotation(context));
    }
    if (evaluateRotateWithView) {
      shape.setRotateWithView(evaluateRotateWithView(context));
    }
    if (evaluateScale) {
      shape.setScale(evaluateScale(context));
    }

    return shape;
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {ParsingContext} context The parsing context.
 * @return {ImageEvaluator} A function that evaluates to a circle symbolizer.
 */
function buildCircle(flatStyle, context) {
  const prefix = 'circle-';

  // settable properties
  const evaluateFill = buildFill(flatStyle, prefix, context);
  const evaluateStroke = buildStroke(flatStyle, prefix, context);
  const evaluateRadius = numberEvaluator(flatStyle, prefix + 'radius', context);
  const evaluateScale = sizeLikeEvaluator(flatStyle, prefix + 'scale', context);
  const evaluateDisplacement = coordinateEvaluator(
    flatStyle,
    prefix + 'displacement',
    context,
  );
  const evaluateRotation = numberEvaluator(
    flatStyle,
    prefix + 'rotation',
    context,
  );
  const evaluateRotateWithView = booleanEvaluator(
    flatStyle,
    prefix + 'rotate-with-view',
    context,
  );

  // the remaining properties are not currently settable
  const declutterMode = optionalDeclutterMode(
    flatStyle,
    prefix + 'declutter-mode',
  );

  const circle = new Circle({
    radius: 5, // this is arbitrary, but required - the evaluated radius is used below
    declutterMode,
  });

  return function (context) {
    if (evaluateRadius) {
      circle.setRadius(evaluateRadius(context));
    }
    if (evaluateFill) {
      circle.setFill(evaluateFill(context));
    }
    if (evaluateStroke) {
      circle.setStroke(evaluateStroke(context));
    }
    if (evaluateDisplacement) {
      circle.setDisplacement(evaluateDisplacement(context));
    }
    if (evaluateRotation) {
      circle.setRotation(evaluateRotation(context));
    }
    if (evaluateRotateWithView) {
      circle.setRotateWithView(evaluateRotateWithView(context));
    }
    if (evaluateScale) {
      circle.setScale(evaluateScale(context));
    }

    return circle;
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} name The property name.
 * @param {ParsingContext} context The parsing context.
 * @return {import('../../expr/cpu.js').NumberEvaluator|undefined} The expression evaluator or undefined.
 */
function numberEvaluator(flatStyle, name, context) {
  if (!(name in flatStyle)) {
    return undefined;
  }
  const evaluator = buildExpression(flatStyle[name], NumberType, context);
  return function (context) {
    return requireNumber(evaluator(context), name);
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} name The property name.
 * @param {ParsingContext} context The parsing context.
 * @return {import('../../expr/cpu.js').StringEvaluator?} The expression evaluator.
 */
function stringEvaluator(flatStyle, name, context) {
  if (!(name in flatStyle)) {
    return null;
  }
  const evaluator = buildExpression(flatStyle[name], StringType, context);
  return function (context) {
    return requireString(evaluator(context), name);
  };
}

function patternEvaluator(flatStyle, prefix, context) {
  const srcEvaluator = stringEvaluator(
    flatStyle,
    prefix + 'pattern-src',
    context,
  );
  const offsetEvaluator = sizeEvaluator(
    flatStyle,
    prefix + 'pattern-offset',
    context,
  );
  const patternSizeEvaluator = sizeEvaluator(
    flatStyle,
    prefix + 'pattern-size',
    context,
  );
  const colorEvaluator = colorLikeEvaluator(
    flatStyle,
    prefix + 'color',
    context,
  );
  return function (context) {
    return {
      src: srcEvaluator(context),
      offset: offsetEvaluator && offsetEvaluator(context),
      size: patternSizeEvaluator && patternSizeEvaluator(context),
      color: colorEvaluator && colorEvaluator(context),
    };
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} name The property name.
 * @param {ParsingContext} context The parsing context.
 * @return {import('../../expr/cpu.js').BooleanEvaluator?} The expression evaluator.
 */
function booleanEvaluator(flatStyle, name, context) {
  if (!(name in flatStyle)) {
    return null;
  }
  const evaluator = buildExpression(flatStyle[name], BooleanType, context);
  return function (context) {
    const value = evaluator(context);
    if (typeof value !== 'boolean') {
      throw new Error(`Expected a boolean for ${name}`);
    }
    return value;
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} name The property name.
 * @param {ParsingContext} context The parsing context.
 * @return {import('../../expr/cpu.js').ColorLikeEvaluator?} The expression evaluator.
 */
function colorLikeEvaluator(flatStyle, name, context) {
  if (!(name in flatStyle)) {
    return null;
  }
  const evaluator = buildExpression(flatStyle[name], ColorType, context);
  return function (context) {
    return requireColorLike(evaluator(context), name);
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} name The property name.
 * @param {ParsingContext} context The parsing context.
 * @return {import('../../expr/cpu.js').NumberArrayEvaluator?} The expression evaluator.
 */
function numberArrayEvaluator(flatStyle, name, context) {
  if (!(name in flatStyle)) {
    return null;
  }
  const evaluator = buildExpression(flatStyle[name], NumberArrayType, context);
  return function (context) {
    return requireNumberArray(evaluator(context), name);
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} name The property name.
 * @param {ParsingContext} context The parsing context.
 * @return {import('../../expr/cpu.js').CoordinateEvaluator?} The expression evaluator.
 */
function coordinateEvaluator(flatStyle, name, context) {
  if (!(name in flatStyle)) {
    return null;
  }
  const evaluator = buildExpression(flatStyle[name], NumberArrayType, context);
  return function (context) {
    const array = requireNumberArray(evaluator(context), name);
    if (array.length !== 2) {
      throw new Error(`Expected two numbers for ${name}`);
    }
    return array;
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} name The property name.
 * @param {ParsingContext} context The parsing context.
 * @return {import('../../expr/cpu.js').SizeEvaluator?} The expression evaluator.
 */
function sizeEvaluator(flatStyle, name, context) {
  if (!(name in flatStyle)) {
    return null;
  }
  const evaluator = buildExpression(flatStyle[name], NumberArrayType, context);
  return function (context) {
    return requireSize(evaluator(context), name);
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} name The property name.
 * @param {ParsingContext} context The parsing context.
 * @return {import('../../expr/cpu.js').SizeLikeEvaluator?} The expression evaluator.
 */
function sizeLikeEvaluator(flatStyle, name, context) {
  if (!(name in flatStyle)) {
    return null;
  }
  const evaluator = buildExpression(
    flatStyle[name],
    NumberArrayType | NumberType,
    context,
  );
  return function (context) {
    return requireSizeLike(evaluator(context), name);
  };
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} property The symbolizer property.
 * @return {number|undefined} A number or undefined.
 */
function optionalNumber(flatStyle, property) {
  const value = flatStyle[property];
  if (value === undefined) {
    return undefined;
  }
  if (typeof value !== 'number') {
    throw new Error(`Expected a number for ${property}`);
  }
  return value;
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} property The symbolizer property.
 * @return {import("../../size.js").Size|undefined} A size or undefined.
 */
function optionalSize(flatStyle, property) {
  const encoded = flatStyle[property];
  if (encoded === undefined) {
    return undefined;
  }
  if (typeof encoded === 'number') {
    return toSize(encoded);
  }
  if (!Array.isArray(encoded)) {
    throw new Error(`Expected a number or size array for ${property}`);
  }
  if (
    encoded.length !== 2 ||
    typeof encoded[0] !== 'number' ||
    typeof encoded[1] !== 'number'
  ) {
    throw new Error(`Expected a number or size array for ${property}`);
  }
  return encoded;
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} property The symbolizer property.
 * @return {string|undefined} A string or undefined.
 */
function optionalString(flatStyle, property) {
  const encoded = flatStyle[property];
  if (encoded === undefined) {
    return undefined;
  }
  if (typeof encoded !== 'string') {
    throw new Error(`Expected a string for ${property}`);
  }
  return encoded;
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} property The symbolizer property.
 * @return {import("../../style/Icon.js").IconOrigin|undefined} An icon origin or undefined.
 */
function optionalIconOrigin(flatStyle, property) {
  const encoded = flatStyle[property];
  if (encoded === undefined) {
    return undefined;
  }
  if (
    encoded !== 'bottom-left' &&
    encoded !== 'bottom-right' &&
    encoded !== 'top-left' &&
    encoded !== 'top-right'
  ) {
    throw new Error(
      `Expected bottom-left, bottom-right, top-left, or top-right for ${property}`,
    );
  }
  return encoded;
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} property The symbolizer property.
 * @return {import("../../style/Icon.js").IconAnchorUnits|undefined} Icon anchor units or undefined.
 */
function optionalIconAnchorUnits(flatStyle, property) {
  const encoded = flatStyle[property];
  if (encoded === undefined) {
    return undefined;
  }
  if (encoded !== 'pixels' && encoded !== 'fraction') {
    throw new Error(`Expected pixels or fraction for ${property}`);
  }
  return encoded;
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} property The symbolizer property.
 * @return {Array<number>|undefined} An array of numbers or undefined.
 */
function optionalNumberArray(flatStyle, property) {
  const encoded = flatStyle[property];
  if (encoded === undefined) {
    return undefined;
  }
  return requireNumberArray(encoded, property);
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} property The symbolizer property.
 * @return {import('../../style/Style.js').DeclutterMode} Icon declutter mode.
 */
function optionalDeclutterMode(flatStyle, property) {
  const encoded = flatStyle[property];
  if (encoded === undefined) {
    return undefined;
  }
  if (typeof encoded !== 'string') {
    throw new Error(`Expected a string for ${property}`);
  }
  if (encoded !== 'declutter' && encoded !== 'obstacle' && encoded !== 'none') {
    throw new Error(`Expected declutter, obstacle, or none for ${property}`);
  }
  return encoded;
}

/**
 * @param {FlatStyle} flatStyle The flat style.
 * @param {string} property The symbolizer property.
 * @return {string|Array<number>|undefined} A string or an array of color values or undefined.
 */
function optionalColorLike(flatStyle, property) {
  const encoded = flatStyle[property];
  if (encoded === undefined) {
    return undefined;
  }
  return requireColorLike(encoded, property);
}

/**
 * @param {any} value The value.
 * @param {string} property The property.
 * @return {Array<number>} An array of numbers.
 */
function requireNumberArray(value, property) {
  if (!Array.isArray(value)) {
    throw new Error(`Expected an array for ${property}`);
  }
  const length = value.length;
  for (let i = 0; i < length; ++i) {
    if (typeof value[i] !== 'number') {
      throw new Error(`Expected an array of numbers for ${property}`);
    }
  }
  return value;
}

/**
 * @param {any} value The value.
 * @param {string} property The property.
 * @return {string} A string.
 */
function requireString(value, property) {
  if (typeof value !== 'string') {
    throw new Error(`Expected a string for ${property}`);
  }
  return value;
}

/**
 * @param {any} value The value.
 * @param {string} property The property.
 * @return {number} A number.
 */
function requireNumber(value, property) {
  if (typeof value !== 'number') {
    throw new Error(`Expected a number for ${property}`);
  }
  return value;
}

/**
 * @param {any} value The value.
 * @param {string} property The property.
 * @return {Array<number>|string} A color.
 */
function requireColorLike(value, property) {
  if (typeof value === 'string') {
    return value;
  }
  const array = requireNumberArray(value, property);
  const length = array.length;
  if (length < 3 || length > 4) {
    throw new Error(`Expected a color with 3 or 4 values for ${property}`);
  }
  return array;
}

/**
 * @param {any} value The value.
 * @param {string} property The property.
 * @return {Array<number>} A number or an array of two numbers.
 */
function requireSize(value, property) {
  const size = requireNumberArray(value, property);
  if (size.length !== 2) {
    throw new Error(`Expected an array of two numbers for ${property}`);
  }
  return size;
}

/**
 * @param {any} value The value.
 * @param {string} property The property.
 * @return {number|Array<number>} A number or an array of two numbers.
 */
function requireSizeLike(value, property) {
  if (typeof value === 'number') {
    return value;
  }
  return requireSize(value, property);
}
