// Copyright (C) 2026 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 type {Engine} from '../../../trace_processor/engine';
import type {SqlValue, Row} from '../../../trace_processor/query_result';
import {Spinner} from '../../../widgets/spinner';
import {EmptyState} from '../../../widgets/empty_state';
import {DataGrid} from '../../../components/widgets/datagrid/datagrid';
import {SQLDataSource} from '../../../components/widgets/datagrid/sql_data_source';
import {createSimpleSchema} from '../../../components/widgets/datagrid/sql_schema';
import type {SchemaRegistry} from '../../../components/widgets/datagrid/datagrid_schema';
import type {StringListRow} from '../types';
import {fmtSize, fmtHex} from '../format';
import type {Filter} from '../../../components/widgets/datagrid/model';
import {
  type NavFn,
  sizeRenderer,
  countRenderer,
  SQL_PREAMBLE,
  RowCounter,
  COL_INFO,
  colHeader,
} from '../components';
import * as queries from '../queries';
import {dumpFilterSql, type HeapDump} from '../queries';

function buildQuery(activeDump: HeapDump): string {
  return `
    SELECT base.*,
      a.cumulative_size AS reachable_size,
      a.cumulative_native_size AS reachable_native,
      a.cumulative_count AS reachable_count
    FROM (
      SELECT
        o.id,
        od.value_string AS value,
        LENGTH(od.value_string) AS len,
        o.self_size,
        ifnull(d.dominated_size_bytes, o.self_size)
          + ifnull(d.dominated_native_size_bytes, o.native_size) AS retained,
        ifnull(o.heap_type, 'default') AS heap
      FROM heap_graph_object o
      JOIN heap_graph_class c ON o.type_id = c.id
      LEFT JOIN heap_graph_object_data od ON o.object_data_id = od.id
      LEFT JOIN heap_graph_dominator_tree d ON d.id = o.id
      WHERE o.reachable != 0
        AND ${dumpFilterSql(activeDump, 'o')}
        AND od.value_string IS NOT NULL
        AND (c.name = 'java.lang.String'
          OR c.deobfuscated_name = 'java.lang.String')
    ) base
    LEFT JOIN _heap_graph_object_tree_aggregation a ON a.id = base.id
  `;
}

function makeUiSchema(navigate: NavFn): SchemaRegistry {
  return {
    query: {
      id: {
        title: 'Object',
        columnType: 'identifier',
        cellRenderer: (value: SqlValue, row) => {
          const id = Number(value);
          const str = row.value != null ? String(row.value) : null;
          const display = `String ${fmtHex(id)}`;
          return m(
            'button',
            {
              class: 'pf-hde-link',
              onclick: () =>
                navigate('object', {
                  id,
                  label: str
                    ? `"${str.length > 40 ? str.slice(0, 40) + '\u2026' : str}"`
                    : display,
                }),
            },
            m(
              'span',
              {
                class: 'pf-hde-mono pf-hde-break-all pf-hde-str-color',
              },
              str
                ? '"' +
                    (str.length > 300 ? str.slice(0, 300) + '\u2026' : str) +
                    '"'
                : display,
            ),
          );
        },
      },
      retained: {
        title: colHeader('Retained', COL_INFO.retained),
        titleString: 'Retained',
        columnType: 'quantitative',
        cellRenderer: sizeRenderer,
      },
      len: {
        title: 'Length',
        columnType: 'quantitative',
        cellRenderer: countRenderer,
      },
      heap: {
        title: 'Heap',
        columnType: 'text',
      },
      value: {
        title: 'Value',
        columnType: 'text',
      },
      self_size: {
        title: colHeader('Shallow', COL_INFO.shallow),
        titleString: 'Shallow',
        columnType: 'quantitative',
        cellRenderer: sizeRenderer,
      },
      reachable_size: {
        title: colHeader('Reachable', COL_INFO.reachable),
        titleString: 'Reachable',
        columnType: 'quantitative',
        cellRenderer: sizeRenderer,
      },
      reachable_native: {
        title: colHeader('Reachable native', COL_INFO.reachableNative),
        titleString: 'Reachable native',
        columnType: 'quantitative',
        cellRenderer: sizeRenderer,
      },
      reachable_count: {
        title: colHeader('Reachable count', COL_INFO.reachableCount),
        titleString: 'Reachable count',
        columnType: 'quantitative',
        cellRenderer: countRenderer,
      },
    },
  };
}

const SUMMARY_SCHEMA: SchemaRegistry = {
  query: {
    property: {title: 'Property', columnType: 'text'},
    value: {title: 'Value', columnType: 'text'},
  },
};

// --- StringsView -------------------------------------------------------------

interface StringsViewAttrs {
  readonly engine: Engine;
  readonly activeDump: HeapDump;
  readonly navigate: NavFn;
  readonly clearNavParam: (key: string) => void;
  readonly initialQuery?: string;
  readonly hasFieldValues?: boolean;
}

function StringsView(): m.Component<StringsViewAttrs> {
  let allRows: StringListRow[] | null = null;
  let alive = true;
  let dataSource: SQLDataSource | null = null;
  const counter = new RowCounter();
  let filters: Filter[] = [];

  function applyNavFilter(
    q: string | undefined,
    clearNavParam: (key: string) => void,
  ) {
    if (!q) return;
    filters = [{field: 'value', op: '=' as const, value: q}];
    counter.onFiltersChanged(filters);
    clearNavParam('q');
  }

  return {
    oninit(vnode) {
      const {engine, activeDump} = vnode.attrs;
      const query = buildQuery(activeDump);
      dataSource = new SQLDataSource({
        engine,
        sqlSchema: createSimpleSchema(query),
        rootSchemaName: 'query',
        preamble: SQL_PREAMBLE,
      });
      counter.init(engine, query, SQL_PREAMBLE);
      applyNavFilter(vnode.attrs.initialQuery, vnode.attrs.clearNavParam);
      queries
        .getStringList(engine, activeDump)
        .then((r) => {
          if (!alive) return;
          allRows = r;
          m.redraw();
        })
        .catch(console.error);
    },
    onupdate(vnode) {
      applyNavFilter(vnode.attrs.initialQuery, vnode.attrs.clearNavParam);
    },
    onremove() {
      alive = false;
    },
    view(vnode) {
      const {navigate} = vnode.attrs;

      if (!allRows) {
        return m('div', {class: 'pf-hde-loading'}, m(Spinner, {easing: true}));
      }

      if (allRows.length === 0) {
        return m(EmptyState, {
          icon: 'text_fields',
          title:
            vnode.attrs.hasFieldValues === false
              ? 'String values require an ART heap dump (.hprof)'
              : 'No string data available',
          fillHeight: true,
        });
      }

      const totalRetained = allRows.reduce((s, r) => s + r.retainedSize, 0);
      const uniqueCount = (() => {
        const seen = new Set<string>();
        for (const r of allRows) seen.add(r.value);
        return seen.size;
      })();

      const summaryRows: Row[] = [
        {property: 'Total strings', value: allRows.length.toLocaleString()},
        {property: 'Unique values', value: uniqueCount.toLocaleString()},
        {property: 'Total retained', value: fmtSize(totalRetained)},
      ];

      return m('div', {class: 'pf-hde-view-content'}, [
        m('h2', {class: 'pf-hde-view-heading'}, counter.heading('Strings')),

        m('div', {class: 'pf-hde-card pf-hde-mb-4 pf-hde-flex-none'}, [
          m(DataGrid, {
            schema: SUMMARY_SCHEMA,
            rootSchema: 'query',
            data: summaryRows,
            initialColumns: [
              {id: 'property', field: 'property'},
              {id: 'value', field: 'value'},
            ],
          }),
        ]),

        dataSource
          ? m(DataGrid, {
              schema: makeUiSchema(navigate),
              rootSchema: 'query',
              data: dataSource,
              fillHeight: true,
              initialColumns: [
                {id: 'id', field: 'id'},
                {id: 'value', field: 'value'},
                {id: 'retained', field: 'retained'},
                {id: 'reachable_size', field: 'reachable_size'},
                {id: 'reachable_native', field: 'reachable_native'},
                {id: 'reachable_count', field: 'reachable_count'},
                {id: 'len', field: 'len'},
                {id: 'heap', field: 'heap'},
              ],
              filters,
              showExportButton: true,
              onFiltersChanged: (f) => {
                filters = [...f];
                counter.onFiltersChanged(f);
              },
            })
          : null,
      ]);
    },
  };
}

export default StringsView;
