// 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 {TimeSpan, time} from './time';
import {HighPrecisionTime} from './high_precision_time';

/**
 * Represents a time span using a high precision time value to represent the
 * start of the span, and a number to represent the duration of the span.
 */
export class HighPrecisionTimeSpan {
  static readonly ZERO = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0);

  readonly start: HighPrecisionTime;
  readonly duration: number;

  constructor(start: HighPrecisionTime, duration: number) {
    this.start = start;
    this.duration = duration;
  }

  /**
   * Create a new span from two high precision times. The earlier time is used
   * as the start and and the later time as the end of the span.
   */
  static fromHpTimes(
    a: HighPrecisionTime,
    b: HighPrecisionTime,
  ): HighPrecisionTimeSpan {
    const start = HighPrecisionTime.min(a, b);
    const end = HighPrecisionTime.max(a, b);
    return new HighPrecisionTimeSpan(start, Number(end.sub(start)));
  }

  /**
   * Create a new span from integral start and end points.
   *
   * @param start The start of the span.
   * @param end The end of the span.
   */
  static fromTime(start: time, end: time): HighPrecisionTimeSpan {
    return new HighPrecisionTimeSpan(
      new HighPrecisionTime(start),
      Number(end - start),
    );
  }

  /**
   * The center point of the span.
   */
  get midpoint(): HighPrecisionTime {
    return this.start.addNumber(this.duration / 2);
  }

  /**
   * The end of the span.
   */
  get end(): HighPrecisionTime {
    return this.start.addNumber(this.duration);
  }

  /**
   * Checks if this span exactly equals another.
   */
  equals(other: HighPrecisionTimeSpan): boolean {
    return this.start.equals(other.start) && this.duration === other.duration;
  }

  /**
   * Create a new span with the same duration but the start point moved through
   * time by some amount of time.
   */
  translate(time: number): HighPrecisionTimeSpan {
    return new HighPrecisionTimeSpan(this.start.addNumber(time), this.duration);
  }

  /**
   * Create a new span with the the start of the span moved backward and the end
   * of the span moved forward by a certain amount of time.
   */
  pad(time: number): HighPrecisionTimeSpan {
    return new HighPrecisionTimeSpan(
      this.start.subNumber(time),
      this.duration + 2 * time,
    );
  }

  /**
   * Create a new span which is zoomed in or out centered on a specific point.
   *
   * @param ratio The scaling ratio, the new duration will be the current
   * duration * ratio.
   * @param center The center point as a normalized value between 0 and 1 where
   * 0 is the start of the time window and 1 is the end.
   * @param minDur Don't allow the time span to become shorter than this.
   */
  scale(
    ratio: number,
    center: number,
    minDur: number = 0,
  ): HighPrecisionTimeSpan {
    const currentDuration = this.duration;
    const newDuration = Math.max(currentDuration * ratio, minDur);
    // Delta between new and old duration
    // +ve if new duration is shorter than old duration
    const durationDeltaNanos = currentDuration - newDuration;
    // If offset is 0, don't move the start at all
    // If offset if 1, move the start by the amount the duration has changed
    // If new duration is shorter - move start to right
    // If new duration is longer - move start to left
    const start = this.start.addNumber(durationDeltaNanos * center);
    return new HighPrecisionTimeSpan(start, newDuration);
  }

  /**
   * Create a new span that represents the intersection of this span with
   * another.
   *
   * If the two spans do not overlap at all, the empty span is returned.
   *
   * @param start THe start of the other span.
   * @param end The end of the other span.
   */
  intersect(start: time, end: time): HighPrecisionTimeSpan {
    if (!this.overlaps(start, end)) {
      return HighPrecisionTimeSpan.ZERO;
    }
    const newStart = this.start.clamp(start, end);
    const newEnd = this.end.clamp(start, end);
    const newDuration = newEnd.sub(newStart).toNumber();
    return new HighPrecisionTimeSpan(newStart, newDuration);
  }

  /**
   * Create a new timespan which fits within the specified bounds, preserving
   * its duration if possible.
   *
   * This function moves the timespan forwards or backwards in time while
   * keeping its duration unchanged, so that it fits entirely within the range
   * defined by `start` and `end`.
   *
   * If the specified bounds are smaller than the current timespan's duration, a
   * new timespan matching the bounds is returned.
   *
   * @param start The start of the bounds within which the timespan should fit.
   * @param end The end of the bounds within which the timespan should fit.
   *
   * @example
   * // assume `timespan` is defined as: [5, 8)
   * timespan.fitWithin(10n, 20n); // -> [10, 13)
   * timespan.fitWithin(-10n, -5n); // -> [-8, -5)
   * timespan.fitWithin(1n, 2n); // -> [1, 2)
   */
  fitWithin(start: time, end: time): HighPrecisionTimeSpan {
    if (this.duration > Number(end - start)) {
      // Current span is greater than the limits
      return HighPrecisionTimeSpan.fromTime(start, end);
    }
    if (this.start.integral < start) {
      // Current span starts before limits
      return new HighPrecisionTimeSpan(
        new HighPrecisionTime(start),
        this.duration,
      );
    }
    if (this.end.gt(end)) {
      // Current span ends after limits
      return new HighPrecisionTimeSpan(
        new HighPrecisionTime(end).subNumber(this.duration),
        this.duration,
      );
    }
    return this;
  }

  /**
   * Clamp duration to some minimum value. The start remains the same, just the
   * duration is changed.
   */
  clampDuration(minDuration: number): HighPrecisionTimeSpan {
    if (this.duration < minDuration) {
      return new HighPrecisionTimeSpan(this.start, minDuration);
    } else {
      return this;
    }
  }

  /**
   * Checks whether this span completely contains a time instant.
   */
  contains(t: time): boolean {
    return this.start.lte(t) && this.end.gt(t);
  }

  /**
   * Checks whether this span entirely contains another span.
   *
   * @param start The start of the span to check.
   * @param end The end of the span to check.
   */
  containsSpan(start: time, end: time): boolean {
    return this.start.lte(start) && this.end.gte(end);
  }

  /**
   * Checks if this span is entirely contained within another span.
   *
   * @param start The start of the other span.
   * @param end The end of the other span.
   */
  containedBy(start: time, end: time): boolean {
    return this.start.gte(start) && this.end.lte(end);
  }

  /**
   * Checks if this span overlaps at all with another.
   *
   * @param start The start of the span to check.
   * @param end The end of the span to check.
   */
  overlaps(start: time, end: time): boolean {
    return !(this.start.gte(end) || this.end.lte(start));
  }

  /**
   * Get the span of integer intervals values that overlap this span.
   */
  toTimeSpan(): TimeSpan {
    return new TimeSpan(this.start.toTime('floor'), this.end.toTime('ceil'));
  }
}
