/**
 * @module ol/expr/gpu
 */
import {asArray} from '../color.js';
import {Uniforms} from '../renderer/webgl/TileLayer.js';
import {toSize} from '../size.js';
import PaletteTexture from '../webgl/PaletteTexture.js';
import {
  BooleanType,
  CallExpression,
  ColorType,
  NumberArrayType,
  NumberType,
  Ops,
  SizeType,
  StringType,
  isType,
  parse,
  typeName,
} from './expression.js';

/**
 * @param {string} operator Operator
 * @param {CompilationContext} context Compilation context
 * @return {string} A function name based on the operator, unique in the given context
 */
function computeOperatorFunctionName(operator, context) {
  return `operator_${operator}_${Object.keys(context.functions).length}`;
}

/**
 * Will return the number as a float with a dot separator, which is required by GLSL.
 * @param {number} v Numerical value.
 * @return {string} The value as string.
 */
export function numberToGlsl(v) {
  const s = v.toString();
  return s.includes('.') ? s : s + '.0';
}

/**
 * Will return the number array as a float with a dot separator, concatenated with ', '.
 * @param {Array<number>} array Numerical values array.
 * @return {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
 */
export function arrayToGlsl(array) {
  if (array.length < 2 || array.length > 4) {
    throw new Error(
      '`formatArray` can only output `vec2`, `vec3` or `vec4` arrays.',
    );
  }
  return `vec${array.length}(${array.map(numberToGlsl).join(', ')})`;
}

/**
 * Will normalize and converts to string a `vec4` color array compatible with GLSL.
 * @param {string|import("../color.js").Color} color Color either in string format or [r, g, b, a] array format,
 * with RGB components in the 0..255 range and the alpha component in the 0..1 range.
 * Note that the final array will always have 4 components.
 * @return {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form.
 */
export function colorToGlsl(color) {
  const array = asArray(color);
  const alpha = array.length > 3 ? array[3] : 1;
  return arrayToGlsl([array[0] / 255, array[1] / 255, array[2] / 255, alpha]);
}

/**
 * Normalizes and converts a number or array toa `vec2` array compatible with GLSL.
 * @param {number|import('../size.js').Size} size Size.
 * @return {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form.
 */
export function sizeToGlsl(size) {
  const array = toSize(size);
  return arrayToGlsl(array);
}

/** @type {Object<string, number>} */
const stringToFloatMap = {};
let stringToFloatCounter = 0;

/**
 * Returns a stable equivalent number for the string literal.
 * @param {string} string String literal value
 * @return {number} Number equivalent
 */
export function getStringNumberEquivalent(string) {
  if (!(string in stringToFloatMap)) {
    stringToFloatMap[string] = stringToFloatCounter++;
  }
  return stringToFloatMap[string];
}

/**
 * Returns a stable equivalent number for the string literal, for use in shaders. This number is then
 * converted to be a GLSL-compatible string.
 * Note: with a float precision of `mediump`, the amount of unique strings supported is 16,777,216
 * @param {string} string String literal value
 * @return {string} GLSL-compatible string containing a number
 */
export function stringToGlsl(string) {
  return numberToGlsl(getStringNumberEquivalent(string));
}

/**
 * Get the uniform name given a variable name.
 * @param {string} variableName The variable name.
 * @return {string} The uniform name.
 */
export function uniformNameForVariable(variableName) {
  return 'u_var_' + variableName;
}

/**
 * @typedef {import('./expression.js').ParsingContext} ParsingContext
 */
/**
 *
 * @typedef {import("./expression.js").Expression} Expression
 */
/**
 *
 * @typedef {import("./expression.js").LiteralExpression} LiteralExpression
 */

/**
 * @typedef {Object} CompilationContextProperty
 * @property {string} name Name
 * @property {number} type Resolved property type
 */

/**
 * @typedef {Object} CompilationContextVariable
 * @property {string} name Name
 * @property {number} type Resolved variable type
 */

/**
 * @typedef {Object} CompilationContext
 * @property {Object<string, CompilationContextProperty>} properties The values for properties used in 'get' expressions.
 * @property {Object<string, CompilationContextVariable>} variables The values for variables used in 'var' expressions.
 * @property {Object<string, string>} functions Lookup of functions used by the style.
 * @property {number} [bandCount] Number of bands per pixel.
 * @property {Array<PaletteTexture>} [paletteTextures] List of palettes used by the style.
 * @property {boolean} featureId Whether the feature ID is used in the expression
 * @property {boolean} geometryType Whether the geometry type is used in the expression
 */

/**
 * @return {CompilationContext} A new compilation context.
 */
export function newCompilationContext() {
  return {
    variables: {},
    properties: {},
    functions: {},
    bandCount: 0,
    featureId: false,
    geometryType: false,
  };
}

const GET_BAND_VALUE_FUNC = 'getBandValue';

export const PALETTE_TEXTURE_ARRAY = 'u_paletteTextures';

export const FEATURE_ID_PROPERTY_NAME = 'featureId';
export const GEOMETRY_TYPE_PROPERTY_NAME = 'geometryType';

/**
 * The value `-9999999` will be used to indicate that a property on a feature is not defined, similar to a "no data" value.
 */
export const UNDEFINED_PROP_VALUE = -9999999;

/**
 * @typedef {string} CompiledExpression
 */

/**
 * @typedef {function(CompilationContext, CallExpression, number): string} Compiler
 * Third argument is the expected value types
 */

/**
 * @param {import('./expression.js').EncodedExpression} encoded The encoded expression.
 * @param {number} type The expected type.
 * @param {import('./expression.js').ParsingContext} parsingContext The parsing context.
 * @param {CompilationContext} compilationContext An existing compilation context
 * @return {CompiledExpression} The compiled expression.
 */
export function buildExpression(
  encoded,
  type,
  parsingContext,
  compilationContext,
) {
  const expression = parse(encoded, type, parsingContext);
  return compile(expression, type, compilationContext);
}

/**
 * @param {function(Array<CompiledExpression>, CompilationContext): string} output Function that takes in parsed arguments and returns a string
 * @return {function(CompilationContext, import("./expression.js").CallExpression, number): string} Compiler for the call expression
 */
function createCompiler(output) {
  return (context, expression, type) => {
    const length = expression.args.length;
    const args = new Array(length);
    for (let i = 0; i < length; ++i) {
      args[i] = compile(expression.args[i], type, context);
    }
    return output(args, context);
  };
}

/**
 * @type {Object<string, Compiler>}
 */
const compilers = {
  [Ops.Get]: (context, expression) => {
    const firstArg = /** @type {LiteralExpression} */ (expression.args[0]);
    const propName = /** @type {string} */ (firstArg.value);
    const isExisting = propName in context.properties;
    if (!isExisting) {
      context.properties[propName] = {
        name: propName,
        type: expression.type,
      };
    }
    let result = 'a_prop_' + propName;
    if (isType(expression.type, BooleanType)) {
      result = `(${result} > 0.0)`;
    }
    return result;
  },
  [Ops.Id]: (context) => {
    context.featureId = true;
    return 'a_' + FEATURE_ID_PROPERTY_NAME;
  },
  [Ops.GeometryType]: (context) => {
    context.geometryType = true;
    return 'a_' + GEOMETRY_TYPE_PROPERTY_NAME;
  },
  [Ops.LineMetric]: () => 'currentLineMetric', // this variable is assumed to always be present in shaders, default is 0.
  [Ops.Var]: (context, expression) => {
    const firstArg = /** @type {LiteralExpression} */ (expression.args[0]);
    const varName = /** @type {string} */ (firstArg.value);
    const isExisting = varName in context.variables;
    if (!isExisting) {
      context.variables[varName] = {
        name: varName,
        type: expression.type,
      };
    }
    let result = uniformNameForVariable(varName);
    if (isType(expression.type, BooleanType)) {
      result = `(${result} > 0.0)`;
    }
    return result;
  },
  [Ops.Has]: (context, expression) => {
    const firstArg = /** @type {LiteralExpression} */ (expression.args[0]);
    const propName = /** @type {string} */ (firstArg.value);
    const isExisting = propName in context.properties;
    if (!isExisting) {
      context.properties[propName] = {
        name: propName,
        type: expression.type,
      };
    }
    return `(a_prop_${propName} != ${numberToGlsl(UNDEFINED_PROP_VALUE)})`;
  },
  [Ops.Resolution]: () => 'u_resolution',
  [Ops.Zoom]: () => 'u_zoom',
  [Ops.Time]: () => 'u_time',
  [Ops.Any]: createCompiler((compiledArgs) => `(${compiledArgs.join(` || `)})`),
  [Ops.All]: createCompiler((compiledArgs) => `(${compiledArgs.join(` && `)})`),
  [Ops.Not]: createCompiler(([value]) => `(!${value})`),
  [Ops.Equal]: createCompiler(
    ([firstValue, secondValue]) => `(${firstValue} == ${secondValue})`,
  ),
  [Ops.NotEqual]: createCompiler(
    ([firstValue, secondValue]) => `(${firstValue} != ${secondValue})`,
  ),
  [Ops.GreaterThan]: createCompiler(
    ([firstValue, secondValue]) => `(${firstValue} > ${secondValue})`,
  ),
  [Ops.GreaterThanOrEqualTo]: createCompiler(
    ([firstValue, secondValue]) => `(${firstValue} >= ${secondValue})`,
  ),
  [Ops.LessThan]: createCompiler(
    ([firstValue, secondValue]) => `(${firstValue} < ${secondValue})`,
  ),
  [Ops.LessThanOrEqualTo]: createCompiler(
    ([firstValue, secondValue]) => `(${firstValue} <= ${secondValue})`,
  ),
  [Ops.Multiply]: createCompiler(
    (compiledArgs) => `(${compiledArgs.join(' * ')})`,
  ),
  [Ops.Divide]: createCompiler(
    ([firstValue, secondValue]) => `(${firstValue} / ${secondValue})`,
  ),
  [Ops.Add]: createCompiler((compiledArgs) => `(${compiledArgs.join(' + ')})`),
  [Ops.Subtract]: createCompiler(
    ([firstValue, secondValue]) => `(${firstValue} - ${secondValue})`,
  ),
  [Ops.Clamp]: createCompiler(
    ([value, min, max]) => `clamp(${value}, ${min}, ${max})`,
  ),
  [Ops.Mod]: createCompiler(([value, modulo]) => `mod(${value}, ${modulo})`),
  [Ops.Pow]: createCompiler(([value, power]) => `pow(${value}, ${power})`),
  [Ops.Abs]: createCompiler(([value]) => `abs(${value})`),
  [Ops.Floor]: createCompiler(([value]) => `floor(${value})`),
  [Ops.Ceil]: createCompiler(([value]) => `ceil(${value})`),
  [Ops.Round]: createCompiler(([value]) => `floor(${value} + 0.5)`),
  [Ops.Sin]: createCompiler(([value]) => `sin(${value})`),
  [Ops.Cos]: createCompiler(([value]) => `cos(${value})`),
  [Ops.Atan]: createCompiler(([firstValue, secondValue]) => {
    return secondValue !== undefined
      ? `atan(${firstValue}, ${secondValue})`
      : `atan(${firstValue})`;
  }),
  [Ops.Sqrt]: createCompiler(([value]) => `sqrt(${value})`),
  [Ops.Match]: createCompiler((compiledArgs) => {
    const input = compiledArgs[0];
    const fallback = compiledArgs[compiledArgs.length - 1];
    let result = null;
    for (let i = compiledArgs.length - 3; i >= 1; i -= 2) {
      const match = compiledArgs[i];
      const output = compiledArgs[i + 1];
      result = `(${input} == ${match} ? ${output} : ${result || fallback})`;
    }
    return result;
  }),
  [Ops.Between]: createCompiler(
    ([value, min, max]) => `(${value} >= ${min} && ${value} <= ${max})`,
  ),
  [Ops.Interpolate]: createCompiler(([exponent, input, ...compiledArgs]) => {
    let result = '';
    for (let i = 0; i < compiledArgs.length - 2; i += 2) {
      const stop1 = compiledArgs[i];
      const output1 = result || compiledArgs[i + 1];
      const stop2 = compiledArgs[i + 2];
      const output2 = compiledArgs[i + 3];
      let ratio;
      if (exponent === numberToGlsl(1)) {
        ratio = `(${input} - ${stop1}) / (${stop2} - ${stop1})`;
      } else {
        ratio = `(pow(${exponent}, (${input} - ${stop1})) - 1.0) / (pow(${exponent}, (${stop2} - ${stop1})) - 1.0)`;
      }
      result = `mix(${output1}, ${output2}, clamp(${ratio}, 0.0, 1.0))`;
    }
    return result;
  }),
  [Ops.Case]: createCompiler((compiledArgs) => {
    const fallback = compiledArgs[compiledArgs.length - 1];
    let result = null;
    for (let i = compiledArgs.length - 3; i >= 0; i -= 2) {
      const condition = compiledArgs[i];
      const output = compiledArgs[i + 1];
      result = `(${condition} ? ${output} : ${result || fallback})`;
    }
    return result;
  }),
  [Ops.In]: createCompiler(([needle, ...haystack], context) => {
    const funcName = computeOperatorFunctionName('in', context);
    const tests = [];
    for (let i = 0; i < haystack.length; i += 1) {
      tests.push(`  if (inputValue == ${haystack[i]}) { return true; }`);
    }
    context.functions[funcName] = `bool ${funcName}(float inputValue) {
${tests.join('\n')}
  return false;
}`;
    return `${funcName}(${needle})`;
  }),
  [Ops.Array]: createCompiler(
    (args) => `vec${args.length}(${args.join(', ')})`,
  ),
  [Ops.Color]: createCompiler((compiledArgs) => {
    if (compiledArgs.length === 1) {
      //grayscale
      return `vec4(vec3(${compiledArgs[0]} / 255.0), 1.0)`;
    }
    if (compiledArgs.length === 2) {
      //grayscale with alpha
      return `vec4(vec3(${compiledArgs[0]} / 255.0), ${compiledArgs[1]})`;
    }
    const rgb = compiledArgs.slice(0, 3).map((color) => `${color} / 255.0`);
    if (compiledArgs.length === 3) {
      return `vec4(${rgb.join(', ')}, 1.0)`;
    }
    const alpha = compiledArgs[3];
    return `vec4(${rgb.join(', ')}, ${alpha})`;
  }),
  [Ops.Band]: createCompiler(([band, xOffset, yOffset], context) => {
    if (!(GET_BAND_VALUE_FUNC in context.functions)) {
      let ifBlocks = '';
      const bandCount = context.bandCount || 1;
      for (let i = 0; i < bandCount; i++) {
        const colorIndex = Math.floor(i / 4);
        let bandIndex = i % 4;
        if (i === bandCount - 1 && bandIndex === 1) {
          // LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha
          bandIndex = 3;
        }
        const textureName = `${Uniforms.TILE_TEXTURE_ARRAY}[${colorIndex}]`;
        ifBlocks += `  if (band == ${i + 1}.0) {
    return texture2D(${textureName}, v_textureCoord + vec2(dx, dy))[${bandIndex}];
  }
`;
      }

      context.functions[GET_BAND_VALUE_FUNC] =
        `float getBandValue(float band, float xOffset, float yOffset) {
  float dx = xOffset / ${Uniforms.TEXTURE_PIXEL_WIDTH};
  float dy = yOffset / ${Uniforms.TEXTURE_PIXEL_HEIGHT};
${ifBlocks}
}`;
    }

    return `${GET_BAND_VALUE_FUNC}(${band}, ${xOffset ?? '0.0'}, ${
      yOffset ?? '0.0'
    })`;
  }),
  [Ops.Palette]: (context, expression) => {
    const [index, ...colors] = expression.args;
    const numColors = colors.length;
    const palette = new Uint8Array(numColors * 4);
    for (let i = 0; i < colors.length; i++) {
      const parsedValue = /** @type {string | Array<number>} */ (
        /** @type {LiteralExpression} */ (colors[i]).value
      );
      const color = asArray(parsedValue);
      const offset = i * 4;
      palette[offset] = color[0];
      palette[offset + 1] = color[1];
      palette[offset + 2] = color[2];
      palette[offset + 3] = color[3] * 255;
    }
    if (!context.paletteTextures) {
      context.paletteTextures = [];
    }
    const paletteName = `${PALETTE_TEXTURE_ARRAY}[${context.paletteTextures.length}]`;
    const paletteTexture = new PaletteTexture(paletteName, palette);
    context.paletteTextures.push(paletteTexture);
    const compiledIndex = compile(index, NumberType, context);
    return `texture2D(${paletteName}, vec2((${compiledIndex} + 0.5) / ${numColors}.0, 0.5))`;
  },
  // TODO: unimplemented
  // Ops.Number
  // Ops.String
  // Ops.Coalesce
  // Ops.Concat
  // Ops.ToString
};

/**
 * @param {Expression} expression The expression.
 * @param {number} returnType The expected return type.
 * @param {CompilationContext} context The compilation context.
 * @return {CompiledExpression} The compiled expression
 */
function compile(expression, returnType, context) {
  // operator
  if (expression instanceof CallExpression) {
    const compiler = compilers[expression.operator];
    if (compiler === undefined) {
      throw new Error(
        `No compiler defined for this operator: ${JSON.stringify(
          expression.operator,
        )}`,
      );
    }
    return compiler(context, expression, returnType);
  }

  if ((expression.type & NumberType) > 0) {
    return numberToGlsl(/** @type {number} */ (expression.value));
  }

  if ((expression.type & BooleanType) > 0) {
    return expression.value.toString();
  }

  if ((expression.type & StringType) > 0) {
    return stringToGlsl(expression.value.toString());
  }

  if ((expression.type & ColorType) > 0) {
    return colorToGlsl(
      /** @type {Array<number> | string} */ (expression.value),
    );
  }

  if ((expression.type & NumberArrayType) > 0) {
    return arrayToGlsl(/** @type {Array<number>} */ (expression.value));
  }

  if ((expression.type & SizeType) > 0) {
    return sizeToGlsl(
      /** @type {number|import('../size.js').Size} */ (expression.value),
    );
  }

  throw new Error(
    `Unexpected expression ${expression.value} (expected type ${typeName(
      returnType,
    )})`,
  );
}
