import * as Commands from './../../ast/commands';
import { Types } from './../../typeSystem/types';
import * as Parsers from "./../../typeSystem/parsers";
import { Decimal } from 'decimal.js';
import { MultiType } from '../../typeSystem/multiType';
import { ArrayType } from '../../typeSystem/array_type';
import { Modes } from '../modes';
import { StoreValue } from '../store/value/store_value';
import { ProcessorErrorFactory } from '../error/processorErrorFactory';

/**
 * sin
 * cos
 * tan
 * sqrt
 * pow
 * log
 * abs
 * negate
 * invert
 * max
 * min
 */

function convertToRadians (degrees) {
  return degrees.times(Decimal.acos(-1)).div(180);
}

export function createSinFun () {
   const sinFun = async (sto, _) => {
     const x = sto.applyStore('x');
     const angle = x.get().mod(360);
     let result = null;
     if(angle.eq(90)) {
       result = new Decimal(1);
     } else if (angle.eq(180)) {
      result = new Decimal(0);
     } else if (angle.eq(270)) {
       result = new Decimal(-1);
     } else {
       result = Decimal.sin(convertToRadians(angle));
     }
     const temp = new StoreValue(Types.REAL, result);
     sto.insertStore("$", temp);
     sto.mode = Modes.RETURN;
     return sto;
   };

  const block = new Commands.CommandBlock([], [new Commands.SysCall(sinFun)]);
  const func = new Commands.Function(
    "$sin",
    Types.REAL,
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      )
    ],
    block
  );
  return func;
}

export function createCosFun () {
  const cosFun = async (sto, _) => {
    const x = sto.applyStore('x');
    const angle = x.get().mod(360);
    let result = null;
    if (angle.eq(90)) {
      result = new Decimal(0);
    } else if (angle.eq(180)) {
      result = new Decimal(-1);
    } else if (angle.eq(270)) {
      result = new Decimal(0);
    }
    result = Decimal.cos(convertToRadians(angle));
    const temp = new StoreValue(Types.REAL, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };

  const block = new Commands.CommandBlock([], [new Commands.SysCall(cosFun)]);
  const func = new Commands.Function(
    "$cos",
    Types.REAL,
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      )
    ],
    block
  );
  return func;
}

export function createTanFun () {
  const tanFun = async function (sto, _) {
    const x = sto.applyStore('x');
    const angle = x.get().mod(360);
    if(angle.eq(90) || angle.eq(270)) {
      throw ProcessorErrorFactory.undefined_tanget_value(x.get().toNumber(),
        this.function_call_stack.pop());
    }
    const result = Decimal.tan(convertToRadians(angle));
    const temp = new StoreValue(Types.REAL, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };

  const block = new Commands.CommandBlock([], [new Commands.SysCall(tanFun)]);
  const func = new Commands.Function(
    "$tan",
    Types.REAL,
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      )
    ],
    block
  );
  return func;
}

export function createSqrtFun () {
  const sqrtFun = async function (sto, _) {
    const x = sto.applyStore('x');
    if(x.get().isNeg()) {
      return Promise.reject(ProcessorErrorFactory.negative_sqrt_value(this.function_call_stack.pop()));
    }
    const result = x.get().sqrt();
    const temp = new StoreValue(Types.REAL, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };

  const block = new Commands.CommandBlock([], [new Commands.SysCall(sqrtFun)]);
  const func = new Commands.Function(
    "$sqrt",
    Types.REAL,
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      )
    ],
    block
  );
  return func;
}

export function createPowFun () {
  const powFun = async (sto, _) => {
    const x = sto.applyStore('x');
    const y = sto.applyStore('y');
    const result = x.get().pow(y.get());
    const temp = new StoreValue(Types.REAL, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };

  const block = new Commands.CommandBlock([], [new Commands.SysCall(powFun)]);
  const func = new Commands.Function(
    "$pow",
    Types.REAL,
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      ),
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "y",
        false
      )
    ],
    block
  );
  return func;
}

export function createLogFun () {
  const logFun = async function (sto, _) {
    const x = sto.applyStore('x');
    if (x.get().isNegative()) {
      return Promise.reject(
        ProcessorErrorFactory.negative_log_value(this.function_call_stack.pop())
      );
    }
    const result = Decimal.log10(x.get());
    const temp = new StoreValue(Types.REAL, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };

  const block = new Commands.CommandBlock([], [new Commands.SysCall(logFun)]);
  const func = new Commands.Function(
    "$log",
    Types.REAL,
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      )
    ],
    block
  );
  return func;
}

export function createAbsFun () {
  const absFun = async (sto, _) => {
    const x = sto.applyStore('x');
    const result = x.get().abs();
    const temp = new StoreValue(x.type, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };

  const block = new Commands.CommandBlock([], [new Commands.SysCall(absFun)]);
  const func = new Commands.Function(
    "$abs",
    new MultiType([Types.INTEGER, Types.REAL]),
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      )
    ],
    block
  );
  return func;
}

export function createNegateFun () {
  const negateFun = async (sto, _) => {
    const x = sto.applyStore('x');
    const result = x.get().negated();
    const temp = new StoreValue(x.type, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };

  const block = new Commands.CommandBlock(
    [],
    [new Commands.SysCall(negateFun)]
  );
  const func = new Commands.Function(
    "$negate",
    new MultiType([Types.INTEGER, Types.REAL]),
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      )
    ],
    block
  );
  return func;
}

export function createInvertFun () {
  const invertFun = async (sto, _) => {
    const x = sto.applyStore('x');
    const result = Parsers.toReal(1).dividedBy(x.get());
    const temp = new StoreValue(Types.REAL, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };

  const block = new Commands.CommandBlock(
    [],
    [new Commands.SysCall(invertFun)]
  );
  const func = new Commands.Function(
    "$invert",
    Types.REAL,
    [
      new Commands.FormalParameter(
        new MultiType([Types.INTEGER, Types.REAL]),
        "x",
        false
      )
    ],
    block
  );
  return func;
}

export function createMaxFun () {
  const maxFun = async (sto, _) => {
    const x = sto.applyStore('x');
    const numbers = x.get().map(sto_addrs => sto_addrs.get());
    const result = Decimal.max(...numbers);
    const temp = new StoreValue(x.type.innerType, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };
  const paramType = new ArrayType(
    new MultiType([Types.INTEGER, Types.REAL]),
    1
  );
  const block = new Commands.CommandBlock([], [new Commands.SysCall(maxFun)]);
  const func = new Commands.Function(
    "$max",
    new MultiType([Types.INTEGER, Types.REAL]),
    [new Commands.FormalParameter(paramType, "x", false)],
    block
  );
  return func;
}

export function createMinFun () {
  const minFun = async (sto, _) => {
    const x = sto.applyStore('x');
    const numbers = x.get().map(sto_addrs => sto_addrs.get());
    const result = Decimal.min(...numbers);
    const temp = new StoreValue(x.type.innerType, result);
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };
  const paramType = new ArrayType(
    new MultiType([Types.INTEGER, Types.REAL]),
    1
  );
  const block = new Commands.CommandBlock([], [new Commands.SysCall(minFun)]);
  const func = new Commands.Function(
    "$min",
    new MultiType([Types.INTEGER, Types.REAL]),
    [new Commands.FormalParameter(paramType, "x", false)],
    block
  );
  return func;
}

let seed = Date.now();
export function createRandFun () {
  const a = 16807;
  const m = 2147483647;
  const c = 12345;
  const randFun = async (sto, _) => {
    seed = (a * seed + c) % m;
    const val = seed / (m - 1);
    const temp = new StoreValue(Types.REAL, new Decimal(val));
    sto.insertStore("$", temp);
    sto.mode = Modes.RETURN;
    return sto;
  };
  const block = new Commands.CommandBlock([], [new Commands.SysCall(randFun)]);
  const func = new Commands.Function("$rand", Types.REAL, [], block);
  return func;
}

export function createSetSeedFun () {
  const setSeedFun = async (sto, _) => {
    const value = sto.applyStore("x");
    seed = value.get().toNumber();
    sto.mode = Modes.RETURN;
    return sto;
  };
  const block = new Commands.CommandBlock(
    [],
    [new Commands.SysCall(setSeedFun)]
  );
  const func = new Commands.Function(
    "$setSeed",
    Types.VOID,
    [new Commands.FormalParameter(Types.INTEGER, "x", false)],
    block
  );
  return func;
}
