// 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 {EngineProxy} from '../trace_processor/engine';
import {STR} from '../trace_processor/query_result';

const CACHED_SCHEMAS = new WeakMap<EngineProxy, DatabaseSchema>();

export class SchemaError extends Error {
  constructor(message: string) {
    super(message);
  }
}

// POJO representing the table structure of trace_processor.
// Exposed for testing.
export interface DatabaseInfo {
  tables: TableInfo[];
}

interface TableInfo {
  name: string;
  parent?: TableInfo;
  columns: ColumnInfo[];
}

interface ColumnInfo {
  name: string;
}

async function getColumns(
    engine: EngineProxy, table: string): Promise<ColumnInfo[]> {
  const result = await engine.query(`PRAGMA table_info(${table});`);
  const it = result.iter({
    name: STR,
  });
  const columns = [];
  for (; it.valid(); it.next()) {
    columns.push({name: it['name']});
  }
  return columns;
}

// Opinionated view on the schema of the given trace_processor instance
// suitable for EventSets to use for query generation.
export class DatabaseSchema {
  private tableToKeys: Map<string, Set<string>>;

  constructor(info: DatabaseInfo) {
    this.tableToKeys = new Map();
    for (const tableInfo of info.tables) {
      const columns = new Set(tableInfo.columns.map((c) => c.name));
      this.tableToKeys.set(tableInfo.name, columns);
    }
  }

  // Return all the EventSet keys available for a given table. This
  // includes the direct columns on the table (and all parent tables)
  // as well as all direct and indirect joinable tables where the join
  // is N:1 or 1:1. e.g. for the table thread_slice we also include
  // the columns from thread, process, thread_track etc.
  getKeys(tableName: string): Set<string> {
    const columns = this.tableToKeys.get(tableName);
    if (columns === undefined) {
      throw new SchemaError(`Unknown table '${tableName}'`);
    }
    return columns;
  }
}

// Deliberately not exported. Users should call getSchema below and
// participate in cacheing.
async function createSchema(engine: EngineProxy): Promise<DatabaseSchema> {
  const tables: TableInfo[] = [];
  const result = await engine.query(`SELECT name from perfetto_tables;`);
  const it = result.iter({
    name: STR,
  });
  for (; it.valid(); it.next()) {
    const name = it['name'];
    tables.push({
      name,
      columns: await getColumns(engine, name),
    });
  }

  const database: DatabaseInfo = {
    tables,
  };

  return new DatabaseSchema(database);
}

// Get the schema for the given engine (from the cache if possible).
// The schemas are per-engine (i.e. they can't be statically determined
// at build time) since we might be in httpd mode and not-running
// against the version of trace_processor we build with.
export async function getSchema(engine: EngineProxy): Promise<DatabaseSchema> {
  const schema = CACHED_SCHEMAS.get(engine);
  if (schema === undefined) {
    const newSchema = await createSchema(engine);
    CACHED_SCHEMAS.set(engine, newSchema);
    return newSchema;
  }
  return schema;
}
