// Copyright (C) 2021 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 m from 'mithril';
import {tryGetTrace} from '../core/cache_manager';
import {showModal} from '../widgets/modal';
import {loadPermalink} from './permalink';
import {loadAndroidBugToolInfo} from './android_bug_tool';
import {Route, Router} from '../core/router';
import {taskTracker} from './task_tracker';
import {AppImpl} from '../core/app_impl';

function getCurrentTraceUrl(): undefined | string {
  const source = AppImpl.instance.trace?.traceInfo.source;
  if (source && source.type === 'URL') {
    return source.url;
  }
  return undefined;
}

export function maybeOpenTraceFromRoute(route: Route) {
  if (route.args.s) {
    // /?s=xxxx for permalinks.
    loadPermalink(route.args.s);
    return;
  }

  const url = route.args.url;
  if (url && url !== getCurrentTraceUrl()) {
    // /?url=https://example.com/path/to/trace.pftrace
    // Works with any public HTTPS URL. The service worker firewall enforces
    // that only GET requests without query strings are allowed.
    loadTraceFromUrl(url);
    return;
  }

  if (route.args.openFromAndroidBugTool) {
    // Handles interaction with the Android Bug Tool extension. See b/163421158.
    openTraceFromAndroidBugTool();
    return;
  }

  if (route.args.p && route.page === '/record') {
    // Handles backwards compatibility for old URLs (linked from various docs),
    // generated before we switched URL scheme. e.g., 'record?p=power' vs
    // 'record/power'. See b/191255021#comment2.
    Router.navigate(`#!/record/${route.args.p}`);
    return;
  }

  if (route.args.local_cache_key) {
    // Handles the case of loading traces from the cache storage.
    maybeOpenCachedTrace(route.args.local_cache_key);
    return;
  }
}

/*
 * openCachedTrace(uuid) is called: (1) on startup, from frontend/index.ts; (2)
 * every time the fragment changes (from Router.onRouteChange).
 * This function must be idempotent (imagine this is called on every frame).
 * It must take decision based on the app state, not on URL change events.
 * Fragment changes are handled by the union of Router.onHashChange() and this
 * function, as follows:
 * 1. '' -> URL without a ?local_cache_key=xxx arg:
 *  - no effect (except redrawing)
 * 2. URL without local_cache_key -> URL with local_cache_key:
 *  - Load cached trace (without prompting any dialog).
 *  - Show a (graceful) error dialog in the case of cache misses.
 * 3. '' -> URL with a ?local_cache_key=xxx arg:
 *  - Same as case 2.
 * 4. URL with local_cache_key=1 -> URL with local_cache_key=2:
 *  a) If 2 != uuid of the trace currently loaded (TraceImpl.traceInfo.uuid):
 *  - Ask the user if they intend to switch trace and load 2.
 *  b) If 2 == uuid of current trace (e.g., after a new trace has loaded):
 *  - no effect (except redrawing).
 * 5. URL with local_cache_key -> URL without local_cache_key:
 *  - Redirect to ?local_cache_key=1234 where 1234 is the UUID of the previous
 *    URL (this might or might not match traceInfo.uuid).
 *
 * Backward navigation cases:
 * 6. URL without local_cache_key <- URL with local_cache_key:
 *  - Same as case 5.
 * 7. URL with local_cache_key=1 <- URL with local_cache_key=2:
 *  - Same as case 4a: go back to local_cache_key=1 but ask the user to confirm.
 * 8. landing page <- URL with local_cache_key:
 *  - Same as case 5: re-append the local_cache_key.
 */
async function maybeOpenCachedTrace(traceUuid: string) {
  const curTrace = AppImpl.instance.trace?.traceInfo;
  const curCacheUuid = curTrace?.cached ? curTrace.uuid : '';

  if (traceUuid === curCacheUuid) {
    // Do nothing, matches the currently loaded trace.
    return;
  }

  if (traceUuid === '') {
    // This can happen if we switch from an empty UI state to an invalid UUID
    // (e.g. due to a cache miss, below). This can also happen if the user just
    // types /#!/viewer?local_cache_key=.
    return;
  }

  // This handles the case when a trace T1 is loaded and then the url is set to
  // ?local_cache_key=T2. In that case globals.state.traceUuid remains set to T1
  // until T2 has been loaded by the trace processor (can take several seconds).
  // This early out prevents to re-trigger the openTraceFromXXX() action if the
  // URL changes (e.g. if the user navigates back/fwd) while the new trace is
  // being loaded.
  if (
    curTrace !== undefined &&
    curTrace.source.type === 'ARRAY_BUFFER' &&
    curTrace.source.uuid === traceUuid
  ) {
    return;
  }

  // Fetch the trace from the cache storage. If available load it. If not, show
  // a dialog informing the user about the cache miss.
  const maybeTrace = await tryGetTrace(traceUuid);

  const navigateToOldTraceUuid = () =>
    Router.navigate(`#!/viewer?local_cache_key=${curCacheUuid}`);

  if (!maybeTrace) {
    showModal({
      title: 'Could not find the trace in the cache storage',
      content: m(
        'div',
        m(
          'p',
          'You are trying to load a cached trace by setting the ' +
            '?local_cache_key argument in the URL.',
        ),
        m('p', "Unfortunately the trace wasn't in the cache storage."),
        m(
          'p',
          "This can happen if a tab was discarded and wasn't opened " +
            'for too long, or if you just mis-pasted the URL.',
        ),
        m('pre', `Trace UUID: ${traceUuid}`),
      ),
    });
    navigateToOldTraceUuid();
    return;
  }

  // If the UI is in a blank state (no trace has been ever opened), just load
  // the trace without showing any further dialog. This is the case of tab
  // discarding, reloading or pasting a url with a local_cache_key in an empty
  // instance.
  if (curTrace === undefined) {
    AppImpl.instance.openTraceFromBuffer(maybeTrace);
    return;
  }

  // If, instead, another trace is loaded, ask confirmation to the user.
  // Switching to another trace clears the UI state. It can be quite annoying to
  // lose the UI state by accidentally navigating back too much.
  let hasOpenedNewTrace = false;

  await showModal({
    title: 'You are about to load a different trace and reset the UI state',
    content: m(
      'div',
      m(
        'p',
        'You are seeing this because you either pasted a URL with ' +
          'a different ?local_cache_key=xxx argument or because you hit ' +
          'the history back/fwd button and reached a different trace.',
      ),
      m(
        'p',
        'If you continue another trace will be loaded and the UI ' +
          'state will be cleared.',
      ),
      m(
        'pre',
        `Old trace: ${curTrace !== undefined ? curCacheUuid : '<no trace>'}\n` +
          `New trace: ${traceUuid}`,
      ),
    ),
    buttons: [
      {
        text: 'Continue',
        id: 'trace_id_open', // Used by tests.
        primary: true,
        action: () => {
          hasOpenedNewTrace = true;
          AppImpl.instance.openTraceFromBuffer(maybeTrace);
        },
      },
      {text: 'Cancel'},
    ],
  });

  if (!hasOpenedNewTrace) {
    // We handle this after the modal await rather than in the cancel button
    // action so this has effect even if the user clicks Esc or clicks outside
    // of the modal dialog and dismisses it.
    navigateToOldTraceUuid();
  }
}

function loadTraceFromUrl(url: string) {
  const isLocalhostTraceUrl = ['127.0.0.1', 'localhost'].includes(
    new URL(url).hostname,
  );

  if (isLocalhostTraceUrl) {
    // This handles the special case of tools/record_android_trace serving the
    // traces from a local webserver and killing it immediately after having
    // seen the HTTP GET request. In those cases store the trace as a file, so
    // when users click on share we don't fail the re-fetch().
    const fileName = url.split('/').pop() ?? 'local_trace.pftrace';
    const request = fetch(url)
      .then((response) => response.blob())
      .then((b) => AppImpl.instance.openTraceFromFile(new File([b], fileName)))
      .catch((e) => alert(`Could not load local trace ${e}`));
    taskTracker.trackPromise(request, 'Downloading local trace');
  } else {
    AppImpl.instance.openTraceFromUrl(url);
  }
}

function openTraceFromAndroidBugTool() {
  const msg = 'Loading trace from ABT extension';
  AppImpl.instance.omnibox.showStatusMessage(msg);
  const loadInfo = loadAndroidBugToolInfo();
  taskTracker.trackPromise(loadInfo, msg);
  loadInfo
    .then((info) => AppImpl.instance.openTraceFromFile(info.file))
    .catch((e) => console.error(e));
}
