// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size 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 {findRef} from '../base/dom_utils';
import {assertExists, assertTrue} from '../base/logging';
import {Time} from '../base/time';
import {Actions} from '../common/actions';
import {viewingOptions} from '../common/flamegraph_util';
import {
  CallsiteInfo,
  FlamegraphStateViewingOption,
  ProfileType,
} from '../common/state';
import {profileType} from '../controller/flamegraph_controller';
import {raf} from '../core/raf_scheduler';
import {Button} from '../widgets/button';
import {Icon} from '../widgets/icon';
import {Modal, ModalAttrs} from '../widgets/modal';
import {Popup} from '../widgets/popup';

import {Flamegraph, NodeRendering} from './flamegraph';
import {globals} from './globals';
import {debounce} from './rate_limiters';
import {Router} from './router';
import {getCurrentTrace} from './sidebar';
import {convertTraceToPprofAndDownload} from './trace_converter';
import {DurationWidget} from './widgets/duration';

const HEADER_HEIGHT = 30;

function toSelectedCallsite(c: CallsiteInfo|undefined): string {
  if (c !== undefined && c.name !== undefined) {
    return c.name;
  }
  return '(none)';
}

const RENDER_SELF_AND_TOTAL: NodeRendering = {
  selfSize: 'Self',
  totalSize: 'Total',
};
const RENDER_OBJ_COUNT: NodeRendering = {
  selfSize: 'Self objects',
  totalSize: 'Subtree objects',
};

export class FlamegraphDetailsPanel implements m.ClassComponent {
  private profileType?: ProfileType = undefined;
  private ts = Time.ZERO;
  private pids: number[] = [];
  private flamegraph: Flamegraph = new Flamegraph([]);
  private focusRegex = '';
  private updateFocusRegexDebounced = debounce(() => {
    this.updateFocusRegex();
  }, 20);
  private canvas?: HTMLCanvasElement;

  view() {
    const flamegraphDetails = globals.flamegraphDetails;
    if (flamegraphDetails.type !== undefined &&
        flamegraphDetails.start !== undefined &&
        flamegraphDetails.dur !== undefined &&
        flamegraphDetails.pids !== undefined &&
        flamegraphDetails.upids !== undefined) {
      this.profileType = profileType(flamegraphDetails.type);
      this.ts = Time.add(flamegraphDetails.start, flamegraphDetails.dur);
      this.pids = flamegraphDetails.pids;
      if (flamegraphDetails.flamegraph) {
        this.flamegraph.updateDataIfChanged(
            this.nodeRendering(), flamegraphDetails.flamegraph);
      }
      const height = flamegraphDetails.flamegraph ?
          this.flamegraph.getHeight() + HEADER_HEIGHT :
          0;
      return m(
          '.details-panel',
          this.maybeShowModal(flamegraphDetails.graphIncomplete),
          m('.details-panel-heading.flamegraph-profile',
            {onclick: (e: MouseEvent) => e.stopPropagation()},
            [
              m('div.options',
                [
                  m('div.title',
                    this.getTitle(),
                    (this.profileType === ProfileType.MIXED_HEAP_PROFILE) &&
                        m(Popup,
                          {
                            trigger: m(Icon, {icon: 'warning'}),
                          },
                          m('',
                            {style: {width: '300px'}},
                            'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.')),
                    ':'),
                  this.getViewingOptionButtons(),
                ]),
              m('div.details',
                [
                  m('div.selected',
                    `Selected function: ${
                        toSelectedCallsite(
                            flamegraphDetails.expandedCallsite)}`),
                  m('div.time',
                    `Snapshot time: `,
                    m(DurationWidget, {dur: flamegraphDetails.dur})),
                  m('input[type=text][placeholder=Focus]', {
                    oninput: (e: Event) => {
                      const target = (e.target as HTMLInputElement);
                      this.focusRegex = target.value;
                      this.updateFocusRegexDebounced();
                    },
                    // Required to stop hot-key handling:
                    onkeydown: (e: Event) => e.stopPropagation(),
                  }),
                  (this.profileType === ProfileType.NATIVE_HEAP_PROFILE ||
                   this.profileType === ProfileType.JAVA_HEAP_SAMPLES) &&
                      m(Button, {
                        icon: 'file_download',
                        onclick: () => {
                          this.downloadPprof();
                        },
                      }),
                ]),
            ]),
          m(`canvas[ref=canvas]`, {
            style: `height:${height}px; width:100%`,
            onmousemove: (e: MouseEvent) => {
              const {offsetX, offsetY} = e;
              this.onMouseMove({x: offsetX, y: offsetY});
            },
            onmouseout: () => {
              this.onMouseOut();
            },
            onclick: (e: MouseEvent) => {
              const {offsetX, offsetY} = e;
              this.onMouseClick({x: offsetX, y: offsetY});
            },
          }),
      );
    } else {
      return m(
          '.details-panel',
          m('.details-panel-heading', m('h2', `Flamegraph Profile`)));
    }
  }


  private maybeShowModal(graphIncomplete?: boolean) {
    if (!graphIncomplete || globals.state.flamegraphModalDismissed) {
      return undefined;
    }
    return m(Modal, {
      title: 'The flamegraph is incomplete',
      vAlign: 'TOP',
      content:
          m('div', 'The current trace does not have a fully formed flamegraph'),
      buttons: [
        {
          text: 'Show the errors',
          primary: true,
          action: () => Router.navigate('#!/info'),
        },
        {
          text: 'Skip',
          action: () => {
            globals.dispatch(Actions.dismissFlamegraphModal({}));
            raf.scheduleFullRedraw();
          },
        },
      ],
    } as ModalAttrs);
  }

  private getTitle(): string {
    const profileType = this.profileType!;
    switch (profileType) {
      case ProfileType.MIXED_HEAP_PROFILE:
        return 'Mixed heap profile';
      case ProfileType.HEAP_PROFILE:
        return 'Heap profile';
      case ProfileType.NATIVE_HEAP_PROFILE:
        return 'Native heap profile';
      case ProfileType.JAVA_HEAP_SAMPLES:
        return 'Java heap samples';
      case ProfileType.JAVA_HEAP_GRAPH:
        return 'Java heap graph';
      case ProfileType.PERF_SAMPLE:
        return 'Profile';
      default:
        throw new Error('unknown type');
    }
  }

  private nodeRendering(): NodeRendering {
    if (this.profileType === undefined) {
      return {};
    }
    const profileType = this.profileType;
    const viewingOption: FlamegraphStateViewingOption =
        globals.state.currentFlamegraphState!.viewingOption;
    switch (profileType) {
      case ProfileType.JAVA_HEAP_GRAPH:
        if (viewingOption ===
            FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY) {
          return RENDER_OBJ_COUNT;
        } else {
          return RENDER_SELF_AND_TOTAL;
        }
      case ProfileType.MIXED_HEAP_PROFILE:
      case ProfileType.HEAP_PROFILE:
      case ProfileType.NATIVE_HEAP_PROFILE:
      case ProfileType.JAVA_HEAP_SAMPLES:
      case ProfileType.PERF_SAMPLE:
        return RENDER_SELF_AND_TOTAL;
      default:
        const exhaustiveCheck: never = profileType;
        throw new Error(`Unhandled case: ${exhaustiveCheck}`);
    }
  }

  private updateFocusRegex() {
    globals.dispatch(Actions.changeFocusFlamegraphState({
      focusRegex: this.focusRegex,
    }));
  }

  getViewingOptionButtons(): m.Children {
    return m(
        'div',
        ...FlamegraphDetailsPanel.selectViewingOptions(
            assertExists(this.profileType)));
  }

  downloadPprof() {
    const engine = globals.getCurrentEngine();
    if (!engine) return;
    getCurrentTrace()
        .then((file) => {
          assertTrue(
              this.pids.length === 1,
              'Native profiles can only contain one pid.');
          convertTraceToPprofAndDownload(file, this.pids[0], this.ts);
        })
        .catch((error) => {
          throw new Error(`Failed to get current trace ${error}`);
        });
  }

  private changeFlamegraphData() {
    const data = globals.flamegraphDetails;
    const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph;
    this.flamegraph.updateDataIfChanged(
        this.nodeRendering(), flamegraphData, data.expandedCallsite);
  }

  oncreate({dom}: m.CVnodeDOM) {
    this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom);
    // TODO(stevegolton): If we truely want to be standalone, then we shouldn't
    // rely on someone else calling the rafScheduler when the window is resized,
    // but it's good enough for now as we know the ViewerPage will do it.
    raf.addRedrawCallback(this.rafRedrawCallback);
  }

  onupdate({dom}: m.CVnodeDOM) {
    this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom);
  }

  onremove(_vnode: m.CVnodeDOM) {
    raf.removeRedrawCallback(this.rafRedrawCallback);
  }

  private static findCanvasElement(dom: Element): HTMLCanvasElement|undefined {
    const canvas = findRef(dom, 'canvas');
    if (canvas && canvas instanceof HTMLCanvasElement) {
      return canvas;
    } else {
      return undefined;
    }
  }

  private rafRedrawCallback = () => {
    if (this.canvas) {
      const canvas = this.canvas;
      canvas.width = canvas.offsetWidth * devicePixelRatio;
      canvas.height = canvas.offsetHeight * devicePixelRatio;
      const ctx = canvas.getContext('2d');
      if (ctx) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.save();
        ctx.scale(devicePixelRatio, devicePixelRatio);
        const {offsetWidth: width, offsetHeight: height} = canvas;
        this.renderLocalCanvas(ctx, width, height);
        ctx.restore();
      }
    }
  };

  private renderLocalCanvas(
      ctx: CanvasRenderingContext2D, width: number, height: number) {
    this.changeFlamegraphData();
    const current = globals.state.currentFlamegraphState;
    if (current === null) return;
    const unit = current.viewingOption ===
                FlamegraphStateViewingOption
                    .SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
            current.viewingOption ===
                FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
        'B' :
        '';
    this.flamegraph.draw(ctx, width, height, 0, 0, unit);
  }

  private onMouseClick({x, y}: {x: number, y: number}): boolean {
    const expandedCallsite = this.flamegraph.onMouseClick({x, y});
    globals.dispatch(Actions.expandFlamegraphState({expandedCallsite}));
    return true;
  }

  private onMouseMove({x, y}: {x: number, y: number}): boolean {
    this.flamegraph.onMouseMove({x, y});
    raf.scheduleFullRedraw();
    return true;
  }

  private onMouseOut() {
    this.flamegraph.onMouseOut();
    raf.scheduleFullRedraw();
  }

  private static selectViewingOptions(profileType: ProfileType) {
    const ret = [];
    for (const {option, name} of viewingOptions(profileType)) {
      ret.push(this.buildButtonComponent(option, name));
    }
    return ret;
  }

  private static buildButtonComponent(
      viewingOption: FlamegraphStateViewingOption, text: string) {
    const active =
        (globals.state.currentFlamegraphState !== null &&
         globals.state.currentFlamegraphState.viewingOption === viewingOption);
    return m(Button, {
      label: text,
      active,
      minimal: true,
      onclick: () => {
        globals.dispatch(Actions.changeViewFlamegraphState({viewingOption}));
      },
    });
  }
}
