// Copyright (C) 2024 The Android Open Source Project
//
// 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.

import {HighPrecisionTime as HPTime} from './high_precision_time';
import {HighPrecisionTimeSpan as HPTimeSpan} from './high_precision_time_span';
import {Time} from './time';

const t = Time.fromRaw;

// Quick 'n' dirty function to convert a string to a HPtime
// Used to make tests more readable
// E.g. '1.3' -> {base: 1, offset: 0.3}
// E.g. '-0.3' -> {base: -1, offset: 0.7}
function hptime(time: string): HPTime {
  const array = time.split('.');
  if (array.length > 2) throw new Error(`Bad time format ${time}`);
  const [base, fractions] = array;
  const negative = time.startsWith('-');
  const numBase = BigInt(base);

  if (fractions) {
    const numFractions = Number(`0.${fractions}`);
    if (negative) {
      return new HPTime(t(numBase - 1n), 1.0 - numFractions);
    } else {
      return new HPTime(t(numBase), numFractions);
    }
  } else {
    return new HPTime(t(numBase));
  }
}

describe('HighPrecisionTimeSpan', () => {
  it('can be constructed from integer time', () => {
    const span = HPTimeSpan.fromTime(t(10n), t(20n));
    expect(span.start.integral).toEqual(10n);
    expect(span.start.fractional).toBeCloseTo(0);
    expect(span.duration).toBeCloseTo(10);
  });

  it('can be constructed from hp times', () => {
    const span = HPTimeSpan.fromHpTimes(hptime('123'), hptime('456'));
    expect(span.start.integral).toEqual(123n);
    expect(span.start.fractional).toBeCloseTo(0);
    expect(span.duration).toBeCloseTo(333);
  });

  it('can be constructed from hp times reversed', () => {
    const span = HPTimeSpan.fromHpTimes(hptime('456'), hptime('123'));
    expect(span.start.integral).toEqual(123n);
    expect(span.start.fractional).toBeCloseTo(0);
    expect(span.duration).toBeCloseTo(333);
  });

  test('end', () => {
    const span = HPTimeSpan.fromTime(t(10n), t(20n));
    expect(span.end.integral).toEqual(20n);
    expect(span.end.fractional).toBeCloseTo(0);
  });

  test('midpoint', () => {
    const span = HPTimeSpan.fromTime(t(10n), t(20n));
    expect(span.midpoint.integral).toEqual(15n);
    expect(span.midpoint.fractional).toBeCloseTo(0);
  });

  test('translate', () => {
    const span = HPTimeSpan.fromTime(t(10n), t(20n));
    expect(span.translate(10).start.integral).toEqual(20n);
    expect(span.translate(10).start.fractional).toEqual(0);
    expect(span.translate(10).duration).toBeCloseTo(10);
  });

  test('pad', () => {
    const span = HPTimeSpan.fromTime(t(10n), t(20n));
    expect(span.pad(10).start.integral).toEqual(0n);
    expect(span.pad(10).start.fractional).toEqual(0);
    expect(span.pad(10).duration).toBeCloseTo(30);
  });

  test('scale', () => {
    const span = HPTimeSpan.fromTime(t(10n), t(20n));
    const zoomed = span.scale(2, 0.5, 0);
    expect(zoomed.start.integral).toEqual(5n);
    expect(zoomed.start.fractional).toBeCloseTo(0);
    expect(zoomed.duration).toBeCloseTo(20);
  });

  test('intersect', () => {
    const span = new HPTimeSpan(hptime('5'), 3);

    let result = span.intersect(t(7n), t(10n));
    expect(result.start.integral).toBe(7n);
    expect(result.start.fractional).toBeCloseTo(0);
    expect(result.duration).toBeCloseTo(1);

    result = span.intersect(t(1n), t(6n));
    expect(result.start.integral).toBe(5n);
    expect(result.start.fractional).toBeCloseTo(0);
    expect(result.duration).toBeCloseTo(1);

    // Non overlapping time spans should return 0
    result = span.intersect(t(100n), t(200n));
    expect(result.start.integral).toBe(0n);
    expect(result.start.fractional).toBeCloseTo(0);
    expect(result.duration).toBeCloseTo(0);
  });

  test('fitWithin', () => {
    const span = new HPTimeSpan(hptime('5'), 3);

    let result = span.fitWithin(t(10n), t(20n));
    expect(result.start.integral).toBe(10n);
    expect(result.start.fractional).toBeCloseTo(0);
    expect(result.duration).toBeCloseTo(3);

    result = span.fitWithin(t(-10n), t(-5n));
    expect(result.start.integral).toBe(-8n);
    expect(result.start.fractional).toBeCloseTo(0);
    expect(result.duration).toBeCloseTo(3);

    result = span.fitWithin(t(1n), t(2n));
    expect(result.start.integral).toBe(1n);
    expect(result.start.fractional).toBeCloseTo(0);
    expect(result.duration).toBeCloseTo(1);
  });

  test('clampDuration', () => {
    const span = new HPTimeSpan(hptime('5'), 1);
    const clamped = span.clampDuration(10);

    expect(clamped.start.integral).toBe(5n);
    expect(clamped.start.fractional).toBeCloseTo(0);
    expect(clamped.duration).toBeCloseTo(10);
  });

  test('equality', () => {
    const span = new HPTimeSpan(hptime('10'), 10);
    expect(span.equals(span)).toBe(true);
    expect(span.equals(new HPTimeSpan(hptime('10'), 10.5))).toBe(false);
    expect(span.equals(new HPTimeSpan(hptime('10.1'), 10))).toBe(false);
  });

  test('contains', () => {
    const span = new HPTimeSpan(hptime('10'), 10);
    expect(span.contains(t(9n))).toBe(false);
    expect(span.contains(t(10n))).toBe(true);
    expect(span.contains(t(19n))).toBe(true);
    expect(span.contains(t(20n))).toBe(false);
  });

  test('containsSpan', () => {
    const span = new HPTimeSpan(hptime('10'), 10);
    expect(span.containsSpan(t(9n), t(15n))).toBe(false);
    expect(span.containsSpan(t(10n), t(15n))).toBe(true);
    expect(span.containsSpan(t(15n), t(20n))).toBe(true);
    expect(span.containsSpan(t(15n), t(21n))).toBe(false);
    expect(span.containsSpan(t(30n), t(40n))).toBe(false);
  });

  test('overlapsSpan', () => {
    const span = new HPTimeSpan(hptime('10'), 10);
    expect(span.overlaps(t(9n), t(10n))).toBe(false);
    expect(span.overlaps(t(9n), t(11n))).toBe(true);
    expect(span.overlaps(t(19n), t(21n))).toBe(true);
    expect(span.overlaps(t(20n), t(21n))).toBe(false);
  });

  test('containedBy', () => {
    const span = new HPTimeSpan(hptime('10'), 10);

    // Span completely contained within bounds
    expect(span.containedBy(t(5n), t(25n))).toBe(true);
    expect(span.containedBy(t(0n), t(100n))).toBe(true);

    // Span exactly matches bounds
    expect(span.containedBy(t(10n), t(20n))).toBe(true);

    // Span starts before bounds
    expect(span.containedBy(t(15n), t(25n))).toBe(false);

    // Span ends after bounds
    expect(span.containedBy(t(5n), t(15n))).toBe(false);

    // Span larger than bounds (extends both sides)
    expect(span.containedBy(t(12n), t(18n))).toBe(false);

    // Span partially overlaps start
    expect(span.containedBy(t(15n), t(30n))).toBe(false);

    // Span partially overlaps end
    expect(span.containedBy(t(5n), t(12n))).toBe(false);

    // Span completely outside (before)
    expect(span.containedBy(t(0n), t(5n))).toBe(false);

    // Span completely outside (after)
    expect(span.containedBy(t(25n), t(30n))).toBe(false);
  });
});
