// Copyright (C) 2018 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 {assertTrue, assertUnreachable} from '../base/logging';
import {Time, time, TimeSpan} from '../base/time';
import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
import {Area} from '../public/selection';
import {raf} from './raf_scheduler';
import {HighPrecisionTime} from '../base/high_precision_time';
import {DurationPrecision, Timeline, TimestampFormat} from '../public/timeline';
import {
  durationPrecision,
  setDurationPrecision,
  setTimestampFormat,
  timestampFormat,
} from './timestamp_format';
import {TraceInfo} from '../public/trace_info';

const MIN_DURATION = 10;

/**
 * State that is shared between several frontend components, but not the
 * controller. This state is updated at 60fps.
 */
export class TimelineImpl implements Timeline {
  private _visibleWindow: HighPrecisionTimeSpan;
  private _hoverCursorTimestamp?: time;
  private _highlightedSliceId?: number;
  private _hoveredNoteTimestamp?: time;

  // TODO(stevegolton): These are currently only referenced by the cpu slice
  // tracks and the process summary tracks. We should just make this a local
  // property of the cpu slice tracks and ignore them in the process tracks.
  private _hoveredUtid?: number;
  private _hoveredPid?: number;

  get highlightedSliceId() {
    return this._highlightedSliceId;
  }

  set highlightedSliceId(x) {
    this._highlightedSliceId = x;
    raf.scheduleCanvasRedraw();
  }

  get hoveredNoteTimestamp() {
    return this._hoveredNoteTimestamp;
  }

  set hoveredNoteTimestamp(x) {
    this._hoveredNoteTimestamp = x;
    raf.scheduleCanvasRedraw();
  }

  get hoveredUtid() {
    return this._hoveredUtid;
  }

  set hoveredUtid(x) {
    this._hoveredUtid = x;
    raf.scheduleCanvasRedraw();
  }

  get hoveredPid() {
    return this._hoveredPid;
  }

  set hoveredPid(x) {
    this._hoveredPid = x;
    raf.scheduleCanvasRedraw();
  }

  // This is used to calculate the tracks within a Y range for area selection.
  private _selectedArea?: Area;

  constructor(private readonly traceInfo: TraceInfo) {
    this._visibleWindow = HighPrecisionTimeSpan.fromTime(
      traceInfo.start,
      traceInfo.end,
    );
  }

  // TODO: there is some redundancy in the fact that both |visibleWindowTime|
  // and a |timeScale| have a notion of time range. That should live in one
  // place only.

  zoomVisibleWindow(ratio: number, centerPoint: number) {
    this._visibleWindow = this._visibleWindow
      .scale(ratio, centerPoint, MIN_DURATION)
      .fitWithin(this.traceInfo.start, this.traceInfo.end);

    raf.scheduleCanvasRedraw();
  }

  panVisibleWindow(delta: number) {
    this._visibleWindow = this._visibleWindow
      .translate(delta)
      .fitWithin(this.traceInfo.start, this.traceInfo.end);

    raf.scheduleCanvasRedraw();
  }

  // Given a timestamp, if |ts| is not currently in view move the view to
  // center |ts|, keeping the same zoom level.
  panToTimestamp(ts: time) {
    if (this._visibleWindow.contains(ts)) return;
    // TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
    const halfDuration = this.visibleWindow.duration / 2;
    const newStart = new HighPrecisionTime(ts).subNumber(halfDuration);
    const newWindow = new HighPrecisionTimeSpan(
      newStart,
      this._visibleWindow.duration,
    );
    this.updateVisibleTimeHP(newWindow);
  }

  // Set the highlight box to draw
  selectArea(
    start: time,
    end: time,
    tracks = this._selectedArea ? this._selectedArea.trackUris : [],
  ) {
    assertTrue(
      end >= start,
      `Impossible select area: start [${start}] >= end [${end}]`,
    );
    this._selectedArea = {start, end, trackUris: tracks};
    raf.scheduleFullRedraw();
  }

  deselectArea() {
    this._selectedArea = undefined;
    raf.scheduleCanvasRedraw();
  }

  get selectedArea(): Area | undefined {
    return this._selectedArea;
  }

  // Set visible window using an integer time span
  updateVisibleTime(ts: TimeSpan) {
    this.updateVisibleTimeHP(HighPrecisionTimeSpan.fromTime(ts.start, ts.end));
  }

  // TODO(primiano): we ended up with two entry-points for the same function,
  // unify them.
  setViewportTime(start: time, end: time): void {
    this.updateVisibleTime(new TimeSpan(start, end));
  }

  // Set visible window using a high precision time span
  updateVisibleTimeHP(ts: HighPrecisionTimeSpan) {
    this._visibleWindow = ts
      .clampDuration(MIN_DURATION)
      .fitWithin(this.traceInfo.start, this.traceInfo.end);

    raf.scheduleCanvasRedraw();
  }

  // Get the bounds of the visible window as a high-precision time span
  get visibleWindow(): HighPrecisionTimeSpan {
    return this._visibleWindow;
  }

  get hoverCursorTimestamp(): time | undefined {
    return this._hoverCursorTimestamp;
  }

  set hoverCursorTimestamp(t: time | undefined) {
    this._hoverCursorTimestamp = t;
    raf.scheduleCanvasRedraw();
  }

  // Offset between t=0 and the configured time domain.
  timestampOffset(): time {
    const fmt = timestampFormat();
    switch (fmt) {
      case TimestampFormat.Timecode:
      case TimestampFormat.Seconds:
      case TimestampFormat.Milliseconds:
      case TimestampFormat.Microseconds:
        return this.traceInfo.start;
      case TimestampFormat.TraceNs:
      case TimestampFormat.TraceNsLocale:
        return Time.ZERO;
      case TimestampFormat.UTC:
        return this.traceInfo.utcOffset;
      case TimestampFormat.TraceTz:
        return this.traceInfo.traceTzOffset;
      default:
        assertUnreachable(fmt);
    }
  }

  // Convert absolute time to domain time.
  toDomainTime(ts: time): time {
    return Time.sub(ts, this.timestampOffset());
  }

  get timestampFormat() {
    return timestampFormat();
  }

  set timestampFormat(format: TimestampFormat) {
    setTimestampFormat(format);
  }

  get durationPrecision() {
    return durationPrecision();
  }

  set durationPrecision(precision: DurationPrecision) {
    setDurationPrecision(precision);
  }
}
