/*
 * Copyright The OpenTelemetry Authors
 *
 * 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
 *
 *      https://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.
 */
import { ExponentMapping } from '../../../src/aggregator/exponential-histogram/mapping/ExponentMapping';
import * as ieee754 from '../../../src/aggregator/exponential-histogram/mapping/ieee754';
import * as assert from 'assert';

const MIN_SCALE = -10;
const MAX_SCALE = 0;

describe('ExponentMapping', () => {
  it('maps expected values for scale 0', () => {
    const mapping = new ExponentMapping(0);
    assert.strictEqual(mapping.scale, 0);

    const expectedMappings = [
      // near +inf
      [Number.MAX_VALUE, ieee754.MAX_NORMAL_EXPONENT],
      [Number.MAX_VALUE, 1023],
      [Math.pow(2, 1023), 1022],
      [1.0625 * Math.pow(2, 1023), 1023],
      [Math.pow(2, 1022), 1021],
      [1.0625 * Math.pow(2, 1023), 1023],

      // near 0
      [Math.pow(2, -1022), -1023],
      [1.0625 * Math.pow(2, -1022), -1022],
      [Math.pow(2, -1021), -1022],
      [1.0625 * Math.pow(2, -1021), -1021],

      [Math.pow(2, -1022), ieee754.MIN_NORMAL_EXPONENT - 1],
      [Math.pow(2, -1021), ieee754.MIN_NORMAL_EXPONENT],
      [Number.MIN_VALUE, ieee754.MIN_NORMAL_EXPONENT - 1],

      // near 1
      [4, 1],
      [3, 1],
      [2, 0],
      [1.5, 0],
      [1, -1],
      [0.75, -1],
      [0.51, -1],
      [0.5, -2],
      [0.26, -2],
      [0.25, -3],
      [0.126, -3],
      [0.125, -4],
    ];

    expectedMappings.forEach(([value, expected]) => {
      const result = mapping.mapToIndex(value);
      assert.strictEqual(
        result,
        expected,
        `expected: ${value} to map to: ${expected}, got: ${result}`
      );
    });
  });

  it('maps expected values for min scale', () => {
    const mapping = new ExponentMapping(MIN_SCALE);
    assert.strictEqual(mapping.scale, MIN_SCALE);

    const expectedMappings = [
      [1.000001, 0],
      [1, -1],
      [Number.MAX_VALUE / 2, 0],
      [Number.MAX_VALUE, 0],
      [Number.MIN_VALUE, -1],
      [0.5, -1],
    ];

    expectedMappings.forEach(([value, expected]) => {
      const result = mapping.mapToIndex(value);
      assert.strictEqual(
        result,
        expected,
        `expected: ${value} to map to: ${expected}, got: ${result}`
      );
    });
  });

  it('maps expected values for scale -1', () => {
    const mapping = new ExponentMapping(-1);
    assert.strictEqual(mapping.scale, -1);

    const expectedMappings = [
      [17, 2],
      [16, 1],
      [15, 1],
      [9, 1],
      [8, 1],
      [5, 1],
      [4, 0],
      [3, 0],
      [2, 0],
      [1.5, 0],
      [1, -1],
      [0.75, -1],
      [0.5, -1],
      [0.25, -2],
      [0.2, -2],
      [0.13, -2],
      [0.125, -2],
      [0.1, -2],
      [0.0625, -3],
      [0.06, -3],
    ];

    expectedMappings.forEach(([value, expected]) => {
      const result = mapping.mapToIndex(value);
      assert.strictEqual(
        result,
        expected,
        `expected: ${value} to map to: ${expected}, got: ${result}`
      );
    });
  });

  it('maps expected values for scale -4', () => {
    const mapping = new ExponentMapping(-4);
    assert.strictEqual(mapping.scale, -4);

    const expectedMappings = [
      [0x1, -1],
      [0x10, 0],
      [0x100, 0],
      [0x1000, 0],
      [0x10000, 0], // Base == 2**16
      [0x100000, 1],
      [0x1000000, 1],
      [0x10000000, 1],
      [0x100000000, 1], // == 2**32
      [0x1000000000, 2],
      [0x10000000000, 2],
      [0x100000000000, 2],
      [0x1000000000000, 2], // 2**48
      [0x10000000000000, 3],
      [0x1000000000000000, 3],
      [0x10000000000000000, 3], // 2**64
      [0x100000000000000000, 4],
      [0x1000000000000000000, 4],
      [0x10000000000000000000, 4],
      [0x100000000000000000000, 4], // 2**80
      [0x1000000000000000000000, 5],

      [1 / 0x1, -1],
      [1 / 0x10, -1],
      [1 / 0x100, -1],
      [1 / 0x1000, -1],
      [1 / 0x10000, -2], // 2**-16
      [1 / 0x100000, -2],
      [1 / 0x1000000, -2],
      [1 / 0x10000000, -2],
      [1 / 0x100000000, -3], // 2**-32
      [1 / 0x1000000000, -3],
      [1 / 0x10000000000, -3],
      [1 / 0x100000000000, -3],
      [1 / 0x1000000000000, -4], // 2**-48
      [1 / 0x10000000000000, -4],
      [1 / 0x100000000000000, -4],
      [1 / 0x1000000000000000, -4],
      [1 / 0x10000000000000000, -5], // 2**-64
      [1 / 0x100000000000000000, -5],

      // Max values
      // below is equivalent to [0x1.FFFFFFFFFFFFFp1023, 63],
      [
        Array.from({ length: 13 }, (_, x) => 0xf * Math.pow(16, -x - 1)).reduce(
          (x, y) => x + y,
          1
        ) * Math.pow(2, 1023),
        63,
      ],
      [Math.pow(2, 1023), 63],
      [Math.pow(2, 1019), 63],
      [Math.pow(2, 1009), 63],
      [Math.pow(2, 1008), 62],
      [Math.pow(2, 1007), 62],
      [Math.pow(2, 1000), 62],
      [Math.pow(2, 993), 62],
      [Math.pow(2, 992), 61],
      [Math.pow(2, 991), 61],

      // Min and subnormal values
      [Math.pow(2, -1074), -64],
      [Math.pow(2, -1073), -64],
      [Math.pow(2, -1072), -64],
      [Math.pow(2, -1057), -64],
      [Math.pow(2, -1056), -64],
      [Math.pow(2, -1041), -64],
      [Math.pow(2, -1040), -64],
      [Math.pow(2, -1025), -64],
      [Math.pow(2, -1024), -64],
      [Math.pow(2, -1023), -64],
      [Math.pow(2, -1022), -64],
      [Math.pow(2, -1009), -64],
      [Math.pow(2, -1008), -64],
      [Math.pow(2, -1007), -63],
      [Math.pow(2, -993), -63],
      [Math.pow(2, -992), -63],
      [Math.pow(2, -991), -62],
      [Math.pow(2, -977), -62],
      [Math.pow(2, -976), -62],
      [Math.pow(2, -975), -61],
    ];

    expectedMappings.forEach(([value, expected]) => {
      const result = mapping.mapToIndex(value);
      assert.strictEqual(
        result,
        expected,
        `expected: ${value} to map to: ${expected}, got: ${result}`
      );
    });
  });

  it('handles max index for all scales', () => {
    for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) {
      const mapping = new ExponentMapping(scale);
      const index = mapping.mapToIndex(Number.MAX_VALUE);
      const maxIndex = ((ieee754.MAX_NORMAL_EXPONENT + 1) >> -scale) - 1;
      assert.strictEqual(
        index,
        maxIndex,
        `expected index: ${index} and ${maxIndex} to be equal for scale: ${scale}`
      );

      const boundary = mapping.lowerBoundary(index);
      assert.strictEqual(boundary, roundedBoundary(scale, maxIndex));

      assert.throws(() => {
        // one larger will overflow
        mapping.lowerBoundary(index + 1);
      });
    }
  });

  it('handles min index for all scales', () => {
    for (let scale = MIN_SCALE; scale <= MAX_SCALE; scale++) {
      const mapping = new ExponentMapping(scale);
      const minIndex = mapping.mapToIndex(ieee754.MIN_VALUE);
      let expectedMinIndex = ieee754.MIN_NORMAL_EXPONENT >> -scale;
      if (ieee754.MIN_NORMAL_EXPONENT % (1 << -scale) === 0) {
        expectedMinIndex--;
      }
      assert.strictEqual(
        minIndex,
        expectedMinIndex,
        `expected expectedMinIndex: ${expectedMinIndex} and ${minIndex} to be equal for scale: ${scale}`
      );

      const boundary = mapping.lowerBoundary(minIndex);
      const expectedBoundary = roundedBoundary(scale, expectedMinIndex);
      assert.strictEqual(boundary, expectedBoundary);

      //one smaller will underflow
      assert.throws(() => {
        mapping.lowerBoundary(minIndex - 1);
      });

      // subnormals map to the min index
      [
        ieee754.MIN_VALUE / 2,
        ieee754.MIN_VALUE / 3,
        Math.pow(2, -1050),
        Math.pow(2, -1073),
        1.0625 * Math.pow(2, -1073),
        Math.pow(2, -1074),
      ].forEach(value => {
        assert.strictEqual(mapping.mapToIndex(value), expectedMinIndex);
      });
    }
  });
});

function roundedBoundary(scale: number, index: number): number {
  let result = Math.pow(2, index);
  for (let i = scale; i < 0; i++) {
    result = result * result;
  }
  return result;
}
