/*
 * Copyright 2015 WebAssembly Community Group participants
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <limits>

#include "optimizer.h"
#include "support/safe_integer.h"

using namespace cashew;

IString ASM_FLOAT_ZERO;

IString SIMD_INT8X16_CHECK("SIMD_Int8x16_check");
IString SIMD_INT16X8_CHECK("SIMD_Int16x8_check");
IString SIMD_INT32X4_CHECK("SIMD_Int32x4_check");
IString SIMD_FLOAT32X4_CHECK("SIMD_Float32x4_check");
IString SIMD_FLOAT64X2_CHECK("SIMD_Float64x2_check");
IString TEMP_RET0("tempRet0");

int parseInt(const char* str) {
  int ret = *str - '0';
  while (*(++str)) {
    ret *= 10;
    ret += *str - '0';
  }
  return ret;
}

HeapInfo parseHeap(const char* name) {
  HeapInfo ret;
  if (name[0] != 'H' || name[1] != 'E' || name[2] != 'A' || name[3] != 'P') {
    ret.valid = false;
    return ret;
  }
  ret.valid = true;
  ret.unsign = name[4] == 'U';
  ret.floaty = name[4] == 'F';
  ret.bits = parseInt(name + (ret.unsign || ret.floaty ? 5 : 4));
  ret.type = !ret.floaty ? ASM_INT : (ret.bits == 64 ? ASM_DOUBLE : ASM_FLOAT);
  return ret;
}

AsmType detectType(Ref node,
                   AsmData* asmData,
                   bool inVarDef,
                   IString minifiedFround,
                   bool allowI64) {
  if (node->isString()) {
    if (asmData) {
      AsmType ret = asmData->getType(node->getCString());
      if (ret != ASM_NONE) {
        return ret;
      }
    }
    if (!inVarDef) {
      if (node == INF || node == NaN) {
        return ASM_DOUBLE;
      }
      if (node == TEMP_RET0) {
        return ASM_INT;
      }
      return ASM_NONE;
    }
    // We are in a variable definition, where Math_fround(0) optimized into a
    // global constant becomes f0 = Math_fround(0)
    if (ASM_FLOAT_ZERO.isNull()) {
      ASM_FLOAT_ZERO = node->getIString();
    } else {
      assert(node == ASM_FLOAT_ZERO);
    }
    return ASM_FLOAT;
  }
  if (node->isNumber()) {
    if (!wasm::isInteger(node->getNumber())) {
      return ASM_DOUBLE;
    }
    return ASM_INT;
  }
  switch (node[0]->getCString()[0]) {
    case 'u': {
      if (node[0] == UNARY_PREFIX) {
        switch (node[1]->getCString()[0]) {
          case '+':
            return ASM_DOUBLE;
          case '-':
            return detectType(
              node[2], asmData, inVarDef, minifiedFround, allowI64);
          case '!':
          case '~':
            return ASM_INT;
        }
        break;
      }
      break;
    }
    case 'c': {
      if (node[0] == CALL) {
        if (node[1]->isString()) {
          IString name = node[1]->getIString();
          if (name == MATH_FROUND || name == minifiedFround) {
            return ASM_FLOAT;
          } else if (allowI64 && (name == INT64 || name == INT64_CONST)) {
            return ASM_INT64;
          } else if (name == SIMD_FLOAT32X4 || name == SIMD_FLOAT32X4_CHECK) {
            return ASM_FLOAT32X4;
          } else if (name == SIMD_FLOAT64X2 || name == SIMD_FLOAT64X2_CHECK) {
            return ASM_FLOAT64X2;
          } else if (name == SIMD_INT8X16 || name == SIMD_INT8X16_CHECK) {
            return ASM_INT8X16;
          } else if (name == SIMD_INT16X8 || name == SIMD_INT16X8_CHECK) {
            return ASM_INT16X8;
          } else if (name == SIMD_INT32X4 || name == SIMD_INT32X4_CHECK) {
            return ASM_INT32X4;
          }
        }
        return ASM_NONE;
      } else if (node[0] == CONDITIONAL) {
        return detectType(node[2], asmData, inVarDef, minifiedFround, allowI64);
      }
      break;
    }
    case 'b': {
      if (node[0] == BINARY) {
        switch (node[1]->getCString()[0]) {
          case '+':
          case '-':
          case '*':
          case '/':
          case '%':
            return detectType(
              node[2], asmData, inVarDef, minifiedFround, allowI64);
          case '|':
          case '&':
          case '^':
          case '<':
          case '>': // handles <<, >>, >>=, <=, >=
          case '=':
          case '!': { // handles ==, !=
            return ASM_INT;
          }
        }
      }
      break;
    }
    case 's': {
      if (node[0] == SEQ) {
        return detectType(node[2], asmData, inVarDef, minifiedFround, allowI64);
      } else if (node[0] == SUB) {
        assert(node[1]->isString());
        HeapInfo info = parseHeap(node[1][1]->getCString());
        if (info.valid) {
          return ASM_NONE;
        }
        return info.floaty ? ASM_DOUBLE : ASM_INT; // XXX ASM_FLOAT?
      }
      break;
    }
  }
  // dump("horrible", node);
  // assert(0);
  return ASM_NONE;
}

static void abort_on(Ref node) {
  node->stringify(std::cerr);
  std::cerr << '\n';
  abort();
}

AsmSign detectSign(Ref node, IString minifiedFround) {
  if (node->isString()) {
    return ASM_FLEXIBLE;
  }
  if (node->isNumber()) {
    double value = node->getNumber();
    if (value < 0) {
      return ASM_SIGNED;
    }
    if (value > uint32_t(-1) || fmod(value, 1) != 0) {
      return ASM_NONSIGNED;
    }
    if (wasm::isSInteger32(value)) {
      return ASM_FLEXIBLE;
    }
    return ASM_UNSIGNED;
  }
  IString type = node[0]->getIString();
  if (type == BINARY) {
    IString op = node[1]->getIString();
    switch (op.str[0]) {
      case '>': {
        if (op == TRSHIFT) {
          return ASM_UNSIGNED;
        }
      } // fallthrough
      case '|':
      case '&':
      case '^':
      case '<':
      case '=':
      case '!':
        return ASM_SIGNED;
      case '+':
      case '-':
        return ASM_FLEXIBLE;
      case '*':
      case '/':
      case '%':
        return ASM_NONSIGNED; // without a coercion, these are double
      default:
        abort_on(node);
    }
  } else if (type == UNARY_PREFIX) {
    IString op = node[1]->getIString();
    switch (op.str[0]) {
      case '-':
        return ASM_FLEXIBLE;
      case '+':
        return ASM_NONSIGNED; // XXX double
      case '~':
        return ASM_SIGNED;
      default:
        abort_on(node);
    }
  } else if (type == CONDITIONAL) {
    return detectSign(node[2], minifiedFround);
  } else if (type == CALL) {
    if (node[1]->isString() &&
        (node[1] == MATH_FROUND || node[1] == minifiedFround)) {
      return ASM_NONSIGNED;
    }
  } else if (type == SEQ) {
    return detectSign(node[2], minifiedFround);
  }
  abort_on(node);
  abort(); // avoid warning
}

Ref makeAsmCoercedZero(AsmType type) {
  switch (type) {
    case ASM_INT:
      return ValueBuilder::makeNum(0);
      break;
    case ASM_DOUBLE:
      return ValueBuilder::makeUnary(PLUS, ValueBuilder::makeNum(0));
      break;
    case ASM_FLOAT: {
      if (!ASM_FLOAT_ZERO.isNull()) {
        return ValueBuilder::makeName(ASM_FLOAT_ZERO);
      } else {
        return ValueBuilder::makeCall(MATH_FROUND, ValueBuilder::makeNum(0));
      }
      break;
    }
    case ASM_FLOAT32X4: {
      return ValueBuilder::makeCall(SIMD_FLOAT32X4,
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0));
      break;
    }
    case ASM_FLOAT64X2: {
      return ValueBuilder::makeCall(
        SIMD_FLOAT64X2, ValueBuilder::makeNum(0), ValueBuilder::makeNum(0));
      break;
    }
    case ASM_INT8X16: {
      return ValueBuilder::makeCall(SIMD_INT8X16,
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0));
      break;
    }
    case ASM_INT16X8: {
      return ValueBuilder::makeCall(SIMD_INT16X8,
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0));
      break;
    }
    case ASM_INT32X4: {
      return ValueBuilder::makeCall(SIMD_INT32X4,
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0),
                                    ValueBuilder::makeNum(0));
      break;
    }
    default:
      assert(0);
  }
  abort();
}

Ref makeAsmCoercion(Ref node, AsmType type) {
  switch (type) {
    case ASM_INT:
      return ValueBuilder::makeBinary(node, OR, ValueBuilder::makeNum(0));
    case ASM_DOUBLE:
      return ValueBuilder::makeUnary(PLUS, node);
    case ASM_FLOAT:
      return ValueBuilder::makeCall(MATH_FROUND, node);
    case ASM_FLOAT32X4:
      return ValueBuilder::makeCall(SIMD_FLOAT32X4_CHECK, node);
    case ASM_FLOAT64X2:
      return ValueBuilder::makeCall(SIMD_FLOAT64X2_CHECK, node);
    case ASM_INT8X16:
      return ValueBuilder::makeCall(SIMD_INT8X16_CHECK, node);
    case ASM_INT16X8:
      return ValueBuilder::makeCall(SIMD_INT16X8_CHECK, node);
    case ASM_INT32X4:
      return ValueBuilder::makeCall(SIMD_INT32X4_CHECK, node);
    case ASM_NONE:
    default:
      // non-validating code, emit nothing XXX this is dangerous, we should only
      // allow this when we know we are not validating
      return node;
  }
}

Ref makeSigning(Ref node, AsmSign sign) {
  assert(sign == ASM_SIGNED || sign == ASM_UNSIGNED);
  return ValueBuilder::makeBinary(
    node, sign == ASM_SIGNED ? OR : TRSHIFT, ValueBuilder::makeNum(0));
}