// 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 {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
import {time} from '../base/time';
import {ScrollToArgs} from '../public/scroll_helper';
import {TraceInfo} from '../public/trace_info';
import {Workspace} from '../public/workspace';
import {raf} from './raf_scheduler';
import {TimelineImpl} from './timeline';
import {TrackManagerImpl} from './track_manager';

// A helper class to help jumping to tracks and time ranges.
// This class must NOT alter in any way the selection status. That
// responsibility belongs to SelectionManager (which uses this).
export class ScrollHelper {
  constructor(
    private traceInfo: TraceInfo,
    private timeline: TimelineImpl,
    private workspace: Workspace,
    private trackManager: TrackManagerImpl,
  ) {}

  // See comments in ScrollToArgs for the intended semantics.
  scrollTo(args: ScrollToArgs) {
    const {time, track} = args;
    raf.scheduleCanvasRedraw();

    if (time !== undefined) {
      if (time.end === undefined) {
        this.timeline.panToTimestamp(time.start);
      } else if (time.viewPercentage !== undefined) {
        this.focusHorizontalRangePercentage(
          time.start,
          time.end,
          time.viewPercentage,
        );
      } else {
        this.focusHorizontalRange(time.start, time.end);
      }
    }

    if (track !== undefined) {
      this.verticalScrollToTrack(track.uri, track.expandGroup ?? false);
    }
  }

  private focusHorizontalRangePercentage(
    start: time,
    end: time,
    viewPercentage: number,
  ): void {
    const aoi = HighPrecisionTimeSpan.fromTime(start, end);

    if (viewPercentage <= 0.0 || viewPercentage > 1.0) {
      console.warn(
        'Invalid value for [viewPercentage]. ' +
          'Value must be between 0.0 (exclusive) and 1.0 (inclusive).',
      );
      // Default to 50%.
      viewPercentage = 0.5;
    }
    const paddingPercentage = 1.0 - viewPercentage;
    const halfPaddingTime = (aoi.duration * paddingPercentage) / 2;
    this.timeline.updateVisibleTimeHP(aoi.pad(halfPaddingTime));
  }

  private focusHorizontalRange(start: time, end: time): void {
    const visible = this.timeline.visibleWindow;
    const aoi = HighPrecisionTimeSpan.fromTime(start, end);
    const fillRatio = 5; // Default amount to make the AOI fill the viewport
    const padRatio = (fillRatio - 1) / 2;

    // If the area of interest already fills more than half the viewport, zoom
    // out so that the AOI fills 20% of the viewport
    if (aoi.duration * 2 > visible.duration) {
      const padded = aoi.pad(aoi.duration * padRatio);
      this.timeline.updateVisibleTimeHP(padded);
    } else {
      // Center visible window on the middle of the AOI, preserving zoom level.
      const newStart = aoi.midpoint.subNumber(visible.duration / 2);

      // Adjust the new visible window if it intersects with the trace boundaries.
      // It's needed to make the "update the zoom level if visible window doesn't
      // change" logic reliable.
      const newVisibleWindow = new HighPrecisionTimeSpan(
        newStart,
        visible.duration,
      ).fitWithin(this.traceInfo.start, this.traceInfo.end);

      // If preserving the zoom doesn't change the visible window, consider this
      // to be the "second" hotkey press, so just make the AOI fill 20% of the
      // viewport
      if (newVisibleWindow.equals(visible)) {
        const padded = aoi.pad(aoi.duration * padRatio);
        this.timeline.updateVisibleTimeHP(padded);
      } else {
        this.timeline.updateVisibleTimeHP(newVisibleWindow);
      }
    }
  }

  private verticalScrollToTrack(trackUri: string, openGroup: boolean) {
    // Find the actual track node that uses that URI, we need various properties
    // from it.
    const trackNode = this.workspace.findTrackByUri(trackUri);
    if (!trackNode) return;

    // Try finding the track directly.
    const element = document.getElementById(trackNode.id);
    if (element) {
      // block: 'nearest' means that it will only scroll if the track is not
      // currently in view.
      element.scrollIntoView({behavior: 'smooth', block: 'nearest'});
      return;
    }

    // If we get here, the element for this track was not present in the DOM,
    // which might be because it's inside a collapsed group.
    if (openGroup) {
      // Try to reveal the track node in the workspace by opening up all
      // ancestor groups, and mark the track URI to be scrolled to in the
      // future.
      trackNode.reveal();
      this.trackManager.scrollToTrackNodeId = trackNode.id;
    } else {
      // Find the closest visible ancestor of our target track and scroll to
      // that instead.
      const container = trackNode.findClosestVisibleAncestor();
      document
        .getElementById(container.id)
        ?.scrollIntoView({behavior: 'smooth', block: 'nearest'});
    }
  }
}
