import {LiteralConstructsController as LiteralConstructsController2} from "../LiteralConstructsController";
import {CoreAttribute} from "../../../core/geometry/Attribute";
const NATIVE_MATH_METHODS = [
  "abs",
  "acos",
  "acosh",
  "asin",
  "asinh",
  "atan",
  "atan2",
  "atanh",
  "ceil",
  "cos",
  "cosh",
  "exp",
  "expm1",
  "floor",
  "log",
  "log1p",
  "log2",
  "log10",
  "max",
  "min",
  "pow",
  "round",
  "sign",
  "sin",
  "sinh",
  "sqrt",
  "tan",
  "tanh"
];
const NATIVE_ES6_MATH_METHODS = ["cbrt", "hypot", "log10", "trunc"];
const NATIVE_MATH_METHODS_RENAMED = {
  math_random: "random"
};
const CORE_MATH_METHODS = ["fit", "fit01", "fract", "deg2rad", "rad2deg", "rand", "clamp"];
import {Easing as Easing2} from "../../../core/math/Easing";
const EASING_METHODS = Object.keys(Easing2);
const CORE_STRING_METHODS = ["precision"];
const NATIVE_MATH_CONSTANTS = ["E", "LN2", "LN10", "LOG10E", "LOG2E", "PI", "SQRT1_2", "SQRT2"];
const DIRECT_EXPRESSION_FUNCTIONS = {};
NATIVE_MATH_METHODS.forEach((name) => {
  DIRECT_EXPRESSION_FUNCTIONS[name] = `Math.${name}`;
});
NATIVE_ES6_MATH_METHODS.forEach((name) => {
  DIRECT_EXPRESSION_FUNCTIONS[name] = `Math.${name}`;
});
Object.keys(NATIVE_MATH_METHODS_RENAMED).forEach((name) => {
  const remaped = NATIVE_MATH_METHODS_RENAMED[name];
  DIRECT_EXPRESSION_FUNCTIONS[name] = `Math.${remaped}`;
});
CORE_MATH_METHODS.forEach((name) => {
  DIRECT_EXPRESSION_FUNCTIONS[name] = `Core.Math.${name}`;
});
EASING_METHODS.forEach((name) => {
  DIRECT_EXPRESSION_FUNCTIONS[name] = `Core.Math.Easing.${name}`;
});
CORE_STRING_METHODS.forEach((name) => {
  DIRECT_EXPRESSION_FUNCTIONS[name] = `Core.String.${name}`;
});
const LITERAL_CONSTRUCT = {
  if: LiteralConstructsController2.if
};
const GLOBAL_CONSTANTS = {};
NATIVE_MATH_CONSTANTS.forEach((name) => {
  GLOBAL_CONSTANTS[name] = `Math.${name}`;
});
const QUOTE = "'";
const ARGUMENTS_SEPARATOR = ", ";
const ATTRIBUTE_PREFIX = "@";
import {VARIABLE_PREFIX} from "./_Base";
const PROPERTY_OFFSETS = {
  x: 0,
  y: 1,
  z: 2,
  w: 3,
  r: 0,
  g: 1,
  b: 2
};
import {BaseTraverser} from "./_Base";
import {AttributeRequirementsController as AttributeRequirementsController2} from "../AttributeRequirementsController";
import {CoreMath} from "../../../core/math/_Module";
import {CoreString} from "../../../core/String";
import {Poly as Poly2} from "../../Poly";
import {CoreType} from "../../../core/Type";
export class FunctionGenerator extends BaseTraverser {
  constructor(param) {
    super(param);
    this.param = param;
    this._attribute_requirements_controller = new AttributeRequirementsController2();
    this.methods = [];
    this.method_index = -1;
    this.method_dependencies = [];
    this.immutable_dependencies = [];
  }
  parse_tree(parsed_tree) {
    this.reset();
    if (parsed_tree.error_message == null) {
      try {
        this._attribute_requirements_controller.reset();
        if (parsed_tree.node) {
          const function_main_string = this.traverse_node(parsed_tree.node);
          if (function_main_string && !this.is_errored()) {
            this.function_main_string = function_main_string;
          }
        } else {
          console.warn("no parsed_tree.node");
        }
      } catch (e) {
        console.warn(`error in expression for param ${this.param.fullPath()}`);
        console.warn(e);
      }
      if (this.function_main_string) {
        try {
          this.function = new Function("Core", "param", "methods", "_set_error_from_error", `
					try {
						${this.function_body()}
					} catch(e) {
						_set_error_from_error(e)
						return null;
					}`);
        } catch (e) {
          console.warn(e);
          this.set_error("cannot generate function");
        }
      } else {
        this.set_error("cannot generate function body");
      }
    } else {
      this.set_error("cannot parse expression");
    }
  }
  reset() {
    super.reset();
    this.function_main_string = void 0;
    this.methods = [];
    this.method_index = -1;
    this.function = void 0;
    this.method_dependencies = [];
    this.immutable_dependencies = [];
  }
  function_body() {
    if (this.param.options.is_expression_for_entities()) {
      return `
			const entities = param.expression_controller.entities();
			if(entities){
				return new Promise( async (resolve, reject)=>{
					let entity;
					const entity_callback = param.expression_controller.entity_callback();
					${this._attribute_requirements_controller.assign_attributes_lines()}
					if( ${this._attribute_requirements_controller.attribute_presence_check_line()} ){
						${this._attribute_requirements_controller.assign_arrays_lines()}
						for(let index=0; index < entities.length; index++){
							entity = entities[index];
							result = ${this.function_main_string};
							entity_callback(entity, result);
						}
						resolve()
					} else {
						const error = new Error('attribute not found')
						_set_error_from_error(error)
						reject(error)
					}
				})
			}
			return []`;
    } else {
      return `
			return new Promise( async (resolve, reject)=>{
				try {
					const value = ${this.function_main_string}
					resolve(value)
				} catch(e) {
					_set_error_from_error(e)
					reject()
				}
			})
			`;
    }
  }
  eval_allowed() {
    return this.function != null;
  }
  eval_function() {
    if (this.function) {
      this.clear_error();
      const Core = {
        Math: CoreMath,
        String: CoreString
      };
      const result = this.function(Core, this.param, this.methods, this._set_error_from_error_bound);
      return result;
    }
  }
  traverse_CallExpression(node) {
    const method_arguments = node.arguments.map((arg) => {
      return this.traverse_node(arg);
    });
    const callee = node.callee;
    const method_name = callee.name;
    if (method_name) {
      const literal_contruct = LITERAL_CONSTRUCT[method_name];
      if (literal_contruct) {
        return literal_contruct(method_arguments);
      }
      const arguments_joined = `${method_arguments.join(ARGUMENTS_SEPARATOR)}`;
      const direct_function_name = DIRECT_EXPRESSION_FUNCTIONS[method_name];
      if (direct_function_name) {
        return `${direct_function_name}(${arguments_joined})`;
      }
      const expressionRegister = Poly2.expressionsRegister;
      const indirect_method = expressionRegister.getMethod(method_name);
      if (indirect_method) {
        const path_node = node.arguments[0];
        const function_string = `return ${method_arguments[0]}`;
        let path_argument_function;
        let path_argument = [];
        try {
          path_argument_function = new Function(function_string);
          path_argument = path_argument_function();
        } catch {
        }
        this._create_method_and_dependencies(method_name, path_argument, path_node);
        return `(await methods[${this.method_index}].process_arguments([${arguments_joined}]))`;
      } else {
        const available_methods = expressionRegister.availableMethods().join(", ");
        const message = `method not found (${method_name}), available methods are: ${available_methods}`;
        Poly2.warn(message);
      }
    }
    this.set_error(`unknown method: ${method_name}`);
  }
  traverse_BinaryExpression(node) {
    return `(${this.traverse_node(node.left)} ${node.operator} ${this.traverse_node(node.right)})`;
  }
  traverse_LogicalExpression(node) {
    return `(${this.traverse_node(node.left)} ${node.operator} ${this.traverse_node(node.right)})`;
  }
  traverse_MemberExpression(node) {
    return `${this.traverse_node(node.object)}.${this.traverse_node(node.property)}`;
  }
  traverse_UnaryExpression(node) {
    if (node.operator === ATTRIBUTE_PREFIX) {
      let argument = node.argument;
      let attribute_name;
      let property;
      switch (argument.type) {
        case "Identifier": {
          const argument_identifier = argument;
          attribute_name = argument_identifier.name;
          break;
        }
        case "MemberExpression": {
          const argument_member_expression = argument;
          const attrib_node = argument_member_expression.object;
          const property_node = argument_member_expression.property;
          attribute_name = attrib_node.name;
          property = property_node.name;
          break;
        }
      }
      if (attribute_name) {
        attribute_name = CoreAttribute.remap_name(attribute_name);
        if (attribute_name == "ptnum") {
          return "((entity != null) ? entity.index() : 0)";
        } else {
          const var_attribute_size = this._attribute_requirements_controller.var_attribute_size(attribute_name);
          const var_array = this._attribute_requirements_controller.var_array(attribute_name);
          this._attribute_requirements_controller.add(attribute_name);
          if (property) {
            const property_offset = PROPERTY_OFFSETS[property];
            return `${var_array}[entity.index()*${var_attribute_size}+${property_offset}]`;
          } else {
            return `${var_array}[entity.index()*${var_attribute_size}]`;
          }
        }
      } else {
        console.warn("attribute not found");
        return "";
      }
    } else {
      return `${node.operator}${this.traverse_node(node.argument)}`;
    }
  }
  traverse_Literal(node) {
    return `${node.raw}`;
  }
  traverse_Identifier(node) {
    const identifier_first_char = node.name[0];
    if (identifier_first_char == VARIABLE_PREFIX) {
      const identifier_name_without_dollar_sign = node.name.substr(1);
      const direct_constant_name = GLOBAL_CONSTANTS[identifier_name_without_dollar_sign];
      if (direct_constant_name) {
        return direct_constant_name;
      }
      const method_name = `traverse_Identifier_${identifier_name_without_dollar_sign}`;
      const method = this[method_name];
      if (method) {
        return this[method_name]();
      } else {
        this.set_error(`identifier unknown: ${node.name}`);
      }
    } else {
      return node.name;
    }
  }
  traverse_Identifier_F() {
    this.immutable_dependencies.push(this.param.scene().timeController.graph_node);
    return `param.scene().timeController.frame`;
  }
  traverse_Identifier_FPS() {
    this.immutable_dependencies.push(this.param.scene().timeController.graph_node);
    return `param.scene().timeController.fps`;
  }
  traverse_Identifier_T() {
    this.immutable_dependencies.push(this.param.scene().timeController.graph_node);
    return `param.scene().timeController.time`;
  }
  traverse_Identifier_CH() {
    return `${QUOTE}${this.param.name()}${QUOTE}`;
  }
  traverse_Identifier_CEX() {
    return this._method_centroid("x");
  }
  traverse_Identifier_CEY() {
    return this._method_centroid("y");
  }
  traverse_Identifier_CEZ() {
    return this._method_centroid("z");
  }
  _method_centroid(component) {
    const method_arguments = [0, `${QUOTE}${component}${QUOTE}`];
    const arguments_joined = method_arguments.join(ARGUMENTS_SEPARATOR);
    this._create_method_and_dependencies("centroid", 0);
    return `(await methods[${this.method_index}].process_arguments([${arguments_joined}]))`;
  }
  _create_method_and_dependencies(method_name, path_argument, path_node) {
    const expressionRegister = Poly2.expressionsRegister;
    const method_constructor = expressionRegister.getMethod(method_name);
    if (!method_constructor) {
      const available_methods = expressionRegister.availableMethods();
      const message = `method not found (${method_name}), available methods are: ${available_methods.join(", ")}`;
      this.set_error(message);
      Poly2.warn(message);
      return;
    }
    const method = new method_constructor(this.param);
    this.method_index += 1;
    this.methods[this.method_index] = method;
    if (method.require_dependency()) {
      const method_dependency = method.find_dependency(path_argument);
      if (method_dependency) {
        if (path_node) {
          method_dependency.set_jsep_node(path_node);
        }
        this.method_dependencies.push(method_dependency);
      } else {
        if (path_node && CoreType.isString(path_argument)) {
          this.param.scene().missingExpressionReferencesController.register(this.param, path_node, path_argument);
        }
      }
    }
  }
}
