// Copyright (C) 2024 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 {time, Time} from '../../base/time';
import {colorForFtrace} from '../../components/colorizer';
import {DetailsShell} from '../../widgets/details_shell';
import {
  MultiSelectDiff,
  Option as MultiSelectOption,
  PopupMultiSelect,
} from '../../widgets/multiselect';
import {PopupPosition} from '../../widgets/popup';
import {Timestamp} from '../../components/widgets/timestamp';
import {FtraceFilter, FtraceStat} from './common';
import {Engine} from '../../trace_processor/engine';
import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
import {AsyncLimiter} from '../../base/async_limiter';
import {Monitor} from '../../base/monitor';
import {Button} from '../../widgets/button';
import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
import {Store} from '../../base/store';
import {Trace} from '../../public/trace';

const ROW_H = 20;

interface FtraceExplorerAttrs {
  cache: FtraceExplorerCache;
  filterStore: Store<FtraceFilter>;
  trace: Trace;
}

interface FtraceEvent {
  id: number;
  ts: time;
  name: string;
  cpu: number;
  thread: string | null;
  process: string | null;
  args: string;
}

interface FtracePanelData {
  events: FtraceEvent[];
  offset: number;
  numEvents: number; // Number of events in the visible window
}

interface Pagination {
  offset: number;
  count: number;
}

export interface FtraceExplorerCache {
  state: 'blank' | 'loading' | 'valid';
  counters: FtraceStat[];
}

async function getFtraceCounters(engine: Engine): Promise<FtraceStat[]> {
  // TODO(stevegolton): this is an extraordinarily slow query on large traces
  // as it goes through every ftrace event which can be a lot on big traces.
  // Consider if we can have some different UX which avoids needing these
  // counts
  // TODO(mayzner): the +name below is an awful hack to workaround
  // extraordinarily slow sorting of strings. However, even with this hack,
  // this is just a slow query. There are various ways we can improve this
  // (e.g. with using the vtab_distinct APIs of SQLite).
  const result = await engine.query(`
    select
      name,
      count(1) as cnt
    from ftrace_event
    group by name
    order by cnt desc
  `);
  const counters: FtraceStat[] = [];
  const it = result.iter({name: STR, cnt: NUM});
  for (let row = 0; it.valid(); it.next(), row++) {
    counters.push({name: it.name, count: it.cnt});
  }
  return counters;
}

export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> {
  private pagination: Pagination = {
    offset: 0,
    count: 0,
  };
  private readonly monitor: Monitor;
  private readonly queryLimiter = new AsyncLimiter();

  // A cache of the data we have most recently loaded from our store
  private data?: FtracePanelData;

  constructor({attrs}: m.CVnode<FtraceExplorerAttrs>) {
    this.monitor = new Monitor([
      () => attrs.trace.timeline.visibleWindow.toTimeSpan().start,
      () => attrs.trace.timeline.visibleWindow.toTimeSpan().end,
      () => attrs.filterStore.state,
    ]);

    if (attrs.cache.state === 'blank') {
      getFtraceCounters(attrs.trace.engine)
        .then((counters) => {
          attrs.cache.counters = counters;
          attrs.cache.state = 'valid';
        })
        .catch(() => {
          attrs.cache.state = 'blank';
        });
      attrs.cache.state = 'loading';
    }
  }

  view({attrs}: m.CVnode<FtraceExplorerAttrs>) {
    this.monitor.ifStateChanged(() => {
      this.reloadData(attrs);
    });

    return m(
      DetailsShell,
      {
        title: this.renderTitle(),
        buttons: this.renderFilterPanel(attrs),
        fillParent: true,
      },
      m(VirtualTable, {
        className: 'pf-ftrace-explorer',
        columns: [
          {header: 'ID', width: '5em'},
          {header: 'Timestamp', width: '13em'},
          {header: 'Name', width: '24em'},
          {header: 'CPU', width: '3em'},
          {header: 'Process', width: '24em'},
          {header: 'Args', width: '200em'},
        ],
        firstRowOffset: this.data?.offset ?? 0,
        numRows: this.data?.numEvents ?? 0,
        rowHeight: ROW_H,
        rows: this.renderData(),
        onReload: (offset, count) => {
          this.pagination = {offset, count};
          this.reloadData(attrs);
        },
        onRowHover: (id) => {
          const event = this.data?.events.find((event) => event.id === id);
          if (event) {
            attrs.trace.timeline.hoverCursorTimestamp = event.ts;
          }
        },
        onRowOut: () => {
          attrs.trace.timeline.hoverCursorTimestamp = undefined;
        },
      }),
    );
  }

  private reloadData(attrs: FtraceExplorerAttrs): void {
    this.queryLimiter.schedule(async () => {
      this.data = await lookupFtraceEvents(
        attrs.trace,
        this.pagination.offset,
        this.pagination.count,
        attrs.filterStore.state,
      );
      attrs.trace.scheduleFullRedraw();
    });
  }

  private renderData(): VirtualTableRow[] {
    if (!this.data) {
      return [];
    }

    return this.data.events.map((event) => {
      const {ts, name, cpu, process, args, id} = event;
      const timestamp = m(Timestamp, {ts});
      const color = colorForFtrace(name).base.cssString;

      return {
        id,
        cells: [
          id,
          timestamp,
          m(
            '.pf-ftrace-namebox',
            m('.pf-ftrace-colorbox', {style: {background: color}}),
            name,
          ),
          cpu,
          process,
          args,
        ],
      };
    });
  }

  private renderTitle() {
    if (this.data) {
      const {numEvents} = this.data;
      return `Ftrace Events (${numEvents})`;
    } else {
      return 'Ftrace Events';
    }
  }

  private renderFilterPanel(attrs: FtraceExplorerAttrs) {
    if (attrs.cache.state !== 'valid') {
      return m(Button, {
        label: 'Filter',
        disabled: true,
        loading: true,
      });
    }

    const excludeList = attrs.filterStore.state.excludeList;
    const options: MultiSelectOption[] = attrs.cache.counters.map(
      ({name, count}) => {
        return {
          id: name,
          name: `${name} (${count})`,
          checked: !excludeList.some((excluded: string) => excluded === name),
        };
      },
    );

    return m(PopupMultiSelect, {
      label: 'Filter',
      icon: 'filter_list_alt',
      popupPosition: PopupPosition.Top,
      options,
      onChange: (diffs: MultiSelectDiff[]) => {
        const newList = new Set<string>(excludeList);
        diffs.forEach(({checked, id}) => {
          if (checked) {
            newList.delete(id);
          } else {
            newList.add(id);
          }
        });
        attrs.filterStore.edit((draft) => {
          draft.excludeList = Array.from(newList);
        });
      },
    });
  }
}

async function lookupFtraceEvents(
  trace: Trace,
  offset: number,
  count: number,
  filter: FtraceFilter,
): Promise<FtracePanelData> {
  const {start, end} = trace.timeline.visibleWindow.toTimeSpan();

  const excludeList = filter.excludeList;
  const excludeListSql = excludeList.map((s) => `'${s}'`).join(',');

  // TODO(stevegolton): This query can be slow when traces are huge.
  // The number of events is only used for correctly sizing the panel's
  // scroll container so that the scrollbar works as if the panel were fully
  // populated.
  // Perhaps we could work out some UX that doesn't need this.
  let queryRes = await trace.engine.query(`
    select count(id) as numEvents
    from ftrace_event
    where
      ftrace_event.name not in (${excludeListSql}) and
      ts >= ${start} and ts <= ${end}
    `);
  const {numEvents} = queryRes.firstRow({numEvents: NUM});

  queryRes = await trace.engine.query(`
    select
      ftrace_event.id as id,
      ftrace_event.ts as ts,
      ftrace_event.name as name,
      ftrace_event.cpu as cpu,
      thread.name as thread,
      process.name as process,
      to_ftrace(ftrace_event.id) as args
    from ftrace_event
    join thread using (utid)
    left join process on thread.upid = process.upid
    where
      ftrace_event.name not in (${excludeListSql}) and
      ts >= ${start} and ts <= ${end}
    order by id
    limit ${count} offset ${offset};`);
  const events: FtraceEvent[] = [];
  const it = queryRes.iter({
    id: NUM,
    ts: LONG,
    name: STR,
    cpu: NUM,
    thread: STR_NULL,
    process: STR_NULL,
    args: STR,
  });
  for (let row = 0; it.valid(); it.next(), row++) {
    events.push({
      id: it.id,
      ts: Time.fromRaw(it.ts),
      name: it.name,
      cpu: it.cpu,
      thread: it.thread,
      process: it.process,
      args: it.args,
    });
  }
  return {events, offset, numEvents};
}
