// 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 {Actions} from '../common/actions';
import {getContainingTrackId} from '../common/state';
import {fromNs, TimeSpan, toNs} from '../common/time';

import {globals} from './globals';

const INCOMPLETE_SLICE_TIME_S = 0.00003;

// 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: number) {
  const startNs = toNs(globals.frontendLocalState.visibleWindowTime.start);
  const endNs = toNs(globals.frontendLocalState.visibleWindowTime.end);
  const currentViewNs = endNs - startNs;
  if (ts < startNs || ts > endNs) {
    // TODO(hjd): This is an ugly jump, we should do a smooth pan instead.
    globals.frontendLocalState.updateVisibleTime(new TimeSpan(
        fromNs(ts - currentViewNs / 2), fromNs(ts + currentViewNs / 2)));
  }
}

// Given a start and end timestamp (in ns), move the viewport to center this
// range and zoom if necessary:
// - 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(startTs: number, endTs: number) {
  const visibleDur = globals.frontendLocalState.visibleWindowTime.end -
      globals.frontendLocalState.visibleWindowTime.start;
  let selectDur = endTs - startTs;
  // TODO(altimin): We go from `ts` and `dur` to `startTs` and `endTs` and back
  // to `dur`. We should fix that.
  if (toNs(selectDur) === -1) {  // Unfinished slice
    selectDur = INCOMPLETE_SLICE_TIME_S;
    endTs = startTs;
  }
  // If the range is too large to fit on the current zoom level, resize.
  if (selectDur > 0.5 * visibleDur) {
    globals.frontendLocalState.updateVisibleTime(
        new TimeSpan(startTs - (selectDur * 2), endTs + (selectDur * 2)));
    return;
  }
  const midpointTs = (endTs + startTs) / 2;
  // Calculate the new visible window preserving the zoom level.
  let newStartTs = midpointTs - visibleDur / 2;
  let newEndTs = midpointTs + visibleDur / 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 (newEndTs > globals.state.traceTime.endSec) {
    newStartTs = globals.state.traceTime.endSec - visibleDur;
    newEndTs = globals.state.traceTime.endSec;
  }
  if (newStartTs < globals.state.traceTime.startSec) {
    newStartTs = globals.state.traceTime.startSec;
    newEndTs = globals.state.traceTime.startSec + visibleDur;
  }

  const newStartNs = toNs(newStartTs);
  const newEndNs = toNs(newEndTs);

  const viewStartNs = toNs(globals.frontendLocalState.visibleWindowTime.start);
  const viewEndNs = toNs(globals.frontendLocalState.visibleWindowTime.end);

  // If preserving the zoom doesn't change the visible window, update the zoom
  // level.
  if (newStartNs === viewStartNs && newEndNs === viewEndNs) {
    globals.frontendLocalState.updateVisibleTime(
        new TimeSpan(startTs - (selectDur * 2), endTs + (selectDur * 2)));
    return;
  }
  globals.frontendLocalState.updateVisibleTime(
      new TimeSpan(newStartTs, newEndTs));
}

// 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(
    trackId: string|number, openGroup = false) {
  const trackIdString = `${trackId}`;
  const track = document.querySelector('#track_' + trackIdString);

  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, trackIdString);
  if (trackGroupId) {
    trackGroup = document.querySelector('#track_' + trackGroupId);
  }

  if (!trackGroupId || !trackGroup) {
    console.error(`Can't scroll, track (${trackIdString}) 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.frontendLocalState.scrollToTrackId = trackId;
    globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
    return;
  } else {
    trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'});
  }
}


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