// Copyright (C) 2023 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 {Span, Time} from '../base/time';
import {
  HighPrecisionTime,
  HighPrecisionTimeSpan,
} from '../common/high_precision_time';
import {FtraceFilterState, Pagination} from '../common/state';
import {FtraceEvent, globals} from '../frontend/globals';
import {publishFtracePanelData} from '../frontend/publish';
import {ratelimit} from '../frontend/rate_limiters';
import {Engine} from '../trace_processor/engine';
import {LONG, NUM, STR, STR_NULL} from '../trace_processor/query_result';

import {Controller} from './controller';

export interface FtraceControllerArgs {
  engine: Engine;
}

interface RetVal {
  events: FtraceEvent[];
  offset: number;
  numEvents: number;
}

export class FtraceController extends Controller<'main'> {
  private engine: Engine;
  private oldSpan: Span<HighPrecisionTime> = HighPrecisionTimeSpan.ZERO;
  private oldFtraceFilter?: FtraceFilterState;
  private oldPagination?: Pagination;

  constructor({engine}: FtraceControllerArgs) {
    super('main');
    this.engine = engine;
  }

  run() {
    if (this.shouldUpdate()) {
      this.oldSpan = globals.timeline.visibleWindowTime;
      this.oldFtraceFilter = globals.state.ftraceFilter;
      this.oldPagination = globals.state.ftracePagination;
      if (globals.state.ftracePagination.count > 0) {
        this.lookupFtraceEventsRateLimited();
      }
    }
  }

  private lookupFtraceEventsRateLimited = ratelimit(() => {
    const {offset, count} = globals.state.ftracePagination;
    // The formatter doesn't like formatted chained methods :(
    const promise = this.lookupFtraceEvents(offset, count);
    promise.then(({events, offset, numEvents}: RetVal) => {
      publishFtracePanelData({events, offset, numEvents});
    });
  }, 250);

  private shouldUpdate(): boolean {
    // Has the visible window moved?
    const visibleWindow = globals.timeline.visibleWindowTime;
    if (!this.oldSpan.equals(visibleWindow)) {
      return true;
    }

    // Has the pagination changed?
    if (this.oldPagination !== globals.state.ftracePagination) {
      return true;
    }

    // Has the filter changed?
    if (this.oldFtraceFilter !== globals.state.ftraceFilter) {
      return true;
    }

    return false;
  }

  async lookupFtraceEvents(offset: number, count: number): Promise<RetVal> {
    const appState = globals.state;
    const {start, end} = globals.stateVisibleTime();

    const excludeList = appState.ftraceFilter.excludedNames;
    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 this.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 this.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
      left join thread
      on ftrace_event.utid = thread.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};
  }
};
