// 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 {
  LineChartSvg,
  type LineChartData,
  type LineChartSeries,
} from '../../../../components/widgets/charts_svg/line_chart_svg';
import type {LiveSession, SnapshotData} from '../../sessions/live_session';
import {
  billboardKb,
  counterPoints,
  formatKb,
  maxSeriesKb,
  niceKbInterval,
} from '../../utils';
import {Billboard} from '../../components/billboard';
import {Panel} from '../../components/panel';
import {Stack} from '../../../../widgets/stack';

function buildPageCacheTimeSeries(
  data: SnapshotData,
  t0: number,
): LineChartData | undefined {
  const counterNames = ['Cached', 'Shmem', 'Active(file)', 'Inactive(file)'];
  const byTs = new Map<number, Map<string, number>>();
  for (const name of counterNames) {
    const samples = data.systemCounters.get(name);
    if (samples === undefined) continue;
    for (const {ts, value} of samples) {
      let row = byTs.get(ts);
      if (row === undefined) {
        row = new Map();
        byTs.set(ts, row);
      }
      row.set(name, Math.round(value / 1024));
    }
  }
  if (byTs.size < 2) return undefined;
  const timestamps = [...byTs.keys()].sort((a, b) => a - b);
  const activeFilePoints: {x: number; y: number}[] = [];
  const inactiveFilePoints: {x: number; y: number}[] = [];
  const shmemPoints: {x: number; y: number}[] = [];
  for (const ts of timestamps) {
    const row = byTs.get(ts)!;
    const shmem = row.get('Shmem');
    const activeFile = row.get('Active(file)');
    const inactiveFile = row.get('Inactive(file)');
    if (
      shmem === undefined ||
      activeFile === undefined ||
      inactiveFile === undefined
    ) {
      continue;
    }
    const x = (ts - t0) / 1e9;
    activeFilePoints.push({x, y: activeFile});
    inactiveFilePoints.push({x, y: inactiveFile});
    shmemPoints.push({x, y: shmem});
  }
  if (activeFilePoints.length < 2) return undefined;
  return {
    series: [
      {
        name: 'Active(file)',
        points: activeFilePoints,
        color: 'var(--pf-color-success)',
      },
      {
        name: 'Inactive(file)',
        points: inactiveFilePoints,
        color: 'var(--pf-color-warning)',
      },
      {
        name: 'Shmem (tmpfs/ashmem)',
        points: shmemPoints,
        color: 'var(--pf-color-danger)',
      },
    ],
  };
}

function buildFileCacheBreakdownTimeSeries(
  data: SnapshotData,
  t0: number,
): LineChartData | undefined {
  const counterNames = ['Active(file)', 'Inactive(file)', 'Mapped', 'Dirty'];
  const byTs = new Map<number, Map<string, number>>();
  for (const name of counterNames) {
    const samples = data.systemCounters.get(name);
    if (samples === undefined) continue;
    for (const {ts, value} of samples) {
      let row = byTs.get(ts);
      if (row === undefined) {
        row = new Map();
        byTs.set(ts, row);
      }
      row.set(name, Math.round(value / 1024));
    }
  }
  if (byTs.size < 2) return undefined;
  const timestamps = [...byTs.keys()].sort((a, b) => a - b);
  const mappedDirtyPts: {x: number; y: number}[] = [];
  const mappedCleanPts: {x: number; y: number}[] = [];
  const unmappedDirtyPts: {x: number; y: number}[] = [];
  const unmappedCleanPts: {x: number; y: number}[] = [];
  for (const ts of timestamps) {
    const row = byTs.get(ts)!;
    const activeFile = row.get('Active(file)');
    const inactiveFile = row.get('Inactive(file)');
    const mapped = row.get('Mapped');
    const dirty = row.get('Dirty');
    if (
      activeFile === undefined ||
      inactiveFile === undefined ||
      mapped === undefined ||
      dirty === undefined
    ) {
      continue;
    }
    const fileCache = activeFile + inactiveFile;
    if (fileCache === 0) continue;
    const mappedDirty = (mapped * dirty) / fileCache;
    const x = (ts - t0) / 1e9;
    mappedDirtyPts.push({x, y: Math.round(mappedDirty)});
    mappedCleanPts.push({x, y: Math.round(mapped - mappedDirty)});
    unmappedDirtyPts.push({x, y: Math.round(dirty - mappedDirty)});
    unmappedCleanPts.push({
      x,
      y: Math.max(0, Math.round(fileCache - mapped - dirty + mappedDirty)),
    });
  }
  if (mappedDirtyPts.length < 2) return undefined;
  return {
    series: [
      {name: 'Mapped + Dirty', points: mappedDirtyPts},
      {name: 'Mapped + Clean', points: mappedCleanPts},
      {name: 'Unmapped + Dirty', points: unmappedDirtyPts},
      {name: 'Unmapped + Clean', points: unmappedCleanPts},
    ],
  };
}

function buildFileCacheActivityTimeSeries(
  data: SnapshotData,
  t0: number,
): LineChartData | undefined {
  // Rates (events/s) are computed in SQL — see extractSnapshotData in
  // live_session.ts.
  const series: LineChartSeries[] = [];
  const refault = counterPoints(
    data.systemCounters.get('workingset_refault_file'),
    t0,
  );
  if (refault !== undefined) {
    series.push({
      name: 'Refaults (thrashing)',
      points: refault,
      color: 'var(--pf-color-danger)',
    });
  }
  const steal = counterPoints(data.systemCounters.get('pgsteal_file'), t0);
  if (steal !== undefined) {
    series.push({
      name: 'Stolen (reclaimed)',
      points: steal,
      color: 'var(--pf-color-warning)',
    });
  }
  const scan = counterPoints(data.systemCounters.get('pgscan_file'), t0);
  if (scan !== undefined) {
    series.push({
      name: 'Scanned',
      points: scan,
      color: 'var(--pf-color-success)',
    });
  }
  if (series.length === 0) return undefined;
  return {series};
}

function getPageCacheBillboards(
  fileCacheBreakdownData?: LineChartData,
): {total: number; dirty: number; mapped: number} | undefined {
  const data = fileCacheBreakdownData;
  if (data === undefined || data.series.length < 4) return undefined;
  // Series order: Mapped+Dirty, Mapped+Clean, Unmapped+Dirty, Unmapped+Clean
  const last = (idx: number) => {
    const pts = data.series[idx].points;
    return pts.length > 0 ? pts[pts.length - 1].y : 0;
  };
  const mappedDirty = last(0);
  const mappedClean = last(1);
  const unmappedDirty = last(2);
  const unmappedClean = last(3);
  return {
    total: mappedDirty + mappedClean + unmappedDirty + unmappedClean,
    dirty: mappedDirty + unmappedDirty,
    mapped: mappedDirty + mappedClean,
  };
}

export function renderPageCacheTab(session: LiveSession): m.Children {
  const data = session.data;
  if (!data) return null;

  const t0 = data.ts0;
  const pageCacheChartData = buildPageCacheTimeSeries(data, t0);
  const fileCacheBreakdownData = buildFileCacheBreakdownTimeSeries(data, t0);
  const fileCacheActivityData = buildFileCacheActivityTimeSeries(data, t0);
  const bb = getPageCacheBillboards(fileCacheBreakdownData);

  return m(Stack, {spacing: 'large'}, [
    bb !== undefined &&
      m(
        Stack,
        {orientation: 'horizontal', spacing: 'large'},
        m(Billboard, {
          ...billboardKb(bb.total),
          label: 'Total Page Cache',
          desc: 'Derived: Active(file) + Inactive(file) from /proc/meminfo',
        }),
        m(Billboard, {
          ...billboardKb(bb.dirty),
          label: 'Dirty',
          desc: 'Source: Dirty from /proc/meminfo',
        }),
        m(Billboard, {
          ...billboardKb(bb.mapped),
          label: 'Mapped',
          desc: 'Source: Mapped from /proc/meminfo',
        }),
      ),

    m(
      Panel,
      {
        title: 'Page Cache',
        subtitle:
          'Source: /proc/meminfo counters Active(file), Inactive(file), Shmem. ' +
          'Stacked: Active(file) + Inactive(file) + Shmem \u2248 Cached.',
      },
      pageCacheChartData
        ? m(LineChartSvg, {
            data: pageCacheChartData,
            height: 250,
            xAxisLabel: 'Time (s)',
            yAxisLabel: 'Cache',
            showLegend: true,
            showPoints: false,
            stacked: true,
            gridLines: 'both',
            xAxisMin: data.xMin,
            xAxisMax: data.xMax,
            formatXValue: (v: number) => `${v.toFixed(0)}s`,
            formatYValue: (v: number) => formatKb(v),
            yAxisMinInterval: niceKbInterval(
              maxSeriesKb(pageCacheChartData.series),
            ),
          })
        : m('.pf-memscope-placeholder', 'Waiting for data\u2026'),
    ),

    m(
      Panel,
      {
        title: 'Page Cache Activity',
        subtitle:
          'Source: /proc/vmstat counters, shown as rates (delta/s). ' +
          'Refaults = workingset_refault_file (evicted pages needed again). ' +
          'Stolen = pgsteal_file (pages reclaimed). ' +
          'Scanned = pgscan_file (pages considered for reclaim).',
      },
      fileCacheActivityData
        ? m(LineChartSvg, {
            data: fileCacheActivityData,
            height: 200,
            xAxisLabel: 'Time (s)',
            yAxisLabel: 'Pages/s',
            showLegend: true,
            showPoints: false,
            gridLines: 'both',
            xAxisMin: data.xMin,
            xAxisMax: data.xMax,
            formatXValue: (v: number) => `${v.toFixed(0)}s`,
            formatYValue: (v: number) => v.toLocaleString(),
          })
        : m('.pf-memscope-placeholder', 'Waiting for data\u2026'),
    ),
  ]);
}
