import * as Blockly from "blockly/core";

/**
 * @license Licensed under the Apache License, Version 2.0 (the "License"):
 *          http://www.apache.org/licenses/LICENSE-2.0
 */

/**
 * @fileoverview Generating Arduino code for the Math blocks.
 *
 * TODO: Math on list needs lists to be implemented.
 *       math_constant and math_change needs to be tested in compiler.
 */

/**
 * Generator for a numeric value (X).
 * Arduino code: loop { X }
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_number"] = function (block) {
  // Numeric value.
  var code = parseFloat(block.getFieldValue("NUM"));
  if (code === Infinity) {
    code = "INFINITY";
  } else if (code === -Infinity) {
    code = "-INFINITY";
  }
  return [code, Blockly.Arduino.ORDER_ATOMIC];
};

/**
 * Generator for a basic arithmetic operators (X and Y) and power function
 * (X ^ Y).
 * Arduino code: loop { X operator Y }
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_arithmetic"] = function (block) {
  var OPERATORS = {
    ADD: [" + ", Blockly.Arduino.ORDER_ADDITIVE],
    MINUS: [" - ", Blockly.Arduino.ORDER_ADDITIVE],
    MULTIPLY: [" * ", Blockly.Arduino.ORDER_MULTIPLICATIVE],
    DIVIDE: [" / ", Blockly.Arduino.ORDER_MULTIPLICATIVE],
    POWER: [null, Blockly.Arduino.ORDER_NONE], // Handle power separately.
  };
  var tuple = OPERATORS[block.getFieldValue("OP")];
  var operator = tuple[0];
  var order = tuple[1];
  var argument0 = Blockly.Arduino.valueToCode(block, "A", order) || "0";
  var argument1 = Blockly.Arduino.valueToCode(block, "B", order) || "0";
  var code;
  // Power in C++ requires a special case since it has no operator.
  if (!operator) {
    code = "Math.pow(" + argument0 + ", " + argument1 + ")";
    return [code, Blockly.Arduino.ORDER_UNARY_POSTFIX];
  }
  code = argument0 + operator + argument1;
  return [code, order];
};

/**
 * Generator for math operators that contain a single operand (X).
 * Arduino code: loop { operator(X) }
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_single"] = function (block) {
  var operator = block.getFieldValue("OP");
  var code;
  var arg;
  if (operator === "NEG") {
    // Negation is a special case given its different operator precedents.
    arg =
      Blockly.Arduino.valueToCode(
        block,
        "NUM",
        Blockly.Arduino.ORDER_UNARY_PREFIX
      ) || "0";
    if (arg[0] === "-") {
      // --3 is not legal in C++ in this context.
      arg = " " + arg;
    }
    code = "-" + arg;
    return [code, Blockly.Arduino.ORDER_UNARY_PREFIX];
  }
  if (operator === "ABS" || operator.substring(0, 5) === "ROUND") {
    arg =
      Blockly.Arduino.valueToCode(
        block,
        "NUM",
        Blockly.Arduino.ORDER_UNARY_POSTFIX
      ) || "0";
  } else if (operator === "SIN" || operator === "COS" || operator === "TAN") {
    arg =
      Blockly.Arduino.valueToCode(
        block,
        "NUM",
        Blockly.Arduino.ORDER_MULTIPLICATIVE
      ) || "0";
  } else {
    arg =
      Blockly.Arduino.valueToCode(block, "NUM", Blockly.Arduino.ORDER_NONE) ||
      "0";
  }
  // First, handle cases which generate values that don't need parentheses.
  switch (operator) {
    case "ABS":
      code = "abs(" + arg + ")";
      break;
    case "ROOT":
      code = "sqrt(" + arg + ")";
      break;
    case "LN":
      code = "log(" + arg + ")";
      break;
    case "EXP":
      code = "exp(" + arg + ")";
      break;
    case "POW10":
      code = "pow(10," + arg + ")";
      break;
    case "ROUND":
      code = "round(" + arg + ")";
      break;
    case "ROUNDUP":
      code = "ceil(" + arg + ")";
      break;
    case "ROUNDDOWN":
      code = "floor(" + arg + ")";
      break;
    case "SIN":
      code = "sin(" + arg + " / 180 * Math.PI)";
      break;
    case "COS":
      code = "cos(" + arg + " / 180 * Math.PI)";
      break;
    case "TAN":
      code = "tan(" + arg + " / 180 * Math.PI)";
      break;
    default:
      break;
  }
  if (code) {
    return [code, Blockly.Arduino.ORDER_UNARY_POSTFIX];
  }
  // Second, handle cases which generate values that may need parentheses.
  switch (operator) {
    case "LOG10":
      code = "log(" + arg + ") / log(10)";
      break;
    case "ASIN":
      code = "asin(" + arg + ") / M_PI * 180";
      break;
    case "ACOS":
      code = "acos(" + arg + ") / M_PI * 180";
      break;
    case "ATAN":
      code = "atan(" + arg + ") / M_PI * 180";
      break;
    default:
      throw new Error("Unknown math operator: " + operator);
  }
  return [code, Blockly.Arduino.ORDER_MULTIPLICATIVE];
};

/**
 * Generator for math constants (PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2),
 * INFINITY).
 * Arduino code: loop { constant }
 * TODO: Might need to include "#define _USE_MATH_DEFINES"
 *       The arduino header file already includes math.h
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {string} Completed code.
 */
Blockly.Arduino["math_constant"] = function (block) {
  var CONSTANTS = {
    PI: ["M_PI", Blockly.Arduino.ORDER_UNARY_POSTFIX],
    E: ["M_E", Blockly.Arduino.ORDER_UNARY_POSTFIX],
    GOLDEN_RATIO: ["(1 + sqrt(5)) / 2", Blockly.Arduino.ORDER_MULTIPLICATIVE],
    SQRT2: ["M_SQRT2", Blockly.Arduino.ORDER_UNARY_POSTFIX],
    SQRT1_2: ["M_SQRT1_2", Blockly.Arduino.ORDER_UNARY_POSTFIX],
    INFINITY: ["INFINITY", Blockly.Arduino.ORDER_ATOMIC],
  };
  return CONSTANTS[block.getFieldValue("CONSTANT")];
};

/**
 * Generator for math checks: if a number is even, odd, prime, whole, positive,
 * negative, or if it is divisible by certain number. Returns true or false.
 * Arduino code: complex code, can create external functions.
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_number_property"] = function (block) {
  var number_to_check =
    Blockly.Arduino.valueToCode(
      block,
      "NUMBER_TO_CHECK",
      Blockly.Arduino.ORDER_MULTIPLICATIVE
    ) || "0";
  var dropdown_property = block.getFieldValue("PROPERTY");
  var code;
  if (dropdown_property === "PRIME") {
    var func = [
      "boolean " + Blockly.Arduino.DEF_FUNC_NAME + "(int n) {",
      "  // https://en.wikipedia.org/wiki/Primality_test#Naive_methods",
      "  if (n == 2 || n == 3) {",
      "    return true;",
      "  }",
      "  // False if n is NaN, negative, is 1.",
      "  // And false if n is divisible by 2 or 3.",
      "  if (isnan(n) || (n <= 1) || (n == 1) || (n % 2 == 0) || " +
        "(n % 3 == 0)) {",
      "    return false;",
      "  }",
      "  // Check all the numbers of form 6k +/- 1, up to sqrt(n).",
      "  for (int x = 6; x <= sqrt(n) + 1; x += 6) {",
      "    if (n % (x - 1) == 0 || n % (x + 1) == 0) {",
      "      return false;",
      "    }",
      "  }",
      "  return true;",
      "}",
    ];
    var funcName = Blockly.Arduino.addFunction("mathIsPrime", func.join("\n"));
    Blockly.Arduino.addInclude("math", "#include <math.h>");
    code = funcName + "(" + number_to_check + ")";
    return [code, Blockly.Arduino.ORDER_UNARY_POSTFIX];
  }
  switch (dropdown_property) {
    case "EVEN":
      code = number_to_check + " % 2 == 0";
      break;
    case "ODD":
      code = number_to_check + " % 2 == 1";
      break;
    case "WHOLE":
      Blockly.Arduino.addInclude("math", "#include <math.h>");
      code = "(floor(" + number_to_check + ") == " + number_to_check + ")";
      break;
    case "POSITIVE":
      code = number_to_check + " > 0";
      break;
    case "NEGATIVE":
      code = number_to_check + " < 0";
      break;
    case "DIVISIBLE_BY":
      var divisor =
        Blockly.Arduino.valueToCode(
          block,
          "DIVISOR",
          Blockly.Arduino.ORDER_MULTIPLICATIVE
        ) || "0";
      code = number_to_check + " % " + divisor + " == 0";
      break;
    default:
      break;
  }
  return [code, Blockly.Arduino.ORDER_EQUALITY];
};

/**
 * Generator to add (Y) to a variable (X).
 * If variable X has not been declared before this block it will be declared as
 * a (not initialised) global int, however globals are 0 initialised in C/C++.
 * Arduino code: loop { X += Y; }
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_change"] = function (block) {
  var argument0 =
    Blockly.Arduino.valueToCode(
      block,
      "DELTA",
      Blockly.Arduino.ORDER_ADDITIVE
    ) || "0";
  var varName = Blockly.Arduino.nameDB_.getName(
    block.getFieldValue("VAR"),
    Blockly.Variables.NAME_TYPE
  );
  return varName + " += " + argument0 + ";\n";
};

/** Rounding functions have a single operand. */
Blockly.Arduino["math_round"] = Blockly.Arduino["math_single"];

/** Trigonometry functions have a single operand. */
Blockly.Arduino["math_trig"] = Blockly.Arduino["math_single"];

/**
 * Generator for the math function to a list.
 * Arduino code: ???
 * TODO: List have to be implemented first. Removed from toolbox for now.
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_on_list"] = Blockly.Arduino.noGeneratorCodeInline;

/**
 * Generator for the math modulo function (calculates remainder of X/Y).
 * Arduino code: loop { X % Y }
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_modulo"] = function (block) {
  var argument0 =
    Blockly.Arduino.valueToCode(
      block,
      "DIVIDEND",
      Blockly.Arduino.ORDER_MULTIPLICATIVE
    ) || "0";
  var argument1 =
    Blockly.Arduino.valueToCode(
      block,
      "DIVISOR",
      Blockly.Arduino.ORDER_MULTIPLICATIVE
    ) || "0";
  var code = argument0 + " % " + argument1;
  return [code, Blockly.Arduino.ORDER_MULTIPLICATIVE];
};

/**
 * Generator for clipping a number(X) between two limits (Y and Z).
 * Arduino code: loop { (X < Y ? Y : ( X > Z ? Z : X)) }
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_constrain"] = function (block) {
  // Constrain a number between two limits.
  var argument0 =
    Blockly.Arduino.valueToCode(block, "VALUE", Blockly.Arduino.ORDER_NONE) ||
    "0";
  var argument1 =
    Blockly.Arduino.valueToCode(block, "LOW", Blockly.Arduino.ORDER_NONE) ||
    "0";
  var argument2 =
    Blockly.Arduino.valueToCode(block, "HIGH", Blockly.Arduino.ORDER_NONE) ||
    "0";
  var code =
    "(" +
    argument0 +
    " < " +
    argument1 +
    " ? " +
    argument1 +
    " : ( " +
    argument0 +
    " > " +
    argument2 +
    " ? " +
    argument2 +
    " : " +
    argument0 +
    "))";
  return [code, Blockly.Arduino.ORDER_UNARY_POSTFIX];
};

/**
 * Generator for a random integer between two numbers (X and Y).
 * Arduino code: loop { math_random_int(X, Y); }
 *               and an aditional math_random_int function
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {array} Completed code with order of operation.
 */
Blockly.Arduino["math_random_int"] = function (block) {
  var argument0 =
    Blockly.Arduino.valueToCode(block, "FROM", Blockly.Arduino.ORDER_NONE) ||
    "0";
  var argument1 =
    Blockly.Arduino.valueToCode(block, "TO", Blockly.Arduino.ORDER_NONE) || "0";
  Blockly.Arduino.setupCode_["init_rand"] = "randomSeed(analogRead(0));";
  Blockly.Arduino.functionNames_[
    "math_random_int"
  ] = `int mathRandomInt (int min, int max) {\n
      if (min > max) {
        int temp = min;
        min = max;
        max = temp;
      }
      return min + (rand() % (max - min + 1));
    }
    `;
  var code = `mathRandomInt(${argument0},${argument1});`;
  return [code, Blockly.Arduino.ORDER_ATOMIC];
};

/**
 * Generator for a random float from 0 to 1.
 * Arduino code: loop { (rand() / RAND_MAX) }
 * @param {!Blockly.Block} block Block to generate the code from.
 * @return {string} Completed code.
 */
Blockly.Arduino["math_random_float"] = function (block) {
  return ["(rand() / RAND_MAX)", Blockly.Arduino.ORDER_UNARY_POSTFIX];
};
