// 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 {SqlValue} from '../../trace_processor/query_result';
import {NUM} from '../../trace_processor/query_result';
import type {CellRenderResult} from '../../components/widgets/datagrid/datagrid_schema';
import type {Filter} from '../../components/widgets/datagrid/model';
import {filterToSql} from '../../components/widgets/datagrid/sql_utils';
import type {Engine} from '../../trace_processor/engine';
import type {InstanceRow, PathEntry, PrimOrRef} from './types';
import {fmtSize} from './format';
import type {NavState} from './nav_state';
import {Tooltip} from '../../widgets/tooltip';
import {Icon} from '../../widgets/icon';

export type NavFn = (
  view: NavState['view'],
  params?: Record<string, unknown>,
) => void;

export type ObjLinkRef = {
  id: number;
  display: string;
  str?: string | null;
};

interface InstanceLinkAttrs {
  readonly row: InstanceRow | ObjLinkRef | null;
  readonly navigate: NavFn;
}
export function InstanceLink(): m.Component<InstanceLinkAttrs> {
  return {
    view(vnode) {
      const {row, navigate} = vnode.attrs;
      if (!row || row.id === 0) {
        return m('span', {class: 'pf-hde-badge-referent'}, 'ROOT');
      }
      const full = 'className' in row ? (row as InstanceRow) : null;
      return m(
        'span',
        full &&
          full.reachabilityName !== 'unreachable' &&
          full.reachabilityName !== 'strong'
          ? m(
              'span',
              {class: 'pf-hde-badge-reachability'},
              full.reachabilityName,
            )
          : null,
        full?.isRoot ? m('span', {class: 'pf-hde-badge-root'}, 'root') : null,
        m(
          'button',
          {
            class: 'pf-hde-link',
            onclick: () => navigate('object', {id: row.id, label: row.display}),
          },
          row.display,
        ),
        row.str != null
          ? m(
              'span',
              {
                class: 'pf-hde-badge-string',
                title: row.str.length > 80 ? row.str : undefined,
              },
              '"' +
                (row.str.length > 80
                  ? row.str.slice(0, 80) + '\u2026'
                  : row.str) +
                '"',
            )
          : null,
        full?.referent
          ? m(
              'span',
              {class: 'pf-hde-badge-referent'},
              ' for ',
              m(InstanceLink, {
                row: full.referent,
                navigate,
              }),
            )
          : null,
      );
    },
  };
}

interface SectionAttrs {
  readonly title: string;
  readonly defaultOpen?: boolean;
  // Optional inline actions rendered to the right of the title. Action
  // clicks are isolated from the section toggle.
  readonly actions?: m.Children;
}
export function Section(): m.Component<SectionAttrs> {
  let open = true;
  return {
    oninit(vnode) {
      open = vnode.attrs.defaultOpen !== false;
    },
    view(vnode) {
      return m(
        'div',
        {class: 'pf-hde-section'},
        m(
          'div',
          {class: 'pf-hde-section__header'},
          m(
            'button',
            {
              'class': 'pf-hde-section__toggle',
              'onclick': () => {
                open = !open;
              },
              'aria-expanded': open,
            },
            m('span', {class: 'pf-hde-section__title'}, vnode.attrs.title),
            m(
              'svg',
              {
                class: `pf-hde-section__chevron${open ? ' pf-hde-section__chevron--open' : ''}`,
                viewBox: '0 0 20 20',
                fill: 'currentColor',
              },
              m('path', {
                'fill-rule': 'evenodd',
                'd': 'M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z',
                'clip-rule': 'evenodd',
              }),
            ),
          ),
          vnode.attrs.actions !== undefined
            ? m(
                'div',
                {
                  class: 'pf-hde-section__actions',
                  onclick: (e: Event) => e.stopPropagation(),
                },
                vnode.attrs.actions,
              )
            : null,
        ),
        open ? m('div', {class: 'pf-hde-section__body'}, vnode.children) : null,
      );
    },
  };
}

/** Renders a size value as a right-aligned formatted byte string. */
export function sizeRenderer(value: SqlValue): CellRenderResult {
  return {
    content: m('span', {class: 'pf-hde-mono'}, fmtSize(Number(value ?? 0))),
    align: 'right',
  };
}

/** Renders a numeric count value as a right-aligned locale string. */
export function countRenderer(value: SqlValue): CellRenderResult {
  return {
    content: m(
      'span',
      {class: 'pf-hde-mono'},
      Number(value ?? 0).toLocaleString(),
    ),
    align: 'right',
  };
}

/**
 * Returns the short (unqualified) class name, preserving generics and array
 *  brackets. Each fully-qualified segment between `<>`, `,` delimiters is
 *  shortened independently so `java.util.Map<java.lang.String, int[]>` becomes
 *  `Map<String, int[]>`.
 */
export function shortClassName(full: string): string {
  const bracket = full.indexOf('[');
  const base = bracket >= 0 ? full.slice(0, bracket) : full;
  const suffix = bracket >= 0 ? full.slice(bracket) : '';
  // Shorten each qualified-name token; delimiters (<>,) are preserved.
  const short = base.replace(/[\w$.]+/g, (tok) => {
    const dot = tok.lastIndexOf('.');
    return dot >= 0 ? tok.slice(dot + 1) : tok;
  });
  return short + suffix;
}

// Hover-help text for size/count columns, shared across HDE views.
export const COL_INFO = {
  shallow:
    'Java memory used by this object alone, excluding referenced objects.',
  shallowNative:
    'Native memory attributed to this object alone (e.g. a Bitmap pixel ' +
    'buffer). Zero for most plain Java objects.',
  retained:
    'Java size of every object exclusively held by this one (dominator ' +
    'subtree, self inclusive). If something is reachable from multiple ' +
    'paths, it is not retained here — it is retained higher up at the ' +
    'common ancestor.',
  retainedNative:
    'Native size of every object exclusively held by this one (dominator ' +
    'subtree, self inclusive). Often zero in multi-rooted graphs where ' +
    'native-bearing objects (e.g. Bitmaps) are reachable via multiple ' +
    'paths — try Reachable Native if you expected a non-zero value.',
  retainedCount:
    'Number of objects exclusively held by this one (dominator subtree, ' +
    'self inclusive).',
  reachable:
    'Java size of every object reachable from this one along the heap ' +
    "graph's BFS shortest-path tree. Includes objects also reachable via " +
    'other paths. Matches the flamegraph "Object Size" cumulative.',
  reachableNative:
    'Native size of every object reachable from this one (BFS tree). ' +
    'Includes objects also reachable via other paths.',
  reachableCount: 'Count of objects reachable from this one (BFS tree).',
  bitmapStorage:
    'Pixel-storage backing decoded from Bitmap.mId. ' +
    "'heap' = malloc'd in this process — each duplicate is real RAM cost. " +
    "'ashmem' = shared kernel memory — duplicates with the same source_id " +
    'are kernel-shared (PSS-attributed), no real RAM cost. ' +
    "'hardware' = AHardwareBuffer — duplicates may share GPU memory if " +
    'they wrap the same buffer handle.',
  bitmapId:
    'Encoded Bitmap.mId — process-monotonic instance counter (' +
    'pid·10⁷ + storage_type·10⁶ + counter). Stable identifier within one ' +
    "process; it's never a dedup key (every Bitmap allocation gets a " +
    'fresh value).',
  bitmapSource:
    'Parcel sender derived from Bitmap.mSourceId. Blank when this Bitmap ' +
    "was allocated locally. When present, it's the sender's process name " +
    'and pid at writeToParcel time. Multiple Bitmaps with the same ' +
    'source_id and same content_hash are kernel-shared, NOT duplicated.',
} as const;

// Label + info icon for DataGrid column titles.
export function colHeader(label: string, info: m.Children): m.Children {
  return m(
    'span',
    {class: 'pf-hde-col-header'},
    label,
    m(
      Tooltip,
      {trigger: m(Icon, {className: 'pf-hde-col-header__info', icon: 'info'})},
      info,
    ),
  );
}

/** SQL preamble that includes dominator tree and object tree modules. */
export const SQL_PREAMBLE =
  'INCLUDE PERFETTO MODULE android.memory.heap_graph.dominator_tree;\n' +
  'INCLUDE PERFETTO MODULE android.memory.heap_graph.object_tree';

/**
 * Tracks total and filtered row counts for a SQL-backed DataGrid view.
 *  Call `init()` in oninit, pass `onFiltersChanged` to DataGrid, and read
 *  `heading()` for the formatted title.
 */
export class RowCounter {
  total: number | null = null;
  filtered: number | null = null;

  private engine: Engine | null = null;
  private baseQuery = '';
  private preamble = '';
  private currentFilters: readonly Filter[] = [];

  init(engine: Engine, query: string, preamble = '') {
    this.engine = engine;
    this.baseQuery = query;
    this.preamble = preamble;
    this.runCount();
  }

  /** Format a heading like "Objects (1,234)" or "Objects (42 / 1,234)". */
  heading(label: string): string {
    if (this.total === null) return label;
    if (
      this.filtered !== null &&
      this.currentFilters.length > 0 &&
      this.filtered !== this.total
    ) {
      return `${label} (${this.filtered.toLocaleString()} / ${this.total.toLocaleString()})`;
    }
    return `${label} (${this.total.toLocaleString()})`;
  }

  /** Pass this as the DataGrid `onFiltersChanged` callback. */
  readonly onFiltersChanged = (filters: readonly Filter[]) => {
    this.currentFilters = filters;
    this.runFilteredCount();
  };

  private runCount() {
    if (!this.engine) return;
    const prefix = this.preamble ? `${this.preamble};\n` : '';
    this.engine
      .query(`${prefix}SELECT COUNT(*) AS cnt FROM (${this.baseQuery})`)
      .then((r) => {
        this.total = r.firstRow({cnt: NUM}).cnt;
        m.redraw();
      })
      .catch(console.error);
  }

  private runFilteredCount() {
    if (!this.engine || this.currentFilters.length === 0) {
      this.filtered = null;
      m.redraw();
      return;
    }
    const where = this.currentFilters
      .map((f) => filterToSql(f, f.field))
      .join(' AND ');
    const prefix = this.preamble ? `${this.preamble};\n` : '';
    this.engine
      .query(
        `${prefix}SELECT COUNT(*) AS cnt FROM (${this.baseQuery}) WHERE ${where}`,
      )
      .then((r) => {
        this.filtered = r.firstRow({cnt: NUM}).cnt;
        m.redraw();
      })
      .catch(console.error);
  }
}

interface PrimOrRefCellAttrs {
  readonly v: PrimOrRef;
  readonly navigate: NavFn;
}
export function PrimOrRefCell(): m.Component<PrimOrRefCellAttrs> {
  return {
    view(vnode) {
      const {v, navigate} = vnode.attrs;
      if (v.kind === 'ref') {
        return m(InstanceLink, {
          row: {id: v.id, display: v.display, str: v.str},
          navigate,
        });
      }
      return m('span', {class: 'pf-hde-mono'}, v.v);
    },
  };
}

interface BitmapImageAttrs {
  readonly width: number;
  readonly height: number;
  readonly format: string;
  readonly data: Uint8Array;
}

export function BitmapImage(): m.Component<BitmapImageAttrs> {
  let blobUrl: string | null = null;

  return {
    oncreate(vnode) {
      const {width, height, format, data} = vnode.attrs;
      if (format === 'rgba') {
        const canvas = vnode.dom as HTMLCanvasElement;
        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext('2d');
        if (!ctx) return;
        const clamped = new Uint8ClampedArray(data.length);
        clamped.set(data);
        ctx.putImageData(new ImageData(clamped, width, height), 0, 0);
        return;
      }
      const mimeMap: Record<string, string> = {
        png: 'image/png',
        jpeg: 'image/jpeg',
        webp: 'image/webp',
      };
      const copy = new Uint8Array(data.length);
      copy.set(data);
      const blob = new Blob([copy], {
        type: mimeMap[format] ?? 'image/png',
      });
      blobUrl = URL.createObjectURL(blob);
      m.redraw();
    },
    onremove() {
      if (blobUrl) {
        URL.revokeObjectURL(blobUrl);
        blobUrl = null;
      }
    },
    view(vnode) {
      const {format} = vnode.attrs;
      if (format === 'rgba') {
        return m('canvas', {class: 'pf-hde-bitmap-image'});
      }
      // Always render the img element so oncreate fires and creates the blob URL.
      // Before the blob URL is ready, src is empty (blank image).
      return m('img', {src: blobUrl ?? '', class: 'pf-hde-bitmap-image'});
    },
  };
}

/** Renders a single dominator-tree path as an indented arrow chain. */
export function renderPath(path: PathEntry[], navigate: NavFn): m.Children {
  return m(
    'div',
    {class: 'pf-hde-view-stack--tight'},
    path.map((pe, i) =>
      m(
        'div',
        {
          key: i,
          class: `pf-hde-path-entry${pe.isDominator ? ' pf-hde-semibold' : ''}`,
          style: {'--pf-hde-depth': String(i)},
        },
        [
          m('span', {class: 'pf-hde-path-arrow'}, i === 0 ? '' : '\u2192'),
          m(InstanceLink, {row: pe.row, navigate}),
          pe.field ? m('span', {class: 'pf-hde-path-field'}, pe.field) : null,
        ],
      ),
    ),
  );
}
