import {
  create,
  factory,
  all,
  MathJsFunctionName,
  fractionDependencies,
  addDependencies,
  divideDependencies,
  formatDependencies,
} from 'mathjs';
import * as assert from 'assert';
import { expectTypeOf } from 'expect-type'

// This file serves a dual purpose:
// 1) examples of how to use math.js in TypeScript
// 2) tests for the TypeScript declarations provided by math.js

/*
Basic usage examples
*/
{
  const math = create(all);

  // functions and constants
  math.round(math.e, 3);
  math.round(100.123, 3);
  math.atan2(3, -3) / math.pi;
  math.log(10000, 10);
  math.sqrt(-4);
  math.pow([[-1, 2], [3, 1]], 2);
  const angle = 0.2;
  math.add(math.pow(math.sin(angle), 2), math.pow(math.cos(angle), 2));

  // std and variance check
  math.std(1, 2, 3)
  math.std([1, 2, 3])
  math.std([1, 2, 3], "biased")
  math.std([1,2, 3], 0, "biased")
  math.std([[1,2,3], [4,5,6]], 1, "unbiased")
  math.std([[1,2,3], [4,5,6]], 1, "uncorrected")
  math.variance(1, 2, 3)
  math.variance([1, 2, 3])
  math.variance([1, 2, 3], "biased")
  math.variance([1,2, 3], 0, "biased")
  math.variance([[1,2,3], [4,5,6]], 1, "unbiased")
  math.variance([[1,2,3], [4,5,6]], 1, "uncorrected")

  // std and variance on chain
  math.chain([1, 2, 3]).std("unbiased")
  math.chain([[1, 2, 3], [4, 5, 6]]).std(0, "biased").std(0, "uncorrected")
  math.chain([[1, 2, 3], [4, 5, 6]]).std(0, "biased").std(0, "uncorrected")
  math.chain([1, 2, 3]).std("unbiased")
  math.chain([[1, 2, 3], [4, 5, 6]]).variance(0, "biased")
  math.chain([[1, 2, 3], [4, 5, 6]]).variance(1, "uncorrected").variance("unbiased")


  // expressions
  math.evaluate('1.2 * (2 + 4.5)');

  // chained operations
  const a = math.chain(3).add(4).multiply(2).done();
  assert.strictEqual(a, 14);

  // mixed use of different data types in functions
  assert.deepStrictEqual(math.add(4, [5, 6]), [9, 10]); // number + Array
  assert.deepStrictEqual(math.multiply(math.unit('5 mm'), 3), math.unit('15 mm')); // Unit * number
  assert.deepStrictEqual(math.subtract([2, 3, 4], 5), [-3, -2, -1]); // Array - number
  assert.deepStrictEqual(math.add(math.matrix([2, 3]), [4, 5]), math.matrix([6, 8])); // Matrix + Array

  // narrowed type inference
  const b: math.Matrix = math.add(math.matrix([2]), math.matrix([3]));
  const c: math.Matrix = math.subtract(math.matrix([4]), math.matrix([5]));
}

/*
Bignumbers examples
*/
{
  // configure the default type of numbers as BigNumbers
  const math = create(all, {
    number: 'BigNumber',
    precision: 20,
  });

  {
    assert.deepStrictEqual(math.add(math.bignumber(0.1), math.bignumber(0.2)), math.bignumber(0.3));
    assert.deepStrictEqual(math.divide(math.bignumber(0.3), math.bignumber(0.2)), math.bignumber(1.5));
  }
}

/*
Chaining examples
*/
{
  const math = create(all, {});
  const a = math.chain(3).add(4).multiply(2).done();
  assert.strictEqual(a, 14);

  // Another example, calculate square(sin(pi / 4))
  const b = math.chain(math.pi).divide(4).sin().square().done();

  // toString will return a string representation of the chain's value
  const chain = math.chain(2).divide(3);
  const str: string = chain.toString();
  assert.strictEqual(str, "0.6666666666666666");

  chain.valueOf();

  // the function subset can be used to get or replace sub matrices
  const array = [
    [1, 2],
    [3, 4],
  ];
  const v = math.chain(array).subset(math.index(1, 0)).done();
  assert.strictEqual(v, 3);

  const m = math.chain(array).subset(math.index(0, 0), 8).multiply(3).done();

  // filtering
  assert.deepStrictEqual(
    math
      .chain([-1, 0, 1.1, 2, 3, 1000])
      .filter(math.isPositive)
      .filter(math.isInteger)
      .filter((n) => n !== 1000)
      .done(),
    [2, 3]
  );
}

/*
Simplify examples
*/
{
  const math = create(all);

  math.simplify("2 * 1 * x ^ (2 - 1)");
  math.simplify("2 * 3 * x", { x: 4 });

  const f = math.parse("2 * 1 * x ^ (2 - 1)");
  math.simplify(f);

  math.simplify("0.4 * x", {}, { exactFractions: true });
  math.simplify("0.4 * x", {}, { exactFractions: false });
}

/*
Complex numbers examples
*/
{
  const math = create(all, {});
  const a = math.complex(2, 3);
  // create a complex number by providing a string with real and complex parts
  const b = math.complex('3 - 7i');

  // read the real and complex parts of the complex number
  {
    const x: number = a.re;
    const y: number = a.im;

    // adjust the complex value
    a.re = 5;
  }

  // clone a complex value
  {
    const clone = a.clone();
  }

  // perform operations with complex numbers
  {
    math.add(a, b);
    math.multiply(a, b);
    math.sin(a);
  }

  // create a complex number from polar coordinates
  {
    const p: math.PolarCoordinates = { r: math.sqrt(2), phi: math.pi / 4 };
    const c: math.Complex = math.complex(p);
  }

  // get polar coordinates of a complex number
  {
    const p: math.PolarCoordinates = math.complex(3, 4).toPolar();
  }
}

/*
Expressions examples
*/
{
  const math = create(all, {});
  // evaluate expressions
  {
    math.evaluate('sqrt(3^2 + 4^2)');
  }

  // evaluate multiple expressions at once
  {
    math.evaluate(['f = 3', 'g = 4', 'f * g']);
  }

  // get content of a parenthesis node
  {
    const node = math.parse('(1)');
    if (node.type !== 'ParenthesisNode') {
      throw Error(`expected ParenthesisNode, got ${node.type}`);
    }
    const innerNode = node.content;
  }

  // scope can contain both variables and functions
  {
    const scope = { hello: (name: string) => `hello, ${name}!` };
    assert.strictEqual(math.evaluate('hello("hero")', scope), "hello, hero!");
  }

  // define a function as an expression
  {
    const scope: any = {
      a: 3,
      b: 4,
    };
    const f = math.evaluate('f(x) = x ^ a', scope);
    f(2);
    scope.f(2);
  }

  {
    const node2 = math.parse('x^a');
    const code2: math.EvalFunction = node2.compile();
    node2.toString();
  }

  // 3. using function math.compile
  // parse an expression
  {
    // provide a scope for the variable assignment
    const code2 = math.compile('a = a + 3');
    const scope = { a: 7 };
    code2.evaluate(scope);
  }
  // 4. using a parser
  const parser = math.parser();

  // get and set variables and functions
  {
    assert.strictEqual(parser.evaluate('x = 7 / 2'), 3.5);
    assert.strictEqual(parser.evaluate('x + 3'), 6.5);
    parser.evaluate('f(x, y) = x^y'); // f(x, y)
    assert.strictEqual(parser.evaluate('f(2, 3)'), 8);

    const x = parser.get('x');
    const f = parser.get('f');
    const y = parser.getAll();
    const g = f(3, 3);

    parser.set('h', 500);
    parser.set('hello', (name: string) => `hello, ${name}!`);
  }

  // clear defined functions and variables
  parser.clear();
}

/*
Fractions examples
*/
{
  // configure the default type of numbers as Fractions
  const math = create(all, {
    number: 'Fraction',
  });

  const x = math.fraction(0.125);
  const y = math.fraction('1/3');
  math.fraction(2, 3);

  math.add(x, y);
  math.divide(x, y);

  // output formatting
  const a = math.fraction('2/3');
}

/*
Matrices examples
*/
{
  const math = create(all, {});

  // create matrices and arrays. a matrix is just a wrapper around an Array,
  // providing some handy utilities.
  const a: math.Matrix = math.matrix([1, 4, 9, 16, 25]);
  const b: math.Matrix = math.matrix(math.ones([2, 3]));
  b.size();

  // the Array data of a Matrix can be retrieved using valueOf()
  const array = a.valueOf();

  // Matrices can be cloned
  const clone: math.Matrix = a.clone();

  // perform operations with matrices
  math.sqrt(a);
  math.factorial(a);

  // create and manipulate matrices. Arrays and Matrices can be used mixed.
  {
    const a = [
      [1, 2],
      [3, 4],
    ];
    const b: math.Matrix = math.matrix([
      [5, 6],
      [1, 1],
    ]);

    b.subset(math.index(1, [0, 1]), [[7, 8]]);
    const c = math.multiply(a, b);
    const f: math.Matrix = math.matrix([1, 0]);
    const d: math.Matrix = f.subset(math.index(1));
  }

  // get a sub matrix
  {
    const a: math.Matrix = math.diag(math.range(1, 4));
    a.subset(math.index([1, 2], [1, 2]));
    const b: math.Matrix = math.range(1, 6);
    b.subset(math.index(math.range(1, 4)));
  }

  // resize a multi dimensional matrix
  {
    const a = math.matrix();
    a.resize([2, 2, 2], 0);
    a.size();
    a.resize([2, 2]);
    a.size();
  }

  // can set a subset of a matrix to uninitialized
  {
    const m = math.matrix();
    m.subset(math.index(2), 6, math.uninitialized);
  }

  // create ranges
  {
    math.range(1, 6);
    math.range(0, 18, 3);
    math.range('2:-1:-3');
    math.factorial(math.range('1:6'));
  }

  // map matrix
  {
    assert.deepStrictEqual(
      math.map([1, 2, 3], function (value) {
        return value * value;
      }),
      [1, 4, 9]
    );
  }

  // filter matrix
  {
    assert.deepStrictEqual(
      math.filter([6, -2, -1, 4, 3], function (x) {
        return x > 0;
      }),
      [6, 4, 3]
    )
    assert.deepStrictEqual(math.filter(['23', 'foo', '100', '55', 'bar'], /[0-9]+/), ["23", "100", "55"]);
  }

  // concat matrix
  {
    assert.deepStrictEqual(math.concat([[0, 1, 2]], [[1, 2, 3]]), [[ 0, 1, 2, 1, 2, 3 ]]);
    assert.deepStrictEqual(math.concat([[0, 1, 2]], [[1, 2, 3]], 0), [[ 0, 1, 2 ], [ 1, 2, 3 ]]);
  }

  // Matrix is available as a constructor for instanceof checks
  {
    assert.strictEqual(math.matrix([1, 2, 3]) instanceof math.Matrix, true)
  }
}

/*
Sparse matrices examples
*/
{
  const math = create(all, {});

  // create a sparse matrix
  const a = math.identity(1000, 1000, 'sparse');

  // do operations with a sparse matrix
  const b = math.multiply(a, a);
  const c = math.multiply(b, math.complex(2, 2));
  const d = math.matrix([0, 1]);
  const e = math.transpose(d);
  const f = math.multiply(e, d);
}

/*
Units examples
*/
{
  const math = create(all, {});

  // units can be created by providing a value and unit name, or by providing
  // a string with a valued unit.
  const a = math.unit(45, 'cm'); // 450 mm
  const b = math.unit('0.1m'); // 100 mm
  const c = math.unit(b)

  // creating units
  math.createUnit('foo');
  math.createUnit('furlong', '220 yards');
  math.createUnit('furlong', '220 yards', { override: true });
  math.createUnit('testunit', { definition: '0.555556 kelvin', offset: 459.67 });
  math.createUnit('testunit', { definition: '0.555556 kelvin', offset: 459.67 }, { override: true });
  math.createUnit('knot', { definition: '0.514444 m/s', aliases: ['knots', 'kt', 'kts'] });
  math.createUnit('knot', { definition: '0.514444 m/s', aliases: ['knots', 'kt', 'kts'] }, { override: true });
  math.createUnit(
    'knot',
    {
      definition: '0.514444 m/s',
      aliases: ['knots', 'kt', 'kts'],
      prefixes: 'long',
    },
    { override: true }
  );
  math.createUnit(
    {
      foo2: {
        prefixes: 'long',
      },
      bar: '40 foo',
      baz: {
        definition: '1 bar/hour',
        prefixes: 'long',
      },
    },
    {
      override: true,
    }
  );
  // use Unit as definition
  math.createUnit('c', { definition: b });
  math.createUnit('c', { definition: b }, { override: true });

  // units can be added, subtracted, and multiplied or divided by numbers and by other units
  math.add(a, b);
  math.multiply(b, 2);
  math.divide(math.unit('1 m'), math.unit('1 s'));
  math.pow(math.unit('12 in'), 3);

  // units can be converted to a specific type, or to a number
  b.to('cm');
  math.to(b, 'inch');
  b.toNumber('cm');
  math.number(b, 'cm');

  // the expression parser supports units too
  math.evaluate('2 inch to cm');

  // units can be converted to SI
  math.unit('1 inch').toSI();

  // units can be split into other units
  math.unit('1 m').splitUnit(['ft', 'in']);
}

/*
Expression tree examples
*/
{
  const math = create(all, {});

  // Filter an expression tree
  const node: math.MathNode = math.parse('x^2 + x/4 + 3*y');
  const filtered: math.MathNode[] = node.filter((node: math.MathNode) => node.type === 'SymbolNode' && node.name === 'x');

  const arr: string[] = filtered.map((node: math.MathNode) => node.toString());

  // Traverse an expression tree
  const node1: math.MathNode = math.parse('3 * x + 2');
  node1.traverse((node: math.MathNode, path: string, parent: math.MathNode) => {
    switch (node.type) {
      case 'OperatorNode':
        return node.type === 'OperatorNode';
      case 'ConstantNode':
        return node.type === 'ConstantNode';
      case 'SymbolNode':
        return node.type === 'SymbolNode';
      default:
        return;
    }
  });
}

/*
Function floor examples
*/
{
  const math = create(all, {});

  // number input
  assert.strictEqual(math.floor(3.2), 3);
  assert.strictEqual(math.floor(-4.2), -5);

  // number input
  // roundoff result to 2 decimals
  assert.strictEqual(math.floor(3.212, 2), 3.21);
  assert.strictEqual(math.floor(-4.212, 2), -4.22);

  // Complex input
  const c = math.complex(3.24, -2.71);
  assert.deepStrictEqual(math.floor(c), math.complex(3, -3));
  assert.deepStrictEqual(math.floor(c, 1), math.complex(3.2, -2.8));

  //array input
  assert.deepStrictEqual(math.floor([3.2, 3.8, -4.7]), [3, 3, -5]);
  assert.deepStrictEqual(math.floor([3.21, 3.82, -4.71], 1), [3.2, 3.8, -4.8]);
}


/*
JSON serialization/deserialization
*/
{
  const math = create(all, {});

  const data = {
    bigNumber: math.bignumber('1.5'),
  };
  const stringified = JSON.stringify(data);
  const parsed = JSON.parse(stringified, math.reviver);
  assert.deepStrictEqual(parsed.bigNumber, math.bignumber('1.5'));
}

/*
Extend functionality with import
 */

declare module 'mathjs' {
  interface MathJsStatic {
    testFun(): number;
    value: number;
  }
}

{
  const math = create(all, {});
  const testFun = () => 5;

  math.import(
    {
      testFun,
      value: 10,
    },
    {}
  );

  math.testFun();

  const a = math.value * 2;
}

/*
Renamed functions from v5 => v6
 */
{
  const math = create(all, {});
  math.typeOf(1);
  math.variance([1, 2, 3, 4]);
  math.evaluate('1 + 2');

  // chained operations
  math.chain(3).typeOf().done();
  math.chain([1, 2, 3]).variance().done();
  math.chain('1 + 2').evaluate().done();
}

/*
Factory Test
 */
{
  // create a factory function
  const name = 'negativeSquare';
  const dependencies: MathJsFunctionName[] = ['multiply', 'unaryMinus'];
  const createNegativeSquare = factory(name, dependencies, (injected) => {
    const { multiply, unaryMinus } = injected;
    return function negativeSquare(x: number): number {
      return unaryMinus(multiply(x, x));
    };
  });

  // create an instance of the function yourself:
  const multiply = (a: number, b: number) => a * b;
  const unaryMinus = (a: number) => -a;
  const negativeSquare = createNegativeSquare({ multiply, unaryMinus });
  negativeSquare(3);
}

/**
 * Dependency map typing test from mathjs official document:
 * https://mathjs.org/docs/custom_bundling.html#using-just-a-few-functions
 */
{
  const config = {
    // optionally, you can specify configuration
  };

  // Create just the functions we need
  const { fraction, add, divide, format } = create(
    {
      fractionDependencies,
      addDependencies,
      divideDependencies,
      formatDependencies,
    },
    config
  );

  // Use the created functions
  const a = fraction(1, 3);
  const b = fraction(3, 7);
  const c = add(a, b);
  const d = divide(a, b);
  assert.strictEqual(format(c), "16/21");
  assert.strictEqual(format(d), "7/9");
}

/**
 * Custom parsing functions
 * https://mathjs.org/docs/expressions/customization.html#customize-supported-characters
 */
{
  const math = create(all, {});
  const isAlphaOriginal = math.parse.isAlpha;
  math.parse.isAlpha = (c, cPrev, cNext) => {
    return isAlphaOriginal(c, cPrev, cNext) || c === "\u260E";
  };

  // now we can use the \u260E (phone) character in expressions
  const result = math.evaluate("\u260Efoo", { "\u260Efoo": 42 });
  assert.strictEqual(result, 42);
}

/**
 * Util functions
 * https://mathjs.org/docs/reference/functions.html#utils-functions
 */
{
  const math = create(all, {});

  // hasNumericValue function
  assert.    strictEqual(math.hasNumericValue(2),                    true);
  assert.    strictEqual(math.hasNumericValue('2'),                  true);
  assert.    strictEqual(math.isNumeric('2'),                        false);
  assert.    strictEqual(math.hasNumericValue(0),                    true);
  assert.    strictEqual(math.hasNumericValue(math.bignumber(500)),  true);
  assert.deepStrictEqual(math.hasNumericValue([2.3, 'foo', false]),  [true, false, true]);
  assert.    strictEqual(math.hasNumericValue(math.fraction(4)),     true);
  assert.    strictEqual(math.hasNumericValue(math.complex('2-4i')), false);
}

/**
 * src/util/is functions
 */
{
  const math = create(all, {});

  type IsFunc = (x: unknown) => boolean;
  const isFuncs: IsFunc[] = [
    math.isNumber,
    math.isBigNumber,
    math.isComplex,
    math.isFraction,
    math.isUnit,
    math.isString,
    math.isArray,
    math.isMatrix,
    math.isCollection,
    math.isDenseMatrix,
    math.isSparseMatrix,
    math.isRange,
    math.isIndex,
    math.isBoolean,
    math.isResultSet,
    math.isHelp,
    math.isFunction,
    math.isDate,
    math.isRegExp,
    math.isObject,
    math.isNull,
    math.isUndefined,
    math.isAccessorNode,
    math.isArrayNode,
    math.isAssignmentNode,
    math.isBlockNode,
    math.isConditionalNode,
    math.isConstantNode,
    math.isFunctionAssignmentNode,
    math.isFunctionNode,
    math.isIndexNode,
    math.isNode,
    math.isObjectNode,
    math.isOperatorNode,
    math.isParenthesisNode,
    math.isRangeNode,
    math.isSymbolNode,
    math.isChain
  ]

  isFuncs.forEach(f => {
    const result = f(1);
    const isResultBoolean = result === true || result === false;
    assert.ok(isResultBoolean);
  })

  // Check guards do type refinement

  let x: unknown

  if (math.isNumber(x)) {
    expectTypeOf(x).toMatchTypeOf<number>()
  }
  if (math.isBigNumber(x)) {
    expectTypeOf(x).toMatchTypeOf<math.BigNumber>()
  }
  if (math.isComplex(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Complex>()
  }
  if (math.isFraction(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Fraction>()
  }
  if (math.isUnit(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Unit>()
  }
  if (math.isString(x)) {
    expectTypeOf(x).toMatchTypeOf<string>()
  }
  if (math.isArray(x)) {
    expectTypeOf(x).toMatchTypeOf<unknown[]>()
  }
  if (math.isMatrix(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Matrix>()
  }
  if (math.isDenseMatrix(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Matrix>()
  }
  if (math.isSparseMatrix(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Matrix>()
  }
  if (math.isIndex(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Index>()
  }
  if (math.isBoolean(x)) {
    expectTypeOf(x).toMatchTypeOf<boolean>()
  }
  if (math.isHelp(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Help>()
  }
  if (math.isDate(x)) {
    expectTypeOf(x).toMatchTypeOf<Date>()
  }
  if (math.isRegExp(x)) {
    expectTypeOf(x).toMatchTypeOf<RegExp>()
  }
  if (math.isNull(x)) {
    expectTypeOf(x).toMatchTypeOf<null>()
  }
  if (math.isUndefined(x)) {
    expectTypeOf(x).toMatchTypeOf<undefined>()
  }

  if (math.isAccessorNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.AccessorNode>()
  }
  if (math.isArrayNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ArrayNode>()
  }
  if (math.isAssignmentNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.AssignmentNode>()
  }
  if (math.isBlockNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.BlockNode>()
  }
  if (math.isConditionalNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ConditionalNode>()
  }
  if (math.isConstantNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ConstantNode>()
  }
  if (math.isFunctionAssignmentNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.FunctionAssignmentNode>()
  }
  if (math.isFunctionNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.FunctionNode>()
  }
  if (math.isIndexNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.IndexNode>()
  }
  if (math.isNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.MathNodeCommon>()
  }
  if (math.isNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.MathNodeCommon>()
  }
  if (math.isObjectNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ObjectNode>()
  }
  if (math.isOperatorNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.OperatorNode>()
  }
  if (math.isParenthesisNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ParenthesisNode>()
  }
  if (math.isRangeNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.RangeNode>()
  }
  if (math.isSymbolNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.SymbolNode>()
  }
  if (math.isChain(x)) {
    expectTypeOf(x).toMatchTypeOf<math.MathJsChain>()
  }
}

/*
Probability function examples
*/
{
  const math = create(all, {});

  expectTypeOf(math.lgamma(1.5)).toMatchTypeOf<number>()
  expectTypeOf(math.lgamma(math.complex(1.5, -1.5))).toMatchTypeOf<math.Complex>()
}
