// Copyright (C) 2018 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 {Duration, Time, TimeSpan, duration, time} from '../base/time';
import {colorForCpu} from '../components/colorizer';
import {timestampFormat} from '../core/timestamp_format';
import {
  OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
  TRACK_SHELL_WIDTH,
} from './css_constants';
import {BorderDragStrategy} from './drag/border_drag_strategy';
import {DragStrategy} from './drag/drag_strategy';
import {InnerDragStrategy} from './drag/inner_drag_strategy';
import {OuterDragStrategy} from './drag/outer_drag_strategy';
import {DragGestureHandler} from '../base/drag_gesture_handler';
import {
  getMaxMajorTicks,
  MIN_PX_PER_STEP,
  generateTicks,
  TickType,
} from './gridline_helper';
import {Size2D} from '../base/geom';
import {Panel} from './panel_container';
import {TimeScale} from '../base/time_scale';
import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
import {TraceImpl} from '../core/trace_impl';
import {LONG, NUM} from '../trace_processor/query_result';
import {raf} from '../core/raf_scheduler';
import {getOrCreate} from '../base/utils';
import {assertUnreachable} from '../base/logging';
import {TimestampFormat} from '../public/timeline';

const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>();

export class OverviewTimelinePanel implements Panel {
  private static HANDLE_SIZE_PX = 5;
  readonly kind = 'panel';
  readonly selectable = false;
  private width = 0;
  private gesture?: DragGestureHandler;
  private timeScale?: TimeScale;
  private dragStrategy?: DragStrategy;
  private readonly boundOnMouseMove = this.onMouseMove.bind(this);
  private readonly overviewData: OverviewDataLoader;

  constructor(private trace: TraceImpl) {
    this.overviewData = getOrCreate(
      tracesData,
      trace,
      () => new OverviewDataLoader(trace),
    );
  }

  // Must explicitly type now; arguments types are no longer auto-inferred.
  // https://github.com/Microsoft/TypeScript/issues/1373
  onupdate({dom}: m.CVnodeDOM) {
    this.width = dom.getBoundingClientRect().width;
    const traceTime = this.trace.traceInfo;
    if (this.width > TRACK_SHELL_WIDTH) {
      const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width};
      const hpTraceTime = HighPrecisionTimeSpan.fromTime(
        traceTime.start,
        traceTime.end,
      );
      this.timeScale = new TimeScale(hpTraceTime, pxBounds);
      if (this.gesture === undefined) {
        this.gesture = new DragGestureHandler(
          dom as HTMLElement,
          this.onDrag.bind(this),
          this.onDragStart.bind(this),
          this.onDragEnd.bind(this),
        );
      }
    } else {
      this.timeScale = undefined;
    }
  }

  oncreate(vnode: m.CVnodeDOM) {
    this.onupdate(vnode);
    (vnode.dom as HTMLElement).addEventListener(
      'mousemove',
      this.boundOnMouseMove,
    );
  }

  onremove({dom}: m.CVnodeDOM) {
    if (this.gesture) {
      this.gesture[Symbol.dispose]();
      this.gesture = undefined;
    }
    (dom as HTMLElement).removeEventListener(
      'mousemove',
      this.boundOnMouseMove,
    );
  }

  render(): m.Children {
    return m('.overview-timeline', {
      oncreate: (vnode) => this.oncreate(vnode),
      onupdate: (vnode) => this.onupdate(vnode),
      onremove: (vnode) => this.onremove(vnode),
    });
  }

  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
    if (this.width === undefined) return;
    if (this.timeScale === undefined) return;

    const headerHeight = 20;
    const tracksHeight = size.height - headerHeight;
    const traceContext = new TimeSpan(
      this.trace.traceInfo.start,
      this.trace.traceInfo.end,
    );

    if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) {
      const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
      const offset = this.trace.timeline.timestampOffset();
      const tickGen = generateTicks(traceContext, maxMajorTicks, offset);

      // Draw time labels
      ctx.font = '10px Roboto Condensed';
      ctx.fillStyle = '#999';
      for (const {type, time} of tickGen) {
        const xPos = Math.floor(this.timeScale.timeToPx(time));
        if (xPos <= 0) continue;
        if (xPos > this.width) break;
        if (type === TickType.MAJOR) {
          ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
          const domainTime = this.trace.timeline.toDomainTime(time);
          renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP);
        } else if (type == TickType.MEDIUM) {
          ctx.fillRect(xPos - 1, 0, 1, 8);
        } else if (type == TickType.MINOR) {
          ctx.fillRect(xPos - 1, 0, 1, 5);
        }
      }
    }

    // Draw mini-tracks with quanitzed density for each process.
    const overviewData = this.overviewData.overviewData;
    if (overviewData.size > 0) {
      const numTracks = overviewData.size;
      let y = 0;
      const trackHeight = (tracksHeight - 1) / numTracks;
      for (const key of overviewData.keys()) {
        const loads = overviewData.get(key)!;
        for (let i = 0; i < loads.length; i++) {
          const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start));
          const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end));
          const yOff = Math.floor(headerHeight + y * trackHeight);
          const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100);
          const color = colorForCpu(y).setHSL({s: 50, l: lightness});
          ctx.fillStyle = color.cssString;
          ctx.fillRect(xStart, yOff, xEnd - xStart, Math.ceil(trackHeight));
        }
        y++;
      }
    }

    // Draw bottom border.
    ctx.fillStyle = '#dadada';
    ctx.fillRect(0, size.height - 1, this.width, 1);

    // Draw semi-opaque rects that occlude the non-visible time range.
    const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale);

    ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR;
    ctx.fillRect(
      TRACK_SHELL_WIDTH - 1,
      headerHeight,
      vizStartPx - TRACK_SHELL_WIDTH,
      tracksHeight,
    );
    ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight);

    // Draw brushes.
    ctx.fillStyle = '#999';
    ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight);
    ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);

    const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX;
    const hbarHeight = tracksHeight * 0.4;
    // Draw handlebar
    ctx.fillRect(
      vizStartPx - Math.floor(hbarWidth / 2) - 1,
      headerHeight,
      hbarWidth,
      hbarHeight,
    );
    ctx.fillRect(
      vizEndPx - Math.floor(hbarWidth / 2),
      headerHeight,
      hbarWidth,
      hbarHeight,
    );
  }

  private onMouseMove(e: MouseEvent) {
    if (this.gesture === undefined || this.gesture.isDragging) {
      return;
    }
    (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX);
  }

  private chooseCursor(x: number) {
    if (this.timeScale === undefined) return 'default';
    const [startBound, endBound] = this.extractBounds(this.timeScale);
    if (
      OverviewTimelinePanel.inBorderRange(x, startBound) ||
      OverviewTimelinePanel.inBorderRange(x, endBound)
    ) {
      return 'ew-resize';
    } else if (x < TRACK_SHELL_WIDTH) {
      return 'default';
    } else if (x < startBound || endBound < x) {
      return 'crosshair';
    } else {
      return 'all-scroll';
    }
  }

  onDrag(x: number) {
    if (this.dragStrategy === undefined) return;
    this.dragStrategy.onDrag(x);
  }

  onDragStart(x: number) {
    if (this.timeScale === undefined) return;

    const cb = (vizTime: HighPrecisionTimeSpan) => {
      this.trace.timeline.updateVisibleTimeHP(vizTime);
      raf.scheduleCanvasRedraw();
    };
    const pixelBounds = this.extractBounds(this.timeScale);
    const timeScale = this.timeScale;
    if (
      OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
      OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])
    ) {
      this.dragStrategy = new BorderDragStrategy(timeScale, pixelBounds, cb);
    } else if (x < pixelBounds[0] || pixelBounds[1] < x) {
      this.dragStrategy = new OuterDragStrategy(timeScale, cb);
    } else {
      this.dragStrategy = new InnerDragStrategy(timeScale, pixelBounds, cb);
    }
    this.dragStrategy.onDragStart(x);
  }

  onDragEnd() {
    this.dragStrategy = undefined;
  }

  private extractBounds(timeScale: TimeScale): [number, number] {
    const vizTime = this.trace.timeline.visibleWindow;
    return [
      Math.floor(timeScale.hpTimeToPx(vizTime.start)),
      Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
    ];
  }

  private static inBorderRange(a: number, b: number): boolean {
    return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2;
  }
}

// Print a timestamp in the configured time format
function renderTimestamp(
  ctx: CanvasRenderingContext2D,
  time: time,
  x: number,
  y: number,
  minWidth: number,
): void {
  const fmt = timestampFormat();
  switch (fmt) {
    case TimestampFormat.UTC:
    case TimestampFormat.TraceTz:
    case TimestampFormat.Timecode:
      renderTimecode(ctx, time, x, y, minWidth);
      break;
    case TimestampFormat.TraceNs:
      ctx.fillText(time.toString(), x, y, minWidth);
      break;
    case TimestampFormat.TraceNsLocale:
      ctx.fillText(time.toLocaleString(), x, y, minWidth);
      break;
    case TimestampFormat.Seconds:
      ctx.fillText(Time.formatSeconds(time), x, y, minWidth);
      break;
    case TimestampFormat.Milliseconds:
      ctx.fillText(Time.formatMilliseconds(time), x, y, minWidth);
      break;
    case TimestampFormat.Microseconds:
      ctx.fillText(Time.formatMicroseconds(time), x, y, minWidth);
      break;
    default:
      assertUnreachable(fmt);
  }
}

// Print a timecode over 2 lines with this formatting:
// DdHH:MM:SS
// mmm uuu nnn
function renderTimecode(
  ctx: CanvasRenderingContext2D,
  time: time,
  x: number,
  y: number,
  minWidth: number,
): void {
  const timecode = Time.toTimecode(time);
  const {dhhmmss} = timecode;
  ctx.fillText(dhhmmss, x, y, minWidth);
}

interface QuantizedLoad {
  start: time;
  end: time;
  load: number;
}

// Kicks of a sequence of promises that load the overiew data in steps.
// Each step schedules an animation frame.
class OverviewDataLoader {
  overviewData = new Map<string, QuantizedLoad[]>();

  constructor(private trace: TraceImpl) {
    this.beginLoad();
  }

  async beginLoad() {
    const traceSpan = new TimeSpan(
      this.trace.traceInfo.start,
      this.trace.traceInfo.end,
    );
    const engine = this.trace.engine;
    const stepSize = Duration.max(1n, traceSpan.duration / 100n);
    const hasSchedSql = 'select ts from sched limit 1';
    const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
    if (hasSchedOverview) {
      await this.loadSchedOverview(traceSpan, stepSize);
    } else {
      await this.loadSliceOverview(traceSpan, stepSize);
    }
  }

  async loadSchedOverview(traceSpan: TimeSpan, stepSize: duration) {
    const stepPromises = [];
    for (
      let start = traceSpan.start;
      start < traceSpan.end;
      start = Time.add(start, stepSize)
    ) {
      const progress = start - traceSpan.start;
      const ratio = Number(progress) / Number(traceSpan.duration);
      this.trace.omnibox.showStatusMessage(
        'Loading overview ' + `${Math.round(ratio * 100)}%`,
      );
      const end = Time.add(start, stepSize);
      // The (async() => {})() queues all the 100 async promises in one batch.
      // Without that, we would wait for each step to be rendered before
      // kicking off the next one. That would interleave an animation frame
      // between each step, slowing down significantly the overall process.
      stepPromises.push(
        (async () => {
          const schedResult = await this.trace.engine.query(
            `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` +
              `where ts >= ${start} and ts < ${end} and utid != 0 ` +
              'group by cpu order by cpu',
          );
          const schedData: {[key: string]: QuantizedLoad} = {};
          const it = schedResult.iter({load: NUM, cpu: NUM});
          for (; it.valid(); it.next()) {
            const load = it.load;
            const cpu = it.cpu;
            schedData[cpu] = {start, end, load};
          }
          this.appendData(schedData);
        })(),
      );
    } // for(start = ...)
    await Promise.all(stepPromises);
  }

  async loadSliceOverview(traceSpan: TimeSpan, stepSize: duration) {
    // Slices overview.
    const sliceResult = await this.trace.engine.query(`select
            bucket,
            upid,
            ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
          from thread
          inner join (
            select
              ifnull(cast((ts - ${traceSpan.start})/${stepSize} as int), 0) as bucket,
              sum(dur) as utid_sum,
              utid
            from slice
            inner join thread_track on slice.track_id = thread_track.id
            group by bucket, utid
          ) using(utid)
          where upid is not null
          group by bucket, upid`);

    const slicesData: {[key: string]: QuantizedLoad[]} = {};
    const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
    for (; it.valid(); it.next()) {
      const bucket = it.bucket;
      const upid = it.upid;
      const load = it.load;

      const start = Time.add(traceSpan.start, stepSize * bucket);
      const end = Time.add(start, stepSize);

      const upidStr = upid.toString();
      let loadArray = slicesData[upidStr];
      if (loadArray === undefined) {
        loadArray = slicesData[upidStr] = [];
      }
      loadArray.push({start, end, load});
    }
    this.appendData(slicesData);
  }

  appendData(data: {[key: string]: QuantizedLoad | QuantizedLoad[]}) {
    for (const [key, value] of Object.entries(data)) {
      if (!this.overviewData.has(key)) {
        this.overviewData.set(key, []);
      }
      if (value instanceof Array) {
        this.overviewData.get(key)!.push(...value);
      } else {
        this.overviewData.get(key)!.push(value);
      }
    }
    raf.scheduleCanvasRedraw();
  }
}
