// Copyright (C) 2019 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 {time} from '../base/time';
import {Actions} from '../common/actions';
import {
  HighPrecisionTime,
  HighPrecisionTimeSpan,
} from '../common/high_precision_time';
import {getContainingTrackId} from '../common/state';

import {globals} from './globals';


// Given a timestamp, if |ts| is not currently in view move the view to
// center |ts|, keeping the same zoom level.
export function horizontalScrollToTs(ts: time) {
  const time = HighPrecisionTime.fromTime(ts);
  const visibleWindow = globals.timeline.visibleWindowTime;
  if (!visibleWindow.contains(time)) {
    // TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
    const halfDuration = visibleWindow.duration.divide(2);
    const newStart = time.sub(halfDuration);
    const newWindow = new HighPrecisionTimeSpan(
        newStart, newStart.add(visibleWindow.duration));
    globals.timeline.updateVisibleTime(newWindow);
  }
}

// Given a start and end timestamp (in ns), move the viewport to center this
// range and zoom if necessary:
// - If [viewPercentage] is specified, the viewport will be zoomed so that
//   the given time range takes up this percentage of the viewport.
// The following scenarios assume [viewPercentage] is undefined.
// - If the new range is more than 50% of the viewport, zoom out to a level
// where
//   the range is 1/5 of the viewport.
// - If the new range is already centered, update the zoom level for the
// viewport
//   to cover 1/5 of the viewport.
// - Otherwise, preserve the zoom range.
export function focusHorizontalRange(
    start: time, end: time, viewPercentage?: number) {
  const visible = globals.timeline.visibleWindowTime;
  const trace = globals.stateTraceTime();
  const select = HighPrecisionTimeSpan.fromTime(start, end);

  if (viewPercentage !== undefined) {
    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 paddingTime = select.duration.multiply(paddingPercentage);
    const halfPaddingTime = paddingTime.divide(2);
    globals.timeline.updateVisibleTime(select.pad(halfPaddingTime));
    return;
  }
  // If the range is too large to fit on the current zoom level, resize.
  if (select.duration.gt(visible.duration.multiply(0.5))) {
    const paddedRange = select.pad(select.duration.multiply(2));
    globals.timeline.updateVisibleTime(paddedRange);
    return;
  }
  // Calculate the new visible window preserving the zoom level.
  let newStart = select.midpoint.sub(visible.duration.divide(2));
  let newEnd = select.midpoint.add(visible.duration.divide(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.
  if (newEnd.gt(trace.end)) {
    newStart = trace.end.sub(visible.duration);
    newEnd = trace.end;
  }
  if (newStart.lt(trace.start)) {
    newStart = trace.start;
    newEnd = trace.start.add(visible.duration);
  }

  const view = new HighPrecisionTimeSpan(newStart, newEnd);

  // If preserving the zoom doesn't change the visible window, update the zoom
  // level.
  if (view.start.eq(visible.start) && view.end.eq(visible.end)) {
    const padded = select.pad(select.duration.multiply(2));
    globals.timeline.updateVisibleTime(padded);
  } else {
    globals.timeline.updateVisibleTime(view);
  }
}

// Given a track id, find a track with that id and scroll it into view. If the
// track is nested inside a track group, scroll to that track group instead.
// If |openGroup| then open the track group and scroll to the track.
export function verticalScrollToTrack(
    trackKey: string|number, openGroup = false) {
  const trackKeyString = `${trackKey}`;
  const track = document.querySelector('#track_' + trackKeyString);

  if (track) {
    // block: 'nearest' means that it will only scroll if the track is not
    // currently in view.
    track.scrollIntoView({behavior: 'smooth', block: 'nearest'});
    return;
  }

  let trackGroup = null;
  const trackGroupId = getContainingTrackId(globals.state, trackKeyString);
  if (trackGroupId) {
    trackGroup = document.querySelector('#track_' + trackGroupId);
  }

  if (!trackGroupId || !trackGroup) {
    console.error(`Can't scroll, track (${trackKeyString}) not found.`);
    return;
  }

  // The requested track is inside a closed track group, either open the track
  // group and scroll to the track or just scroll to the track group.
  if (openGroup) {
    // After the track exists in the dom, it will be scrolled to.
    globals.scrollToTrackKey = trackKey;
    globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
    return;
  } else {
    trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'});
  }
}


// Scroll vertically and horizontally to reach track (|trackKey|) at |ts|.
export function scrollToTrackAndTs(
    trackKey: string|number|undefined, ts: time, openGroup = false) {
  if (trackKey !== undefined) {
    verticalScrollToTrack(trackKey, openGroup);
  }
  horizontalScrollToTs(ts);
}

// Scroll vertically and horizontally to a track and time range
export function reveal(
    trackKey: string|number, start: time, end: time, openGroup = false) {
  verticalScrollToTrack(trackKey, openGroup);
  focusHorizontalRange(start, end);
}
