// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Numbers mutator.
 */

'use strict';

const babelTypes = require('@babel/types');

const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');

const MIN_SAFE_INTEGER = -9007199254740991;
const MAX_SAFE_INTEGER = 9007199254740991;


function isObjectKey(path) {
  return (path.parent &&
          babelTypes.isObjectMember(path.parent) &&
          path.parent.key === path.node);
}

function createRandomNumber(value) {
  // TODO(ochang): Maybe replace with variable.
  const probability = random.random();
  if (probability < 0.01) {
    return babelTypes.numericLiteral(
        random.randInt(MIN_SAFE_INTEGER, MAX_SAFE_INTEGER));
  } else if (probability < 0.06) {
    return common.randomInterestingNumber();
  } else {
    return common.nearbyRandomNumber(value);
  }
}

class NumberMutator extends mutator.Mutator {
  constructor(settings) {
    super();
    this.settings = settings;
  }

  ignore(path) {
    return !random.choose(this.settings.MUTATE_NUMBERS) ||
           common.isInForLoopCondition(path) ||
           common.isInWhileLoop(path);
  }

  randomReplace(path, value, forcePositive=false) {
    const randomNumber = createRandomNumber(value);

    if (forcePositive) {
      randomNumber.value = Math.abs(randomNumber.value);
    }

    this.annotate(
        path.node,
        `Replaced ${value} with ${randomNumber.value}`);

    this.replaceWithSkip(path, randomNumber);
  }

  get visitor() {
    const thisMutator = this;

    return {
      NumericLiteral(path) {
        if (thisMutator.ignore(path)) {
          return;
        }

        // We handle negative unary expressions separately to replace the whole
        // expression below. E.g. -5 is UnaryExpression(-, NumericLiteral(5)).
        if (path.parent && babelTypes.isUnaryExpression(path.parent) &&
            path.parent.operator === '-') {
          return;
        }

        // Enfore positive numbers if the literal is the key of an object
        // property or method. Negative keys cause syntax errors.
        const forcePositive = isObjectKey(path);

        thisMutator.randomReplace(path, path.node.value, forcePositive);
      },
      UnaryExpression(path) {
        if (thisMutator.ignore(path)) {
          return;
        }

        // Handle the case we ignore above.
        if (path.node.operator === '-' &&
            babelTypes.isNumericLiteral(path.node.argument)) {
          thisMutator.randomReplace(path, -path.node.argument.value);
        }
      }
    };
  }
}

module.exports = {
  NumberMutator: NumberMutator,
};