// Copyright (C) 2022 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 {Attributes} from 'mithril';
import {assertExists} from '../../base/logging';
import {RecordingConfigUtils} from './recordingV2/recording_config_utils';
import {
  ChromeTargetInfo,
  RecordingTargetV2,
  TargetInfo,
} from './recordingV2/recording_interfaces_v2';
import {
  RecordingPageController,
  RecordingState,
} from './recordingV2/recording_page_controller';
import {EXTENSION_NAME, EXTENSION_URL} from './recordingV2/recording_utils';
import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
import {PageAttrs} from '../../public/page';
import {recordConfigStore} from './record_config';
import {
  Configurations,
  loadRecordConfig,
  maybeGetActiveCss,
  RECORDING_SECTIONS,
  uploadRecordingConfig,
} from './record_page';
import {CodeSnippet} from './record_widgets';
import {AdvancedSettings} from './advanced_settings';
import {AndroidSettings} from './android_settings';
import {ChromeSettings} from './chrome_settings';
import {CpuSettings} from './cpu_settings';
import {EtwSettings} from './etw_settings';
import {GpuSettings} from './gpu_settings';
import {LinuxPerfSettings} from './linux_perf_settings';
import {MemorySettings} from './memory_settings';
import {PowerSettings} from './power_settings';
import {RecordingSettings} from './recording_settings';
import {FORCE_RESET_MESSAGE} from './recording_ui_utils';
import {showAddNewTargetModal} from './reset_target_modal';
import {RecordingManager} from './recording_manager';
import {RecordConfig} from './record_config_types';
import {App} from '../../public/app';
import {scheduleFullRedraw} from '../../widgets/raf';

const START_RECORDING_MESSAGE = 'Start Recording';

// TODO(primiano): this is needs to be rewritten, but then i'm going to rewrite
// the whole record_page_v2 so not worth cleaning up now.
let controller: RecordingPageController;
let recordConfigUtils: RecordingConfigUtils;

// Options for displaying a target selection menu.
export interface TargetSelectionOptions {
  // css attributes passed to the mithril components which displays the target
  // selection menu.
  attributes: Attributes;
  // Whether the selection should be preceded by a text label.
  shouldDisplayLabel: boolean;
}

function isChromeTargetInfo(
  targetInfo: TargetInfo,
): targetInfo is ChromeTargetInfo {
  return ['CHROME', 'CHROME_OS', 'WINDOWS'].includes(targetInfo.targetType);
}

function RecordHeader(recMgr: RecordingManager) {
  const platformSelection = RecordingPlatformSelection();
  const statusLabel = RecordingStatusLabel(recMgr);
  const buttons = RecordingButton(recMgr.state.recordConfig);
  const notes = RecordingNotes(recMgr.state.recordConfig);
  if (!platformSelection && !statusLabel && !buttons && !notes) {
    // The header should not be displayed when it has no content.
    return undefined;
  }
  return m(
    '.record-header',
    m(
      '.top-part',
      m('.target-and-status', platformSelection, statusLabel),
      buttons,
    ),
    notes,
  );
}

function RecordingPlatformSelection() {
  // Don't show the platform selector while we are recording a trace.
  if (controller.getState() >= RecordingState.RECORDING) return undefined;

  return m(
    '.target',
    m(
      '.chip',
      {onclick: () => showAddNewTargetModal(controller)},
      m('button', 'Add new recording target'),
      m('i.material-icons', 'add'),
    ),
    targetSelection(),
  );
}

export function targetSelection(): m.Vnode | undefined {
  if (!controller.shouldShowTargetSelection()) {
    return undefined;
  }

  const targets: RecordingTargetV2[] = targetFactoryRegistry.listTargets();
  const targetNames = [];
  const targetInfo = controller.getTargetInfo();
  if (!targetInfo) {
    targetNames.push(m('option', 'PLEASE_SELECT_TARGET'));
  }

  let selectedIndex = 0;
  for (let i = 0; i < targets.length; i++) {
    const targetName = targets[i].getInfo().name;
    targetNames.push(m('option', targetName));
    if (targetInfo && targetName === targetInfo.name) {
      selectedIndex = i;
    }
  }

  return m(
    'label',
    'Target platform:',
    m(
      'select',
      {
        selectedIndex,
        onchange: (e: Event) => {
          controller.onTargetSelection((e.target as HTMLSelectElement).value);
        },
        onupdate: (select) => {
          // Work around mithril bug
          // (https://github.com/MithrilJS/mithril.js/issues/2107): We may
          // update the select's options while also changing the
          // selectedIndex at the same time. The update of selectedIndex
          // may be applied before the new options are added to the select
          // element. Because the new selectedIndex may be outside of the
          // select's options at that time, we have to reselect the
          // correct index here after any new children were added.
          (select.dom as HTMLSelectElement).selectedIndex = selectedIndex;
        },
      },
      ...targetNames,
    ),
  );
}

// This will display status messages which are informative, but do not require
// user action, such as: "Recording in progress for X seconds" in the recording
// page header.
function RecordingStatusLabel(recMgr: RecordingManager) {
  const recordingStatus = recMgr.state.recordingStatus;
  if (!recordingStatus) return undefined;
  return m('label', recordingStatus);
}

function Instructions(recCfg: RecordConfig, cssClass: string) {
  if (controller.getState() < RecordingState.TARGET_SELECTED) {
    return undefined;
  }
  // We will have a valid target at this step because we checked the state.
  const targetInfo = assertExists(controller.getTargetInfo());

  return m(
    `.record-section.instructions${cssClass}`,
    m('header', 'Recording command'),
    m(
      'button.permalinkconfig',
      {
        onclick: () => uploadRecordingConfig(recCfg),
      },
      'Share recording settings',
    ),
    RecordingSnippet(recCfg, targetInfo),
    BufferUsageProgressBar(),
    m('.buttons', StopCancelButtons()),
  );
}

function BufferUsageProgressBar() {
  // Show the Buffer Usage bar only after we start recording a trace.
  if (controller.getState() !== RecordingState.RECORDING) {
    return undefined;
  }

  controller.fetchBufferUsage();

  const bufferUsage = controller.getBufferUsagePercentage();
  // Buffer usage is not available yet on Android.
  if (bufferUsage === 0) return undefined;

  return m(
    'label',
    'Buffer usage: ',
    m('progress', {max: 100, value: bufferUsage * 100}),
  );
}

function RecordingNotes(recCfg: RecordConfig) {
  if (controller.getState() !== RecordingState.TARGET_INFO_DISPLAYED) {
    return undefined;
  }
  // We will have a valid target at this step because we checked the state.
  const targetInfo = assertExists(controller.getTargetInfo());

  const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
  const cmdlineUrl =
    'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline';

  const notes: m.Children = [];

  const msgFeatNotSupported = m(
    'span',
    `Some probes are only supported in Perfetto versions running
      on Android Q+. Therefore, Perfetto will sideload the latest version onto
      the device.`,
  );

  const msgPerfettoNotSupported = m(
    'span',
    `Perfetto is not supported natively before Android P. Therefore, Perfetto
       will sideload the latest version onto the device.`,
  );

  const msgLinux = m(
    '.note',
    `Use this `,
    m('a', {href: linuxUrl, target: '_blank'}, `quickstart guide`),
    ` to get started with tracing on Linux.`,
  );

  const msgLongTraces = m(
    '.note',
    `Recording in long trace mode through the UI is not supported. Please copy
    the command and `,
    m(
      'a',
      {href: cmdlineUrl, target: '_blank'},
      `collect the trace using ADB.`,
    ),
  );

  if (
    !recordConfigUtils.fetchLatestRecordCommand(recCfg, targetInfo)
      .hasDataSources
  ) {
    notes.push(
      m(
        '.note',
        "It looks like you didn't add any probes. " +
          'Please add at least one to get a non-empty trace.',
      ),
    );
  }

  targetFactoryRegistry.listRecordingProblems().map((recordingProblem) => {
    if (recordingProblem.includes(EXTENSION_URL)) {
      // Special case for rendering the link to the Chrome extension.
      const parts = recordingProblem.split(EXTENSION_URL);
      notes.push(
        m(
          '.note',
          parts[0],
          m('a', {href: EXTENSION_URL, target: '_blank'}, EXTENSION_NAME),
          parts[1],
        ),
      );
    }
  });

  switch (targetInfo.targetType) {
    case 'LINUX':
      notes.push(msgLinux);
      break;
    case 'ANDROID': {
      const androidApiLevel = targetInfo.androidApiLevel;
      if (androidApiLevel === 28) {
        notes.push(m('.note', msgFeatNotSupported));
        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
      } else if (androidApiLevel && androidApiLevel <= 27) {
        /* eslint-enable */
        notes.push(m('.note', msgPerfettoNotSupported));
      }
      break;
    }
    default:
  }

  if (recCfg.mode === 'LONG_TRACE') {
    notes.unshift(msgLongTraces);
  }

  return notes.length > 0 ? m('div', notes) : undefined;
}

function RecordingSnippet(recCfg: RecordConfig, targetInfo: TargetInfo) {
  // We don't need commands to start tracing on chrome
  if (isChromeTargetInfo(targetInfo)) {
    if (controller.getState() > RecordingState.AUTH_P2) {
      // If the UI has started tracing, don't display a message guiding the user
      // to start recording.
      return undefined;
    }
    return m(
      'div',
      m(
        'label',
        `To trace Chrome from the Perfetto UI you just have to press
         '${START_RECORDING_MESSAGE}'.`,
      ),
    );
  }
  return m(CodeSnippet, {text: getRecordCommand(recCfg, targetInfo)});
}

function getRecordCommand(
  recCfg: RecordConfig,
  targetInfo: TargetInfo,
): string {
  const recordCommand = recordConfigUtils.fetchLatestRecordCommand(
    recCfg,
    targetInfo,
  );

  const pbBase64 = recordCommand?.configProtoBase64 ?? '';
  const pbtx = recordCommand?.configProtoText ?? '';
  let cmd = '';
  if (
    targetInfo.targetType === 'ANDROID' &&
    targetInfo.androidApiLevel === 28
  ) {
    cmd += `echo '${pbBase64}' | \n`;
    cmd += 'base64 --decode | \n';
    cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n';
  } else {
    cmd +=
      targetInfo.targetType === 'ANDROID'
        ? 'adb shell perfetto \\\n'
        : 'perfetto \\\n';
    cmd += '  -c - --txt \\\n';
    cmd += '  -o /data/misc/perfetto-traces/trace \\\n';
    cmd += '<<EOF\n\n';
    cmd += pbtx;
    cmd += '\nEOF\n';
  }
  return cmd;
}

function RecordingButton(recCfg: RecordConfig) {
  if (
    controller.getState() !== RecordingState.TARGET_INFO_DISPLAYED ||
    !controller.canCreateTracingSession()
  ) {
    return undefined;
  }

  // We know we have a target because we checked the state.
  const targetInfo = assertExists(controller.getTargetInfo());
  const hasDataSources = recordConfigUtils.fetchLatestRecordCommand(
    recCfg,
    targetInfo,
  ).hasDataSources;
  if (!hasDataSources) {
    return undefined;
  }

  return m(
    '.button',
    m(
      'button',
      {
        class: 'selected',
        onclick: () => controller.onStartRecordingPressed(),
      },
      START_RECORDING_MESSAGE,
    ),
  );
}

function StopCancelButtons() {
  // Show the Stop/Cancel buttons only while we are recording a trace.
  if (!controller.shouldShowStopCancelButtons()) {
    return undefined;
  }

  const stop = m(
    `button.selected`,
    {onclick: () => controller.onStop()},
    'Stop',
  );

  const cancel = m(`button`, {onclick: () => controller.onCancel()}, 'Cancel');

  return [stop, cancel];
}

function recordMenu(routePage: string) {
  const chromeProbe = m(
    'a[href="#!/record/chrome"]',
    m(
      `li${routePage === 'chrome' ? '.active' : ''}`,
      m('i.material-icons', 'laptop_chromebook'),
      m('.title', 'Chrome'),
      m('.sub', 'Chrome traces'),
    ),
  );
  const cpuProbe = m(
    'a[href="#!/record/cpu"]',
    m(
      `li${routePage === 'cpu' ? '.active' : ''}`,
      m('i.material-icons', 'subtitles'),
      m('.title', 'CPU'),
      m('.sub', 'CPU usage, scheduling, wakeups'),
    ),
  );
  const gpuProbe = m(
    'a[href="#!/record/gpu"]',
    m(
      `li${routePage === 'gpu' ? '.active' : ''}`,
      m('i.material-icons', 'aspect_ratio'),
      m('.title', 'GPU'),
      m('.sub', 'GPU frequency, memory'),
    ),
  );
  const powerProbe = m(
    'a[href="#!/record/power"]',
    m(
      `li${routePage === 'power' ? '.active' : ''}`,
      m('i.material-icons', 'battery_charging_full'),
      m('.title', 'Power'),
      m('.sub', 'Battery and other energy counters'),
    ),
  );
  const memoryProbe = m(
    'a[href="#!/record/memory"]',
    m(
      `li${routePage === 'memory' ? '.active' : ''}`,
      m('i.material-icons', 'memory'),
      m('.title', 'Memory'),
      m('.sub', 'Physical mem, VM, LMK'),
    ),
  );
  const androidProbe = m(
    'a[href="#!/record/android"]',
    m(
      `li${routePage === 'android' ? '.active' : ''}`,
      m('i.material-icons', 'android'),
      m('.title', 'Android apps & svcs'),
      m('.sub', 'atrace and logcat'),
    ),
  );
  const advancedProbe = m(
    'a[href="#!/record/advanced"]',
    m(
      `li${routePage === 'advanced' ? '.active' : ''}`,
      m('i.material-icons', 'settings'),
      m('.title', 'Advanced settings'),
      m('.sub', 'Complicated stuff for wizards'),
    ),
  );
  const tracePerfProbe = m(
    'a[href="#!/record/tracePerf"]',
    m(
      `li${routePage === 'tracePerf' ? '.active' : ''}`,
      m('i.material-icons', 'full_stacked_bar_chart'),
      m('.title', 'Stack Samples'),
      m('.sub', 'Lightweight stack polling'),
    ),
  );
  const etwProbe = m(
    'a[href="#!/record/etw"]',
    m(
      `li${routePage === 'etw' ? '.active' : ''}`,
      m('i.material-icons', 'subtitles'),
      m('.title', 'ETW Tracing Config'),
      m('.sub', 'Context switch, Thread state'),
    ),
  );

  // We only display the probes when we have a valid target, so it's not
  // possible for the target to be undefined here.
  const targetType = assertExists(controller.getTargetInfo()).targetType;
  const probes = [];
  if (targetType === 'LINUX') {
    probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
  } else if (targetType === 'WINDOWS') {
    probes.push(chromeProbe, etwProbe);
  } else if (targetType === 'CHROME') {
    probes.push(chromeProbe);
  } else {
    probes.push(
      cpuProbe,
      gpuProbe,
      powerProbe,
      memoryProbe,
      androidProbe,
      chromeProbe,
      tracePerfProbe,
      advancedProbe,
    );
  }

  return m(
    '.record-menu',
    {
      class:
        controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
          ? 'disabled'
          : '',
      onclick: () => scheduleFullRedraw(),
    },
    m('header', 'Trace config'),
    m(
      'ul',
      m(
        'a[href="#!/record/buffers"]',
        m(
          `li${routePage === 'buffers' ? '.active' : ''}`,
          m('i.material-icons', 'tune'),
          m('.title', 'Recording settings'),
          m('.sub', 'Buffer mode, size and duration'),
        ),
      ),
      m(
        'a[href="#!/record/instructions"]',
        m(
          `li${routePage === 'instructions' ? '.active' : ''}`,
          m('i.material-icons-filled.rec', 'fiber_manual_record'),
          m('.title', 'Recording command'),
          m('.sub', 'Manually record trace'),
        ),
      ),
      m(
        'a[href="#!/record/config"]',
        {
          onclick: () => {
            recordConfigStore.reloadFromLocalStorage();
          },
        },
        m(
          `li${routePage === 'config' ? '.active' : ''}`,
          m('i.material-icons', 'save'),
          m('.title', 'Saved configs'),
          m('.sub', 'Manage local configs'),
        ),
      ),
    ),
    m('header', 'Probes'),
    m('ul', probes),
  );
}

function getRecordContainer(recMgr: RecordingManager, subpage?: string) {
  const recCfg = recMgr.state.recordConfig;
  const components: m.Children[] = [RecordHeader(recMgr)];
  if (controller.getState() === RecordingState.NO_TARGET) {
    components.push(m('.full-centered', 'Please connect a valid target.'));
    return m('.record-container', components);
  } else if (controller.getState() <= RecordingState.ASK_TO_FORCE_P1) {
    components.push(
      m(
        '.full-centered',
        'Can not access the device without resetting the ' +
          `connection. Please refresh the page, then click ` +
          `'${FORCE_RESET_MESSAGE}.'`,
      ),
    );
    return m('.record-container', components);
  } else if (controller.getState() === RecordingState.AUTH_P1) {
    components.push(
      m('.full-centered', 'Please allow USB debugging on the device.'),
    );
    return m('.record-container', components);
  } else if (
    controller.getState() === RecordingState.WAITING_FOR_TRACE_DISPLAY
  ) {
    components.push(
      m('.full-centered', 'Waiting for the trace to be collected.'),
    );
    return m('.record-container', components);
  }

  const pages: m.Children = [];
  // we need to remove the `/` character from the route
  let routePage = subpage ? subpage.substr(1) : '';
  if (!RECORDING_SECTIONS.includes(routePage)) {
    routePage = 'buffers';
  }
  pages.push(recordMenu(routePage));

  pages.push(
    m(RecordingSettings, {
      dataSources: [],
      cssClass: maybeGetActiveCss(routePage, 'buffers'),
      recState: recMgr.state,
    }),
  );
  pages.push(
    Instructions(recCfg, maybeGetActiveCss(routePage, 'instructions')),
  );
  pages.push(Configurations(recMgr, maybeGetActiveCss(routePage, 'config')));

  const settingsSections = new Map([
    ['cpu', CpuSettings],
    ['gpu', GpuSettings],
    ['power', PowerSettings],
    ['memory', MemorySettings],
    ['android', AndroidSettings],
    ['chrome', ChromeSettings],
    ['tracePerf', LinuxPerfSettings],
    ['advanced', AdvancedSettings],
    ['etw', EtwSettings],
  ]);
  for (const [section, component] of settingsSections.entries()) {
    pages.push(
      m(component, {
        dataSources: controller.getTargetInfo()?.dataSources || [],
        cssClass: maybeGetActiveCss(routePage, section),
        recState: recMgr.state,
      }),
    );
  }

  components.push(m('.record-container-content', pages));
  return m('.record-container', components);
}

export interface RecordPageV2Attrs extends PageAttrs {
  app: App;
  recCtl: RecordingPageController;
  recMgr: RecordingManager;
}

export class RecordPageV2 implements m.ClassComponent<RecordPageV2Attrs> {
  private lastSubpage: string | undefined = undefined;

  constructor({attrs}: m.CVnode<RecordPageV2Attrs>) {
    controller ??= attrs.recCtl;
    recordConfigUtils ??= new RecordingConfigUtils();
  }

  oninit({attrs}: m.CVnode<RecordPageV2Attrs>) {
    this.lastSubpage = attrs.subpage;
    if (attrs.subpage !== undefined && attrs.subpage.startsWith('/share/')) {
      const hash = attrs.subpage.substring(7);
      loadRecordConfig(attrs.recMgr, hash);
      attrs.app.navigate('#!/record/instructions');
    }
  }

  view({attrs}: m.CVnode<RecordPageV2Attrs>) {
    if (attrs.subpage !== this.lastSubpage) {
      this.lastSubpage = attrs.subpage;
      // TODO(primiano): this is a hack necesasry to retrigger the generation of
      // the record cmdline. Refactor this code once record v1 vs v2 is gone.
      attrs.recMgr.setRecordConfig(attrs.recMgr.state.recordConfig);
    }

    return m(
      '.record-page',
      controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
        ? m('.hider')
        : [],
      getRecordContainer(attrs.recMgr, attrs.subpage),
    );
  }
}
