# Copyright (C) 2022 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 dataclasses
from dataclasses import dataclass
import importlib
from typing import Dict
from typing import List
from typing import Set
from typing import Optional
from typing import Union

from python.generators.trace_processor_table.public import Alias
from python.generators.trace_processor_table.public import Column
from python.generators.trace_processor_table.public import ColumnDoc
from python.generators.trace_processor_table.public import ColumnFlag
from python.generators.trace_processor_table.public import CppAccess
from python.generators.trace_processor_table.public import CppAccessDuration
from python.generators.trace_processor_table.public import CppColumnType
from python.generators.trace_processor_table.public import CppDouble
from python.generators.trace_processor_table.public import CppInt32
from python.generators.trace_processor_table.public import CppInt64
from python.generators.trace_processor_table.public import CppOptional
from python.generators.trace_processor_table.public import CppSelfTableId
from python.generators.trace_processor_table.public import CppString
from python.generators.trace_processor_table.public import CppTableId
from python.generators.trace_processor_table.public import CppUint32
from python.generators.trace_processor_table.public import SqlAccess
from python.generators.trace_processor_table.public import Table


@dataclass
class ParsedType:
  """Result of parsing a CppColumnType into its parts."""
  raw_cpp_type: str
  is_optional: bool = False
  is_alias: bool = False
  alias_underlying_name: Optional[str] = None
  is_self_id: bool = False
  id_table: Optional[Table] = None

  def is_no_transform_id(self) -> bool:
    """Returns true if this is a no-transform id."""
    # ThreadTable and ProcessTable are special for legacy reasons as they were
    # around even before the advent of C++ macro tables. Because of this a lot
    # of code was written assuming that upid and utid were uint32 (e.g. indexing
    # directly into vectors using them) and it was decided this behaviour was
    # too expensive in engineering cost to fix given the trivial benefit. For
    # this reason, continue to maintain this illusion.
    return self.id_table is not None and self.id_table.class_name in (
        'ThreadTable',
        'ProcessTable',
    )

  def cpp_type(self) -> str:
    if self.id_table and self.is_no_transform_id():
      return 'uint32_t'
    return self.raw_cpp_type

  def cpp_type_with_optionality(self) -> str:
    """Returns the C++ type wrapping with base::Optional if necessary."""
    if self.is_optional:
      return f'std::optional<{self.cpp_type()}>'
    return self.cpp_type()


@dataclass(frozen=True)
class ParsedColumn:
  """Representation of a column parsed from a Python definition."""

  column: Column
  doc: Optional[ColumnDoc]

  # Whether this column is the implicit "id" column which is added by while
  # parsing the tables rather than by the user.
  is_implicit_id: bool = False

  # Whether this column comes from copying a column from the ancestor. If this
  # is set to false, the user explicitly specified it for this table.
  is_ancestor: bool = False


@dataclass(frozen=True)
class ParsedTable:
  """Representation of a table parsed from a Python definition."""

  table: Table
  columns: List[ParsedColumn]


def parse_type_with_cols(table: Table, cols: List[Column],
                         col_type: CppColumnType) -> ParsedType:
  """Parses a CppColumnType into its constiuent parts."""

  if isinstance(col_type, CppInt64):
    return ParsedType('int64_t')
  if isinstance(col_type, CppInt32):
    return ParsedType('int32_t')
  if isinstance(col_type, CppUint32):
    return ParsedType('uint32_t')
  if isinstance(col_type, CppDouble):
    return ParsedType('double')
  if isinstance(col_type, CppString):
    return ParsedType('StringPool::Id')

  if isinstance(col_type, Alias):
    col = next(c for c in cols if c.name == col_type.underlying_column)
    return ParsedType(
        parse_type(table, col.type).raw_cpp_type,
        is_alias=True,
        alias_underlying_name=col.name)

  if isinstance(col_type, CppTableId):
    return ParsedType(
        f'{col_type.table.class_name}::Id', id_table=col_type.table)

  if isinstance(col_type, CppSelfTableId):
    return ParsedType(
        f'{table.class_name}::Id', is_self_id=True, id_table=table)

  if isinstance(col_type, CppOptional):
    inner = parse_type(table, col_type.inner)
    assert not inner.is_optional, 'Nested optional not allowed'
    return dataclasses.replace(inner, is_optional=True)

  raise Exception(f'Unknown type {col_type}')


def parse_type(table: Table, col_type: CppColumnType) -> ParsedType:
  """Parses a CppColumnType into its constiuent parts."""
  return parse_type_with_cols(table, table.columns, col_type)


def typed_column_type(table: Table, col: ParsedColumn) -> str:
  """Returns the TypedColumn/IdColumn C++ type for a given column."""

  parsed = parse_type(table, col.column.type)
  if col.is_implicit_id:
    return f'IdColumn<{parsed.raw_cpp_type}>'
  return f'TypedColumn<{parsed.cpp_type_with_optionality()}>'


def data_layer_type(table: Table, col: ParsedColumn) -> str:
  """Returns the DataLayer C++ type for a given column."""

  parsed = parse_type(table, col.column.type)
  if col.is_implicit_id:
    return 'column::IdStorage'
  if parsed.raw_cpp_type == 'StringPool::Id':
    return 'column::StringStorage'
  if ColumnFlag.SET_ID in col.column.flags:
    return 'column::SetIdStorage'
  return f'column::NumericStorage<ColumnType::{col.column.name}::non_optional_stored_type>'


def find_table_deps(table: Table) -> List[Table]:
  """Finds all the other table class names this table depends on.

  By "depends", we mean this table in C++ would need the dependency to be
  defined (or included) before this table is defined."""

  deps: Dict[str, Table] = {}
  if table.parent:
    deps[table.parent.class_name] = table.parent
  for c in table.columns:
    # Aliases cannot have dependencies so simply ignore them: trying to parse
    # them before adding implicit columns can cause issues.
    if isinstance(c.type, Alias):
      continue
    id_table = parse_type(table, c.type).id_table
    if id_table:
      deps[id_table.class_name] = id_table
  return list(deps.values())


def public_sql_name(table: Table) -> str:
  """Extracts SQL name for the table which should be publicised."""

  wrapping_view = table.wrapping_sql_view
  return wrapping_view.view_name if wrapping_view else table.sql_name


def _create_implicit_columns_for_root(table: Table) -> List[ParsedColumn]:
  """Given a root table, returns the implicit id and type columns."""
  assert table.add_implicit_column

  sql_name = public_sql_name(table)
  id_doc = table.tabledoc.columns.get('id') if table.tabledoc else None
  return [
      ParsedColumn(
          Column(
              'id',
              CppSelfTableId(),
              ColumnFlag.SORTED,
              sql_access=SqlAccess.HIGH_PERF,
              cpp_access=CppAccess.READ,
              cpp_access_duration=CppAccessDuration.POST_FINALIZATION,
          ),
          _to_column_doc(id_doc) if id_doc else ColumnDoc(
              doc=f'Unique identifier for this {sql_name}.'),
          is_implicit_id=True),
  ]


def _topological_sort_table_and_deps(parsed: List[Table]) -> List[Table]:
  """Topologically sorts a list of tables (i.e. dependenices appear earlier).

  See [1] for information on a topological sort. We do this to allow
  dependencies to be processed and appear ealier than their dependents.

  [1] https://en.wikipedia.org/wiki/Topological_sorting"""
  visited: Set[str] = set()
  result: List[Table] = []

  # Topological sorting is really just a DFS where we put the nodes in the list
  # after any dependencies.
  def dfs(t: Table):
    if t.class_name in visited:
      return
    visited.add(t.class_name)

    for dep in find_table_deps(t):
      dfs(dep)
    result.append(t)

  for p in parsed:
    dfs(p)
  return result


def _to_column_doc(doc: Union[ColumnDoc, str, None]) -> Optional[ColumnDoc]:
  """Cooerces a user specified ColumnDoc or string into a ColumnDoc."""

  if doc is None or isinstance(doc, ColumnDoc):
    return doc
  return ColumnDoc(doc=doc)


def parse_tables_from_modules(modules: List[str]) -> List[ParsedTable]:
  """Creates a list of tables with the associated paths."""

  # Create a mapping from the table to a "parsed" version of the table.
  tables: Dict[str, Table] = {}
  for module in modules:
    imported = importlib.import_module(module)
    run_tables: List[Table] = imported.__dict__['ALL_TABLES']
    for table in run_tables:
      existing_table = tables.get(table.class_name)
      assert not existing_table or existing_table == table
      tables[table.class_name] = table

  # Sort all the tables: note that this list may include tables which are not
  # in |tables| dictionary due to dependencies on tables which live in a file
  # not covered by |input_paths|.
  sorted_tables = _topological_sort_table_and_deps(list(tables.values()))

  parsed_tables: Dict[str, ParsedTable] = {}
  for table in sorted_tables:
    parsed_columns: List[ParsedColumn] = []
    if table.parent:
      parsed_parent = parsed_tables[table.parent.class_name]
      parsed_columns += [
          dataclasses.replace(c, is_ancestor=True)
          for c in parsed_parent.columns
      ]
    else:
      if table.add_implicit_column or table.use_legacy_table_backend:
        parsed_columns += _create_implicit_columns_for_root(table)

    for c in table.columns:
      doc = table.tabledoc.columns.get(c.name) if table.tabledoc else None
      parsed_columns.append(ParsedColumn(c, _to_column_doc(doc)))
    parsed_tables[table.class_name] = ParsedTable(table, parsed_columns)

  # Only return tables which come directly from |input_paths|. This stops us
  # generating tables which were not requested.
  return [
      parsed_tables[p.class_name]
      for p in sorted_tables
      if p.class_name in tables
  ]
