// 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 {Trace} from '../../public/trace';
import {PerfettoPlugin} from '../../public/plugin';
import {Engine} from '../../trace_processor/engine';
import {createQuerySliceTrack} from '../../components/tracks/query_slice_track';
import {CounterOptions} from '../../components/tracks/base_counter_track';
import {createQueryCounterTrack} from '../../components/tracks/query_counter_track';
import {TrackNode} from '../../public/workspace';

interface ContainedTrace {
  uuid: string;
  subscription: string;
  trigger: string;
  // NB: these are millis.
  ts: number;
  dur: number;
}

const PACKAGE_LOOKUP = `
  create or replace perfetto table package_name_lookup as
  with installed as (
    select uid, string_agg(package_name, ',') as name
    from package_list
    where uid >= 10000
    group by 1
  ),
  system(uid, name) as (
    values
      (0, 'AID_ROOT'),
      (1000, 'AID_SYSTEM_USER'),
      (1001, 'AID_RADIO'),
      (1082, 'AID_ARTD')
  )
  select uid, name from installed
  union all
  select uid, name from system
  order by uid;

  -- Adds a "package_name" column by joining on "uid" from the source table.
  create or replace perfetto macro add_package_name(src TableOrSubquery) returns TableOrSubquery as (
    select A.*, ifnull(B.name, "uid=" || A.uid) as package_name
    from $src as A
    left join package_name_lookup as B
    on (B.uid = (A.uid % 100000))
  );
`;

const DEFAULT_NETWORK = `
  with base as (
      select
          ts,
          substr(s.name, 6) as conn
      from track t join slice s on t.id = s.track_id
      where t.name = 'battery_stats.conn'
  ),
  diff as (
      select
          ts,
          conn,
          conn != lag(conn) over (order by ts) as keep
      from base
  )
  select
      ts,
      ifnull(lead(ts) over (order by ts), (select end_ts from trace_bounds)) - ts as dur,
      case
        when conn like '-1:%' then 'Disconnected'
        when conn like '0:%' then 'Modem'
        when conn like '1:%' then 'WiFi'
        when conn like '4:%' then 'VPN'
        else conn
      end as name
  from diff where keep is null or keep`;

const RADIO_TRANSPORT_TYPE = `
  create or replace perfetto view radio_transport_data_conn as
  select ts, safe_dur AS dur, value_name as data_conn, value AS data_conn_val
  from android_battery_stats_state
  where track_name = "battery_stats.data_conn";

  create or replace perfetto view radio_transport_nr_state as
  select ts, safe_dur AS dur, value AS nr_state_val
  from android_battery_stats_state
  where track_name = "battery_stats.nr_state";

  drop table if exists radio_transport_join;
  create virtual table radio_transport_join
  using span_left_join(radio_transport_data_conn, radio_transport_nr_state);

  create or replace perfetto view radio_transport as
  select
    ts, dur,
    case data_conn_val
      -- On LTE with NR connected is 5G NSA.
      when 13 then iif(nr_state_val = 3, '5G (NSA)', data_conn)
      -- On NR with NR state present, is 5G SA.
      when 20 then iif(nr_state_val is null, '5G (SA or NSA)', '5G (SA)')
      else data_conn
    end as name
  from radio_transport_join;`;

const TETHERING = `
  with base as (
      select
          ts as ts_end,
          EXTRACT_ARG(arg_set_id, 'network_tethering_reported.duration_millis') * 1000000 as dur
      from track t join slice s on t.id = s.track_id
      where t.name = 'Statsd Atoms'
        and s.name = 'network_tethering_reported'
  )
  select ts_end - dur as ts, dur, 'Tethering' as name from base`;

const NETWORK_SUMMARY = `
  create or replace perfetto table network_summary as
  with base as (
      select
          cast(ts / 5000000000 as int64) * 5000000000 AS ts,
          case
              when track_name glob '*wlan*' then 'wifi'
              when track_name glob '*rmnet*' then 'modem'
              else 'unknown'
          end as dev_type,
          package_name as pkg,
          sum(packet_length) AS value
      from android_network_packets
      where (track_name glob '*wlan*' or track_name glob '*rmnet*')
      group by 1,2,3
  ),
  zeroes as (
      select
          ts,
          dev_type,
          pkg,
          value
      from base
      union all
      select
          ts + 5000000000 as ts,
          dev_type,
          pkg,
          0 as value
      from base
  ),
  final as (
      select
          ts,
          dev_type,
          pkg,
          sum(value) as value
      from zeroes
      group by 1, 2, 3
  )
  select * from final where ts is not null`;

const MODEM_RIL_STRENGTH = `
  DROP VIEW IF EXISTS ScreenOn;
  CREATE VIEW ScreenOn AS
  SELECT ts, dur FROM (
      SELECT
          ts, value,
          LEAD(ts, 1, TRACE_END()) OVER (ORDER BY ts)-ts AS dur
      FROM counter, track ON (counter.track_id = track.id)
      WHERE track.name = 'ScreenState'
  ) WHERE value = 2;

  DROP VIEW IF EXISTS RilSignalStrength;
  CREATE VIEW RilSignalStrength AS
  With RilMessages AS (
      SELECT
          ts, slice.name,
          LEAD(ts, 1, TRACE_END()) OVER (ORDER BY ts)-ts AS dur
      FROM slice, track
      ON (slice.track_id = track.id)
      WHERE track.name = 'RIL'
        AND slice.name GLOB 'UNSOL_SIGNAL_STRENGTH*'
  ),
  BandTypes(band_ril, band_name) AS (
      VALUES ("CellSignalStrengthLte:", "LTE"),
              ("CellSignalStrengthNr:", "NR")
  ),
  ValueTypes(value_ril, value_name) AS (
      VALUES ("rsrp=", "rsrp"),
              ("rssi=", "rssi")
  ),
  Extracted AS (
      SELECT ts, dur, band_name, value_name, (
          SELECT CAST(SUBSTR(key_str, start_idx+1, end_idx-start_idx-1) AS INT64) AS value
          FROM (
              SELECT key_str, INSTR(key_str, "=") AS start_idx, INSTR(key_str, " ") AS end_idx
              FROM (
                  SELECT SUBSTR(band_str, INSTR(band_str, value_ril)) AS key_str
                  FROM (SELECT SUBSTR(name, INSTR(name, band_ril)) AS band_str)
              )
          )
      ) AS value
      FROM RilMessages
      JOIN BandTypes
      JOIN ValueTypes
  )
  SELECT
  ts, dur, band_name, value_name, value,
  value_name || "=" || IIF(value = 2147483647, "unknown", ""||value) AS name,
  ROW_NUMBER() OVER (ORDER BY ts) as id,
  DENSE_RANK() OVER (ORDER BY band_name, value_name) AS track_id
  FROM Extracted;

  DROP TABLE IF EXISTS RilScreenOn;
  CREATE VIRTUAL TABLE RilScreenOn
  USING SPAN_JOIN(RilSignalStrength PARTITIONED track_id, ScreenOn)`;

const MODEM_RIL_CHANNELS_PREAMBLE = `
  CREATE OR REPLACE PERFETTO FUNCTION EXTRACT_KEY_VALUE(source STRING, key_name STRING) RETURNS STRING AS
  SELECT SUBSTR(trimmed, INSTR(trimmed, "=")+1, INSTR(trimmed, ",") - INSTR(trimmed, "=") - 1)
  FROM (SELECT SUBSTR($source, INSTR($source, $key_name)) AS trimmed);`;

const MODEM_RIL_CHANNELS = `
  With RawChannelConfig AS (
      SELECT ts, slice.name AS raw_config
      FROM slice, track
      ON (slice.track_id = track.id)
      WHERE track.name = 'RIL'
      AND slice.name LIKE 'UNSOL_PHYSICAL_CHANNEL_CONFIG%'
  ),
  Attributes(attribute, attrib_name) AS (
      VALUES ("mCellBandwidthDownlinkKhz", "downlink"),
          ("mCellBandwidthUplinkKhz", "uplink"),
          ("mNetworkType", "network"),
          ("mBand", "band")
  ),
  Slots(idx, slot_name) AS (
      VALUES (0, "primary"),
          (1, "secondary 1"),
          (2, "secondary 2")
  ),
  Stage1 AS (
      SELECT *, IFNULL(EXTRACT_KEY_VALUE(STR_SPLIT(raw_config, "}, {", idx), attribute), "") AS name
      FROM RawChannelConfig
      JOIN Attributes
      JOIN Slots
  ),
  Stage2 AS (
      SELECT *, LAG(name) OVER (PARTITION BY idx, attribute ORDER BY ts) AS last_name
      FROM Stage1
  ),
  Stage3 AS (
      SELECT *, LEAD(ts, 1, TRACE_END()) OVER (PARTITION BY idx, attribute ORDER BY ts) - ts AS dur
      FROM Stage2 WHERE name != last_name
  )
  SELECT ts, dur, slot_name || "-" || attrib_name || "=" || name AS name
  FROM Stage3`;

const MODEM_CELL_RESELECTION = `
  with base as (
    select
        ts,
        s.name as raw_ril,
        ifnull(str_split(str_split(s.name, 'CellIdentityLte{', 1), ', operatorNames', 0),
            str_split(str_split(s.name, 'CellIdentityNr{', 1), ', operatorNames', 0)) as cell_id
    from track t join slice s on t.id = s.track_id
    where t.name = 'RIL' and s.name like '%DATA_REGISTRATION_STATE%'
  ),
  base2 as (
    select
        ts,
        raw_ril,
        case
            when cell_id like '%earfcn%' then 'LTE ' || cell_id
            when cell_id like '%nrarfcn%' then 'NR ' || cell_id
            when cell_id is null then 'Unknown'
            else cell_id
        end as cell_id
    from base
  ),
  base3 as (
    select ts, cell_id , lag(cell_id) over (order by ts) as lag_cell_id, raw_ril
    from base2
  )
  select ts, 0 as dur, cell_id as name, raw_ril
  from base3
  where cell_id != lag_cell_id
  order by ts`;

const SUSPEND_RESUME = `
  SELECT
    ts,
    dur,
    'Suspended' AS name
  FROM android_suspend_state
  WHERE power_state = 'suspended'`;

const SCREEN_STATE = `
  WITH _counter AS (
    SELECT counter.id, ts, 0 AS track_id, value
    FROM counter
    JOIN counter_track ON counter_track.id = counter.track_id
    WHERE name = 'ScreenState'
  )
  SELECT
    ts,
    dur,
    CASE value
      WHEN 1 THEN 'Screen off'
      WHEN 2 THEN 'Screen on'
      WHEN 3 THEN 'Always-on display (doze)'
      ELSE 'unknown'
    END AS name
  FROM counter_leading_intervals!(_counter)`;

// See DeviceIdleController.java for where these states come from and how
// they transition.
const DOZE_LIGHT = `
  WITH _counter AS (
    SELECT counter.id, ts, 0 AS track_id, value
    FROM counter
    JOIN counter_track ON counter_track.id = counter.track_id
    WHERE name = 'DozeLightState'
  )
  SELECT
    ts,
    dur,
    CASE value
      WHEN 0 THEN 'active'
      WHEN 1 THEN 'inactive'
      WHEN 4 THEN 'idle'
      WHEN 5 THEN 'waiting_for_network'
      WHEN 6 THEN 'idle_maintenance'
      WHEN 7 THEN 'override'
      ELSE 'unknown'
    END AS name
  FROM counter_leading_intervals!(_counter)`;

const DOZE_DEEP = `
  WITH _counter AS (
    SELECT counter.id, ts, 0 AS track_id, value
    FROM counter
    JOIN counter_track ON counter_track.id = counter.track_id
    WHERE name = 'DozeDeepState'
  )
  SELECT
    ts,
    dur,
    CASE value
      WHEN 0 THEN 'active'
      WHEN 1 THEN 'inactive'
      WHEN 2 THEN 'idle_pending'
      WHEN 3 THEN 'sensing'
      WHEN 4 THEN 'locating'
      WHEN 5 THEN 'idle'
      WHEN 6 THEN 'idle_maintenance'
      WHEN 7 THEN 'quick_doze_delay'
      ELSE 'unknown'
    END AS name
  FROM counter_leading_intervals!(_counter)`;

const CHARGING = `
  WITH _counter AS (
    SELECT counter.id, ts, 0 AS track_id, value
    FROM counter
    JOIN counter_track ON counter_track.id = counter.track_id
    WHERE name = 'BatteryStatus'
  )
  SELECT
    ts,
    dur,
    CASE value
      -- 0 and 1 are both unknown
      WHEN 2 THEN 'Charging'
      WHEN 3 THEN 'Discharging'
      -- special case when charger is present but battery isn't charging
      WHEN 4 THEN 'Not charging'
      WHEN 5 THEN 'Full'
      ELSE 'unknown'
    END AS name
  FROM counter_leading_intervals!(_counter)`;

const THERMAL_THROTTLING = `
  with step1 as (
      select
          ts,
          EXTRACT_ARG(arg_set_id, 'thermal_throttling_severity_state_changed.sensor_type') as sensor_type,
          EXTRACT_ARG(arg_set_id, 'thermal_throttling_severity_state_changed.sensor_name') as sensor_name,
          EXTRACT_ARG(arg_set_id, 'thermal_throttling_severity_state_changed.temperature_deci_celsius') / 10.0 as temperature_celcius,
          EXTRACT_ARG(arg_set_id, 'thermal_throttling_severity_state_changed.severity') as severity
      from track t join slice s on t.id = s.track_id
      where t.name = 'Statsd Atoms'
      and s.name = 'thermal_throttling_severity_state_changed'
  ),
  step2 as (
      select
          ts,
          lead(ts) over (partition by sensor_type, sensor_name order by ts) - ts as dur,
          sensor_type,
          sensor_name,
          temperature_celcius,
          severity
      from step1
      where sensor_type not like 'TEMPERATURE_TYPE_BCL_%'
  )
  select
    ts,
    dur,
    case sensor_name
        when 'VIRTUAL-SKIN' then ''
        else sensor_name || ' is '
    end || severity || ' (' || temperature_celcius || 'C)' as name
  from step2
  where severity != 'NONE'`;

const KERNEL_WAKELOCKS = `
  create or replace perfetto table kernel_wakelocks as
  with kernel_wakelock_args as (
    select
      arg_set_id,
      min(iif(key = 'kernel_wakelock.name', string_value, null)) as wakelock_name,
      min(iif(key = 'kernel_wakelock.count', int_value, null)) as count,
      min(iif(key = 'kernel_wakelock.time_micros', int_value, null)) as time_micros
    from args
    where key in (
      'kernel_wakelock.name',
      'kernel_wakelock.count',
      'kernel_wakelock.time_micros'
    )
    group by 1
  ),
  interesting as (
    select wakelock_name
    from (
      select wakelock_name, max(time_micros)-min(time_micros) as delta_us
      from kernel_wakelock_args
      group by 1
    )
    -- Only consider wakelocks with over 1 second of time during the whole trace
    where delta_us > 1e6
  ),
  step1 as (
    select ts, wakelock_name, count, time_micros
    from kernel_wakelock_args
    join interesting using (wakelock_name)
    join slice using (arg_set_id)
  ),
  step2 as (
    select
      ts,
      wakelock_name,
      lead(ts) over (partition by wakelock_name order by ts) as ts_end,
      lead(count) over (partition by wakelock_name order by ts) - count as count,
      (lead(time_micros) over (partition by wakelock_name order by ts) - time_micros) * 1000 as wakelock_dur
    from step1
  ),
  step3 as (
    select
      ts,
      ts_end,
      ifnull((select sum(dur) from android_suspend_state s
              where power_state = 'suspended'
                and s.ts > step2.ts
                and s.ts < step2.ts_end), 0) as suspended_dur,
      wakelock_name,
      count,
      wakelock_dur
    from step2
    where wakelock_dur is not null
      and wakelock_dur >= 0
  )
  select
    ts,
    ts_end - ts as dur,
    wakelock_name,
    min(100.0 * wakelock_dur / (ts_end - ts - suspended_dur), 100) as value
  from step3`;

const KERNEL_WAKELOCKS_SUMMARY = `
  select wakelock_name, max(value) as max_value
  from kernel_wakelocks
  where wakelock_name not in ('PowerManager.SuspendLockout', 'PowerManagerService.Display')
  group by 1
  having max_value > 1
  order by 1;`;

const HIGH_CPU = `
  create or replace perfetto table high_cpu as
  with cpu_cycles_args AS (
    select
      arg_set_id,
      min(iif(key = 'cpu_cycles_per_uid_cluster.uid', int_value, null)) as uid,
      min(iif(key = 'cpu_cycles_per_uid_cluster.cluster', int_value, null)) as cluster,
      min(iif(key = 'cpu_cycles_per_uid_cluster.time_millis', int_value, null)) as time_millis
    from args
    where key in (
      'cpu_cycles_per_uid_cluster.uid',
      'cpu_cycles_per_uid_cluster.cluster',
      'cpu_cycles_per_uid_cluster.time_millis'
    )
    group by 1
  ),
  interesting AS (
    select uid, cluster
    from (
      select uid, cluster, max(time_millis)-min(time_millis) as delta_ms
      from cpu_cycles_args
      group by 1, 2
    )
    -- Only consider tracks with over 1 second of cpu during the whole trace
    where delta_ms > 1e3
  ),
  base as (
    select ts, uid, cluster, sum(time_millis) as time_millis
    from cpu_cycles_args
    join interesting using (uid, cluster)
    join slice using (arg_set_id)
    group by 1, 2, 3
  ),
  with_windows as (
    select
      ts,
      uid,
      cluster,
      lead(ts) over (partition by uid, cluster order by ts) - ts as dur,
      (lead(time_millis) over (partition by uid, cluster order by ts) - time_millis) * 1000000.0 as cpu_dur
    from base
  ),
  with_ratio as (
    select
      ts,
      iif(dur is null, 0, max(0, 100.0 * cpu_dur / dur)) as value,
      case cluster when 0 then 'little' when 1 then 'mid' when 2 then 'big' else 'cl-' || cluster end as cluster,
      package_name as pkg
    from add_package_name!(with_windows)
  )
  select ts, sum(value) as value, cluster, pkg
  from with_ratio
  group by 1, 3, 4`;

const WAKEUPS = `
  drop table if exists wakeups;
  create table wakeups as
  with wakeup_reason as (
      select
      ts,
      substr(i.name, 0, instr(i.name, ' ')) as id_timestamp,
      substr(i.name, instr(i.name, ' ') + 1) as raw_wakeup
      from track t join instant i on t.id = i.track_id
      where t.name = 'wakeup_reason'
  ),
  wakeup_attribution as (
      select
      substr(i.name, 0, instr(i.name, ' ')) as id_timestamp,
      substr(i.name, instr(i.name, ' ') + 1) as attribution
      from track t join instant i on t.id = i.track_id
      where t.name = 'wakeup_attribution'
  ),
  step1 as(
    select
      ts,
      raw_wakeup,
      attribution,
      null as raw_backoff
    from wakeup_reason r
      left outer join wakeup_attribution using(id_timestamp)
    union all
    select
      ts,
      null as raw_wakeup,
      null as attribution,
      i.name as raw_backoff
    from track t join instant i on t.id = i.track_id
    where t.name = 'suspend_backoff'
  ),
  step2 as (
    select
      ts,
      raw_wakeup,
      attribution,
      lag(raw_backoff) over (order by ts) as raw_backoff
    from step1
  ),
  step3 as (
    select
      ts,
      raw_wakeup,
      attribution,
      str_split(raw_backoff, ' ', 0) as suspend_quality,
      str_split(raw_backoff, ' ', 1) as backoff_state,
      str_split(raw_backoff, ' ', 2) as backoff_reason,
      cast(str_split(raw_backoff, ' ', 3) as int) as backoff_count,
      cast(str_split(raw_backoff, ' ', 4) as int) as backoff_millis,
      false as suspend_end
    from step2
    where raw_wakeup is not null
    union all
    select
      ts,
      null as raw_wakeup,
      null as attribution,
      null as suspend_quality,
      null as backoff_state,
      null as backoff_reason,
      null as backoff_count,
      null as backoff_millis,
      true as suspend_end
    from android_suspend_state
    where power_state = 'suspended'
  ),
  step4 as (
    select
      ts,
      case suspend_quality
        when 'good' then
          min(
            lead(ts, 1, ts + 5e9) over (order by ts) - ts,
            5e9
          )
        when 'bad' then backoff_millis * 1000000
        else 0
      end as dur,
      raw_wakeup,
      attribution,
      suspend_quality,
      backoff_state,
      backoff_reason,
      backoff_count,
      backoff_millis,
      suspend_end
    from step3
  ),
  step5 as (
    select
      ts,
      dur,
      raw_wakeup,
      attribution,
      suspend_quality,
      backoff_state,
      backoff_reason,
      backoff_count,
      backoff_millis
    from step4
    where not suspend_end
  ),
  step6 as (
    select
      ts,
      dur,
      raw_wakeup,
      attribution,
      suspend_quality,
      backoff_state,
      backoff_reason,
      backoff_count,
      backoff_millis,
      case
        when raw_wakeup like 'Abort: Pending Wakeup Sources: %' then 'abort_pending'
        when raw_wakeup like 'Abort: Last active Wakeup Source: %' then 'abort_last_active'
        when raw_wakeup like 'Abort: %' then 'abort_other'
        else 'normal'
      end as type,
      case
        when raw_wakeup like 'Abort: Pending Wakeup Sources: %' then substr(raw_wakeup, 32)
        when raw_wakeup like 'Abort: Last active Wakeup Source: %' then substr(raw_wakeup, 35)
        when raw_wakeup like 'Abort: %' then substr(raw_wakeup, 8)
        else raw_wakeup
      end as main,
      case
        when raw_wakeup like 'Abort: Pending Wakeup Sources: %' then ' '
        when raw_wakeup like 'Abort: %' then 'no delimiter needed'
        else ':'
      end as delimiter
    from step5
  ),
  step7 as (
    select
      ts,
      dur,
      raw_wakeup,
      attribution,
      suspend_quality,
      backoff_state,
      backoff_reason,
      backoff_count,
      backoff_millis,
      type,
      str_split(main, delimiter, 0) as item_0,
      str_split(main, delimiter, 1) as item_1,
      str_split(main, delimiter, 2) as item_2,
      str_split(main, delimiter, 3) as item_3
    from step6
  ),
  step8 as (
    select ts, dur, raw_wakeup, attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, item_0 as item from step7
    union all
    select ts, dur, raw_wakeup, attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, item_1 as item from step7 where item_1 is not null
    union all
    select ts, dur, raw_wakeup, attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, item_2 as item from step7 where item_2 is not null
    union all
    select ts, dur, raw_wakeup, attribution, suspend_quality, backoff_state, backoff_reason, backoff_count, backoff_millis, type, item_3 as item from step7 where item_3 is not null
  )
  select
    ts,
    dur,
    ts + dur as ts_end,
    raw_wakeup,
    attribution,
    suspend_quality,
    backoff_state,
    ifnull(backoff_reason, 'none') as backoff_reason,
    backoff_count,
    backoff_millis,
    type,
    case when type = 'normal' then ifnull(str_split(item, ' ', 1), item) else item end as item
  from step8`;

const WAKEUPS_COLUMNS = [
  'item',
  'type',
  'raw_wakeup',
  'attribution',
  'suspend_quality',
  'backoff_state',
  'backoff_reason',
  'backoff_count',
  'backoff_millis',
];

function bleScanQuery(condition: string) {
  return `
  with step1 as (
      select
          ts,
          extract_arg(arg_set_id, 'ble_scan_state_changed.attribution_node[0].tag') as name,
          extract_arg(arg_set_id, 'ble_scan_state_changed.is_opportunistic') as opportunistic,
          extract_arg(arg_set_id, 'ble_scan_state_changed.is_filtered') as filtered,
          extract_arg(arg_set_id, 'ble_scan_state_changed.state') as state
      from track t join slice s on t.id = s.track_id
      where t.name = 'Statsd Atoms'
      and s.name = 'ble_scan_state_changed'
  ),
  step2 as (
      select
          ts,
          name,
          state,
          opportunistic,
          filtered,
          lead(ts) over (partition by name order by ts) - ts as dur
      from step1
  )
  select ts, dur, name from step2 where state = 'ON' and ${condition} and dur is not null`;
}

const BLE_RESULTS = `
  with step1 as (
      select
          ts,
          extract_arg(arg_set_id, 'ble_scan_result_received.attribution_node[0].tag') as name,
          extract_arg(arg_set_id, 'ble_scan_result_received.num_results') as num_results
      from track t join slice s on t.id = s.track_id
      where t.name = 'Statsd Atoms'
      and s.name = 'ble_scan_result_received'
  )
  select
      ts,
      0 as dur,
      name || ' (' || num_results || ' results)' as name
  from step1`;

const BT_A2DP_AUDIO = `
  with step1 as (
    select
        ts,
        EXTRACT_ARG(arg_set_id, 'bluetooth_a2dp_playback_state_changed.playback_state') as playback_state,
        EXTRACT_ARG(arg_set_id, 'bluetooth_a2dp_playback_state_changed.audio_coding_mode') as audio_coding_mode,
        EXTRACT_ARG(arg_set_id, 'bluetooth_a2dp_playback_state_changed.metric_id') as metric_id
    from track t join slice s on t.id = s.track_id
    where t.name = 'Statsd Atoms'
    and s.name = 'bluetooth_a2dp_playback_state_changed'
  ),
  step2 as (
    select
        ts,
        lead(ts) over (partition by metric_id order by ts) - ts as dur,
        playback_state,
        audio_coding_mode,
        metric_id
    from step1
  )
  select
    ts,
    dur,
    audio_coding_mode as name
  from step2
  where playback_state = 'PLAYBACK_STATE_PLAYING'`;

const BT_CONNS_ACL = `
    with acl1 as (
        select
            ts,
            EXTRACT_ARG(arg_set_id, 'bluetooth_acl_connection_state_changed.state') as state,
            EXTRACT_ARG(arg_set_id, 'bluetooth_acl_connection_state_changed.transport') as transport,
            EXTRACT_ARG(arg_set_id, 'bluetooth_acl_connection_state_changed.metric_id') as metric_id
        from track t join slice s on t.id = s.track_id
        where t.name = 'Statsd Atoms'
        and s.name = 'bluetooth_acl_connection_state_changed'
    ),
    acl2 as (
        select
            ts,
            lead(ts) over (partition by metric_id, transport order by ts) - ts as dur,
            state,
            transport,
            metric_id
        from acl1
    )
    select
        ts,
        dur,
        'Device ' || metric_id ||
          ' (' || case transport when 'TRANSPORT_TYPE_BREDR' then 'Classic' when 'TRANSPORT_TYPE_LE' then 'BLE' end || ')' as name
    from acl2
    where state != 'CONNECTION_STATE_DISCONNECTED' and dur is not null`;

const BT_CONNS_SCO = `
  with sco1 as (
    select
        ts,
        EXTRACT_ARG(arg_set_id, 'bluetooth_sco_connection_state_changed.state') as state,
        EXTRACT_ARG(arg_set_id, 'bluetooth_sco_connection_state_changed.codec') as codec,
        EXTRACT_ARG(arg_set_id, 'bluetooth_sco_connection_state_changed.metric_id') as metric_id
    from track t join slice s on t.id = s.track_id
    where t.name = 'Statsd Atoms'
    and s.name = 'bluetooth_sco_connection_state_changed'
  ),
  sco2 as (
    select
        ts,
        lead(ts) over (partition by metric_id, codec order by ts) - ts as dur,
        state,
        codec,
        metric_id
    from sco1
  )
  select
    ts,
    dur,
    case state when 'CONNECTION_STATE_CONNECTED' then '' when 'CONNECTION_STATE_CONNECTING' then 'Connecting ' when 'CONNECTION_STATE_DISCONNECTING' then 'Disconnecting ' else 'unknown ' end ||
      'Device ' || metric_id || ' (' ||
      case codec when 'SCO_CODEC_CVSD' then 'CVSD' when 'SCO_CODEC_MSBC' then 'MSBC' end || ')' as name
  from sco2
  where state != 'CONNECTION_STATE_DISCONNECTED' and dur is not null`;

const BT_LINK_LEVEL_EVENTS = `
  with base as (
    select
        ts,
        EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.direction') as direction,
        EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.type') as type,
        EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.hci_cmd') as hci_cmd,
        EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.hci_event') as hci_event,
        EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.hci_ble_event') as hci_ble_event,
        EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.cmd_status') as cmd_status,
        EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.reason_code') as reason_code,
        EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.metric_id') as metric_id
    from track t join slice s on t.id = s.track_id
    where t.name = 'Statsd Atoms'
    and s.name = 'bluetooth_link_layer_connection_event'
  )
  select
    *,
    0 as dur,
    'Device '|| metric_id as name
  from base`;

const BT_LINK_LEVEL_EVENTS_COLUMNS = [
  'direction',
  'type',
  'hci_cmd',
  'hci_event',
  'hci_ble_event',
  'cmd_status',
  'reason_code',
  'metric_id',
];

const BT_QUALITY_REPORTS = `
  with base as (
      select
          ts,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.quality_report_id') as quality_report_id,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.packet_types') as packet_types,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.connection_handle') as connection_handle,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.connection_role') as connection_role,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.tx_power_level') as tx_power_level,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.rssi') as rssi,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.snr') as snr,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.unused_afh_channel_count') as unused_afh_channel_count,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.afh_select_unideal_channel_count') as afh_select_unideal_channel_count,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.lsto') as lsto,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.connection_piconet_clock') as connection_piconet_clock,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.retransmission_count') as retransmission_count,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.no_rx_count') as no_rx_count,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.nak_count') as nak_count,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.flow_off_count') as flow_off_count,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.buffer_overflow_bytes') as buffer_overflow_bytes,
          EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.buffer_underflow_bytes') as buffer_underflow_bytes
      from track t join slice s on t.id = s.track_id
      where t.name = 'Statsd Atoms'
      and s.name = 'bluetooth_quality_report_reported'
  )
  select
      *,
      0 as dur,
      'Connection '|| connection_handle as name
  from base`;

const BT_QUALITY_REPORTS_COLUMNS = [
  'quality_report_id',
  'packet_types',
  'connection_handle',
  'connection_role',
  'tx_power_level',
  'rssi',
  'snr',
  'unused_afh_channel_count',
  'afh_select_unideal_channel_count',
  'lsto',
  'connection_piconet_clock',
  'retransmission_count',
  'no_rx_count',
  'nak_count',
  'flow_off_count',
  'buffer_overflow_bytes',
  'buffer_underflow_bytes',
];

const BT_RSSI_REPORTS = `
  with base as (
    select
        ts,
        EXTRACT_ARG(arg_set_id, 'bluetooth_device_rssi_reported.connection_handle') as connection_handle,
        EXTRACT_ARG(arg_set_id, 'bluetooth_device_rssi_reported.hci_status') as hci_status,
        EXTRACT_ARG(arg_set_id, 'bluetooth_device_rssi_reported.rssi') as rssi,
        EXTRACT_ARG(arg_set_id, 'bluetooth_device_rssi_reported.metric_id') as metric_id
    from track t join slice s on t.id = s.track_id
    where t.name = 'Statsd Atoms'
    and s.name = 'bluetooth_device_rssi_reported'
  )
  select
    *,
    0 as dur,
    'Connection '|| connection_handle as name
  from base`;

const BT_RSSI_REPORTS_COLUMNS = [
  'connection_handle',
  'hci_status',
  'rssi',
  'metric_id',
];

const BT_CODE_PATH_COUNTER = `
  with base as (
    select
        ts,
        EXTRACT_ARG(arg_set_id, 'bluetooth_code_path_counter.key') as key,
        EXTRACT_ARG(arg_set_id, 'bluetooth_code_path_counter.number') as number
    from track t join slice s on t.id = s.track_id
    where t.name = 'Statsd Atoms'
    and s.name = 'bluetooth_code_path_counter'
  )
  select
    *,
    0 as dur,
    key as name
  from base`;

const BT_CODE_PATH_COUNTER_COLUMNS = ['key', 'number'];

const BT_HAL_CRASHES = `
  with base as (
      select
          ts,
          EXTRACT_ARG(arg_set_id, 'bluetooth_hal_crash_reason_reported.metric_id') as metric_id,
          EXTRACT_ARG(arg_set_id, 'bluetooth_hal_crash_reason_reported.error_code') as error_code,
          EXTRACT_ARG(arg_set_id, 'bluetooth_hal_crash_reason_reported.vendor_error_code') as vendor_error_code
      from track t join slice s on t.id = s.track_id
      where t.name = 'Statsd Atoms'
      and s.name = 'bluetooth_hal_crash_reason_reported'
  )
  select
      *,
      0 as dur,
      'Device ' || metric_id as name
  from base`;

const BT_HAL_CRASHES_COLUMNS = ['metric_id', 'error_code', 'vendor_error_code'];

const BT_BYTES = `
  with step1 as (
    select
        ts,
        EXTRACT_ARG(arg_set_id, 'bluetooth_bytes_transfer.uid') as uid,
        EXTRACT_ARG(arg_set_id, 'bluetooth_bytes_transfer.tx_bytes') as tx_bytes,
        EXTRACT_ARG(arg_set_id, 'bluetooth_bytes_transfer.rx_bytes') as rx_bytes
    from track t join slice s on t.id = s.track_id
    where t.name = 'Statsd Atoms'
    and s.name = 'bluetooth_bytes_transfer'
  ),
  step2 as (
    select
        ts,
        lead(ts) over (partition by uid order by ts) - ts as dur,
        uid,
        lead(tx_bytes) over (partition by uid order by ts) - tx_bytes as tx_bytes,
        lead(rx_bytes) over (partition by uid order by ts) - rx_bytes as rx_bytes
    from step1
  ),
  step3 as (
    select
        ts,
        dur,
        uid % 100000 as uid,
        sum(tx_bytes) as tx_bytes,
        sum(rx_bytes) as rx_bytes
    from step2
    where tx_bytes >=0 and rx_bytes >=0
    group by 1,2,3
    having tx_bytes > 0 or rx_bytes > 0
  )
    select
        ts,
        dur,
        format("%s: TX %d bytes / RX %d bytes", package_name, tx_bytes, rx_bytes) as name
    from add_package_name!(step3)
`;

// See go/bt_system_context_report for reference on the bit-twiddling.
const BT_ACTIVITY = `
  create perfetto table bt_activity as
  with step1 as (
    select
        EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.timestamp_millis') * 1000000 as ts,
        EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.bluetooth_stack_state') as bluetooth_stack_state,
        EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_idle_time_millis') * 1000000 as controller_idle_dur,
        EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_tx_time_millis') * 1000000 as controller_tx_dur,
        EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_rx_time_millis') * 1000000 as controller_rx_dur
    from track t join slice s on t.id = s.track_id
    where t.name = 'Statsd Atoms'
    and s.name = 'bluetooth_activity_info'
  ),
  step2 as (
    select
        ts,
        lead(ts) over (order by ts) - ts as dur,
        bluetooth_stack_state,
        lead(controller_idle_dur) over (order by ts) - controller_idle_dur as controller_idle_dur,
        lead(controller_tx_dur) over (order by ts) - controller_tx_dur as controller_tx_dur,
        lead(controller_rx_dur) over (order by ts) - controller_rx_dur as controller_rx_dur
    from step1
  )
  select
    ts,
    dur,
    bluetooth_stack_state & 0x0000000F as acl_active_count,
    bluetooth_stack_state & 0x000000F0 >> 4 as acl_sniff_count,
    bluetooth_stack_state & 0x00000F00 >> 8 as acl_ble_count,
    bluetooth_stack_state & 0x0000F000 >> 12 as advertising_count,
    case bluetooth_stack_state & 0x000F0000 >> 16
      when 0 then 0
      when 1 then 5
      when 2 then 10
      when 3 then 25
      when 4 then 100
      else -1
    end as le_scan_duty_cycle,
    bluetooth_stack_state & 0x00100000 >> 20 as inquiry_active,
    bluetooth_stack_state & 0x00200000 >> 21 as sco_active,
    bluetooth_stack_state & 0x00400000 >> 22 as a2dp_active,
    bluetooth_stack_state & 0x00800000 >> 23 as le_audio_active,
    max(0, 100.0 * controller_idle_dur / dur) as controller_idle_pct,
    max(0, 100.0 * controller_tx_dur / dur) as controller_tx_pct,
    max(0, 100.0 * controller_rx_dur / dur) as controller_rx_pct
  from step2
`;

export default class implements PerfettoPlugin {
  static readonly id = 'dev.perfetto.AndroidLongBatteryTracing';
  private readonly groups = new Map<string, TrackNode>();

  private addTrack(ctx: Trace, track: TrackNode, groupName?: string): void {
    if (groupName) {
      const existingGroup = this.groups.get(groupName);
      if (existingGroup) {
        existingGroup.addChildInOrder(track);
      } else {
        const group = new TrackNode({title: groupName, isSummary: true});
        group.addChildInOrder(track);
        this.groups.set(groupName, group);
        ctx.workspace.addChildInOrder(group);
      }
    } else {
      ctx.workspace.addChildInOrder(track);
    }
  }

  async addSliceTrack(
    ctx: Trace,
    name: string,
    query: string,
    groupName?: string,
    columns: string[] = [],
  ) {
    const uri = `/long_battery_tracing_${name}`;
    const track = await createQuerySliceTrack({
      trace: ctx,
      uri,
      data: {
        sqlSource: query,
        columns: ['ts', 'dur', 'name', ...columns],
      },
      argColumns: columns,
    });
    ctx.tracks.registerTrack({
      uri,
      title: name,
      track,
    });
    const trackNode = new TrackNode({uri, title: name});
    this.addTrack(ctx, trackNode, groupName);
  }

  async addCounterTrack(
    ctx: Trace,
    name: string,
    query: string,
    groupName: string,
    options?: Partial<CounterOptions>,
  ) {
    const uri = `/long_battery_tracing_${name}`;
    const track = await createQueryCounterTrack({
      trace: ctx,
      uri,
      data: {
        sqlSource: query,
        columns: ['ts', 'value'],
      },
      options,
    });
    ctx.tracks.registerTrack({
      uri,
      title: name,
      track,
    });
    const trackNode = new TrackNode({uri, title: name});
    this.addTrack(ctx, trackNode, groupName);
  }

  async addBatteryStatsState(
    ctx: Trace,
    name: string,
    track: string,
    groupName: string,
    features: Set<string>,
  ) {
    if (!features.has(`track.${track}`)) {
      return;
    }
    await this.addSliceTrack(
      ctx,
      name,
      `SELECT ts, safe_dur AS dur, value_name AS name
    FROM android_battery_stats_state
    WHERE track_name = "${track}"`,
      groupName,
    );
  }

  async addBatteryStatsEvent(
    ctx: Trace,
    name: string,
    track: string,
    groupName: string | undefined,
    features: Set<string>,
  ) {
    if (!features.has(`track.${track}`)) {
      return;
    }

    await this.addSliceTrack(
      ctx,
      name,
      `SELECT ts, safe_dur AS dur, str_value AS name
    FROM android_battery_stats_event_slices
    WHERE track_name = "${track}"`,
      groupName,
    );
  }

  async addDeviceState(ctx: Trace, features: Set<string>): Promise<void> {
    if (!features.has('track.battery_stats.*')) {
      return;
    }

    const query = (name: string, track: string) =>
      this.addBatteryStatsEvent(ctx, name, track, undefined, features);

    const e = ctx.engine;
    await e.query(`INCLUDE PERFETTO MODULE android.battery_stats;`);
    await e.query(`INCLUDE PERFETTO MODULE android.suspend;`);
    await e.query(`INCLUDE PERFETTO MODULE counters.intervals;`);

    await this.addSliceTrack(ctx, 'Device State: Screen state', SCREEN_STATE);
    await this.addSliceTrack(ctx, 'Device State: Charging', CHARGING);
    await this.addSliceTrack(
      ctx,
      'Device State: Suspend / resume',
      SUSPEND_RESUME,
    );
    await this.addSliceTrack(ctx, 'Device State: Doze light state', DOZE_LIGHT);
    await this.addSliceTrack(ctx, 'Device State: Doze deep state', DOZE_DEEP);

    query('Device State: Top app', 'battery_stats.top');

    await this.addSliceTrack(
      ctx,
      'Device State: Long wakelocks',
      `SELECT
            ts - 60000000000 as ts,
            safe_dur + 60000000000 as dur,
            str_value AS name,
            package_name as package
        FROM add_package_name!((
          select *, int_value as uid
          from android_battery_stats_event_slices
          WHERE track_name = "battery_stats.longwake"
        ))`,
      undefined,
      ['package'],
    );

    query('Device State: Foreground apps', 'battery_stats.fg');
    query('Device State: Jobs', 'battery_stats.job');

    if (features.has('atom.thermal_throttling_severity_state_changed')) {
      await this.addSliceTrack(
        ctx,
        'Device State: Thermal throttling',
        THERMAL_THROTTLING,
      );
    }
  }

  async addAtomCounters(ctx: Trace): Promise<void> {
    const e = ctx.engine;

    try {
      await e.query(
        `INCLUDE PERFETTO MODULE
            google3.wireless.android.telemetry.trace_extractor.modules.atom_counters_slices`,
      );
    } catch (e) {
      return;
    }

    const counters = await e.query(
      `select distinct ui_group, ui_name, ui_unit, counter_name
       from atom_counters
       where ui_name is not null`,
    );
    const countersIt = counters.iter({
      ui_group: 'str',
      ui_name: 'str',
      ui_unit: 'str',
      counter_name: 'str',
    });
    for (; countersIt.valid(); countersIt.next()) {
      const unit = countersIt.ui_unit;
      const opts =
        unit === '%'
          ? {yOverrideMaximum: 100, unit: '%'}
          : unit !== undefined
            ? {unit}
            : undefined;

      await this.addCounterTrack(
        ctx,
        countersIt.ui_name,
        `select ts, ${unit === '%' ? 100.0 : 1.0} * counter_value as value
         from atom_counters
         where counter_name = '${countersIt.counter_name}'`,
        countersIt.ui_group,
        opts,
      );
    }
  }

  async addAtomSlices(ctx: Trace): Promise<void> {
    const e = ctx.engine;

    try {
      await e.query(
        `INCLUDE PERFETTO MODULE
            google3.wireless.android.telemetry.trace_extractor.modules.atom_counters_slices`,
      );
    } catch (e) {
      return;
    }

    const sliceTracks = await e.query(
      `select distinct ui_group, ui_name, atom, field
       from atom_slices
       where ui_name is not null
       order by 1, 2, 3, 4`,
    );
    const slicesIt = sliceTracks.iter({
      atom: 'str',
      ui_group: 'str',
      ui_name: 'str',
      field: 'str',
    });

    const tracks = new Map<
      string,
      {
        ui_group: string;
        ui_name: string;
      }
    >();
    const fields = new Map<string, string[]>();
    for (; slicesIt.valid(); slicesIt.next()) {
      const atom = slicesIt.atom;
      let args = fields.get(atom);
      if (args === undefined) {
        args = [];
        fields.set(atom, args);
      }
      args.push(slicesIt.field);
      tracks.set(atom, {
        ui_group: slicesIt.ui_group,
        ui_name: slicesIt.ui_name,
      });
    }

    for (const [atom, args] of fields) {
      function safeArg(arg: string) {
        return arg.replaceAll(/[[\]]/g, '').replaceAll(/\./g, '_');
      }

      // We need to make arg names compatible with SQL here because they pass through several
      // layers of SQL without being quoted in "".
      function argSql(arg: string) {
        return `max(case when field = '${arg}' then ifnull(string_value, int_value) end)
                as ${safeArg(arg)}`;
      }

      await this.addSliceTrack(
        ctx,
        tracks.get(atom)!.ui_name,
        `select ts, dur, slice_name as name, ${args.map((a) => argSql(a)).join(', ')}
         from atom_slices
         where atom = '${atom}'
         group by ts, dur, name`,
        tracks.get(atom)!.ui_group,
        args.map((a) => safeArg(a)),
      );
    }
  }

  async addNetworkSummary(ctx: Trace, features: Set<string>): Promise<void> {
    if (!features.has('net.modem') && !features.has('net.wifi')) {
      return;
    }

    const groupName = 'Network Summary';

    const e = ctx.engine;
    await e.query(`INCLUDE PERFETTO MODULE android.battery_stats;`);
    await e.query(`INCLUDE PERFETTO MODULE android.network_packets;`);
    await e.query(NETWORK_SUMMARY);
    await e.query(RADIO_TRANSPORT_TYPE);

    await this.addSliceTrack(
      ctx,
      'Default network',
      DEFAULT_NETWORK,
      groupName,
    );

    if (features.has('atom.network_tethering_reported')) {
      await this.addSliceTrack(ctx, 'Tethering', TETHERING, groupName);
    }
    if (features.has('net.wifi')) {
      await this.addCounterTrack(
        ctx,
        'Wifi total bytes',
        `select ts, sum(value) as value from network_summary where dev_type = 'wifi' group by 1`,
        groupName,
        {yDisplay: 'log', yRangeSharingKey: 'net_bytes', unit: 'byte'},
      );
      const result = await e.query(
        `select pkg, sum(value) from network_summary where dev_type='wifi' group by 1 order by 2 desc limit 10`,
      );
      const it = result.iter({pkg: 'str'});
      for (; it.valid(); it.next()) {
        await this.addCounterTrack(
          ctx,
          `Top wifi: ${it.pkg}`,
          `select ts, value from network_summary where dev_type = 'wifi' and pkg = '${it.pkg}'`,
          groupName,
          {yDisplay: 'log', yRangeSharingKey: 'net_bytes', unit: 'byte'},
        );
      }
    }
    this.addBatteryStatsState(
      ctx,
      'Wifi interface',
      'battery_stats.wifi_radio',
      groupName,
      features,
    );
    this.addBatteryStatsState(
      ctx,
      'Wifi supplicant state',
      'battery_stats.wifi_suppl',
      groupName,
      features,
    );
    this.addBatteryStatsState(
      ctx,
      'Wifi strength',
      'battery_stats.wifi_signal_strength',
      groupName,
      features,
    );
    if (features.has('net.modem')) {
      await this.addCounterTrack(
        ctx,
        'Modem total bytes',
        `select ts, sum(value) as value from network_summary where dev_type = 'modem' group by 1`,
        groupName,
        {yDisplay: 'log', yRangeSharingKey: 'net_bytes', unit: 'byte'},
      );
      const result = await e.query(
        `select pkg, sum(value) from network_summary where dev_type='modem' group by 1 order by 2 desc limit 10`,
      );
      const it = result.iter({pkg: 'str'});
      for (; it.valid(); it.next()) {
        await this.addCounterTrack(
          ctx,
          `Top modem: ${it.pkg}`,
          `select ts, value from network_summary where dev_type = 'modem' and pkg = '${it.pkg}'`,
          groupName,
          {yDisplay: 'log', yRangeSharingKey: 'net_bytes', unit: 'byte'},
        );
      }
    }
    this.addBatteryStatsState(
      ctx,
      'Cellular interface',
      'battery_stats.mobile_radio',
      groupName,
      features,
    );
    await this.addSliceTrack(
      ctx,
      'Cellular connection',
      `select ts, dur, name from radio_transport`,
      groupName,
    );
    this.addBatteryStatsState(
      ctx,
      'Cellular strength',
      'battery_stats.phone_signal_strength',
      groupName,
      features,
    );
  }

  async addModemDetail(ctx: Trace, features: Set<string>): Promise<void> {
    const groupName = 'Modem Detail';
    if (features.has('track.ril')) {
      await this.addModemRil(ctx, groupName);
    }
    await this.addModemTeaData(ctx, groupName);
  }

  async addModemRil(ctx: Trace, groupName: string): Promise<void> {
    const rilStrength = async (band: string, value: string) =>
      await this.addSliceTrack(
        ctx,
        `Modem signal strength ${band} ${value}`,
        `SELECT ts, dur, name FROM RilScreenOn WHERE band_name = '${band}' AND value_name = '${value}'`,
        groupName,
      );

    const e = ctx.engine;

    await e.query(MODEM_RIL_STRENGTH);
    await e.query(MODEM_RIL_CHANNELS_PREAMBLE);

    await rilStrength('LTE', 'rsrp');
    await rilStrength('LTE', 'rssi');
    await rilStrength('NR', 'rsrp');
    await rilStrength('NR', 'rssi');

    await this.addSliceTrack(
      ctx,
      'Modem channel config',
      MODEM_RIL_CHANNELS,
      groupName,
    );

    await this.addSliceTrack(
      ctx,
      'Modem cell reselection',
      MODEM_CELL_RESELECTION,
      groupName,
      ['raw_ril'],
    );
  }

  async addModemTeaData(ctx: Trace, groupName: string): Promise<void> {
    const e = ctx.engine;

    try {
      await e.query(
        `INCLUDE PERFETTO MODULE
            google3.wireless.android.telemetry.trace_extractor.modules.modem_tea_metrics`,
      );
    } catch {
      return;
    }

    const counters = await e.query(
      `select distinct name from pixel_modem_counters`,
    );
    const countersIt = counters.iter({name: 'str'});
    for (; countersIt.valid(); countersIt.next()) {
      await this.addCounterTrack(
        ctx,
        countersIt.name,
        `select ts, value from pixel_modem_counters where name = '${countersIt.name}'`,
        groupName,
      );
    }
    const slices = await e.query(
      `select distinct track_name from pixel_modem_slices`,
    );
    const slicesIt = slices.iter({track_name: 'str'});
    for (; slicesIt.valid(); slicesIt.next()) {
      await this.addSliceTrack(
        ctx,
        slicesIt.track_name,
        `select ts, dur, slice_name as name from pixel_modem_slices
            where track_name = '${slicesIt.track_name}'`,
        groupName,
      );
    }
  }

  async addKernelWakelocks(ctx: Trace, features: Set<string>): Promise<void> {
    if (!features.has('atom.kernel_wakelock')) {
      return;
    }
    const groupName = 'Kernel Wakelock Summary';

    const e = ctx.engine;
    await e.query(`INCLUDE PERFETTO MODULE android.suspend;`);
    await e.query(KERNEL_WAKELOCKS);
    const result = await e.query(KERNEL_WAKELOCKS_SUMMARY);
    const it = result.iter({wakelock_name: 'str'});
    for (; it.valid(); it.next()) {
      await this.addCounterTrack(
        ctx,
        it.wakelock_name,
        `select ts, dur, value from kernel_wakelocks where wakelock_name = "${it.wakelock_name}"`,
        groupName,
        {yRangeSharingKey: 'kernel_wakelock', unit: '%'},
      );
    }
  }

  async addWakeups(ctx: Trace, features: Set<string>): Promise<void> {
    if (!features.has('track.suspend_backoff')) {
      return;
    }

    const e = ctx.engine;
    const groupName = 'Wakeups';
    await e.query(`INCLUDE PERFETTO MODULE android.suspend;`);
    await e.query(WAKEUPS);
    const result = await e.query(`select
          item,
          sum(dur) as sum_dur
      from wakeups
      group by 1
      having sum_dur > 600e9`);
    const it = result.iter({item: 'str'});
    const sqlPrefix = `select
                ts,
                dur,
                item || case backoff_reason
                  when 'short' then ' (Short suspend backoff)'
                  when 'failed' then ' (Failed suspend backoff)'
                  else ''
                end as name,
                item,
                type,
                raw_wakeup,
                attribution,
                suspend_quality,
                backoff_state,
                backoff_reason,
                backoff_count,
                backoff_millis
            from wakeups`;
    const items = [];
    let labelOther = false;
    for (; it.valid(); it.next()) {
      labelOther = true;
      await this.addSliceTrack(
        ctx,
        `Wakeup ${it.item}`,
        `${sqlPrefix} where item="${it.item}"`,
        groupName,
        WAKEUPS_COLUMNS,
      );
      items.push(it.item);
    }
    await this.addSliceTrack(
      ctx,
      labelOther ? 'Other wakeups' : 'Wakeups',
      `${sqlPrefix} where item not in ('${items.join("','")}')`,
      groupName,
      WAKEUPS_COLUMNS,
    );
  }

  async addHighCpu(ctx: Trace, features: Set<string>): Promise<void> {
    if (!features.has('atom.cpu_cycles_per_uid_cluster')) {
      return;
    }
    const groupName = 'CPU per UID (major users)';

    const e = ctx.engine;

    await e.query(HIGH_CPU);
    const result = await e.query(
      `select distinct pkg, cluster from high_cpu where value > 10 order by 1, 2`,
    );
    const it = result.iter({pkg: 'str', cluster: 'str'});
    for (; it.valid(); it.next()) {
      await this.addCounterTrack(
        ctx,
        `CPU (${it.cluster}): ${it.pkg}`,
        `select ts, value from high_cpu where pkg = "${it.pkg}" and cluster="${it.cluster}"`,
        groupName,
        {yOverrideMaximum: 100, unit: '%'},
      );
    }
  }

  async addBluetooth(ctx: Trace, features: Set<string>): Promise<void> {
    if (
      !Array.from(features.values()).some(
        (f) => f.startsWith('atom.bluetooth_') || f.startsWith('atom.ble_'),
      )
    ) {
      return;
    }
    const groupName = 'Bluetooth';
    await this.addSliceTrack(
      ctx,
      'BLE Scans (opportunistic)',
      bleScanQuery('opportunistic'),
      groupName,
    );
    await this.addSliceTrack(
      ctx,
      'BLE Scans (filtered)',
      bleScanQuery('filtered'),
      groupName,
    );
    await this.addSliceTrack(
      ctx,
      'BLE Scans (unfiltered)',
      bleScanQuery('not filtered'),
      groupName,
    );
    await this.addSliceTrack(ctx, 'BLE Scan Results', BLE_RESULTS, groupName);
    await this.addSliceTrack(ctx, 'Connections (ACL)', BT_CONNS_ACL, groupName);
    await this.addSliceTrack(ctx, 'Connections (SCO)', BT_CONNS_SCO, groupName);
    await this.addSliceTrack(
      ctx,
      'Link-level Events',
      BT_LINK_LEVEL_EVENTS,
      groupName,
      BT_LINK_LEVEL_EVENTS_COLUMNS,
    );
    await this.addSliceTrack(ctx, 'A2DP Audio', BT_A2DP_AUDIO, groupName);
    await this.addSliceTrack(
      ctx,
      'Bytes Transferred (L2CAP/RFCOMM)',
      BT_BYTES,
      groupName,
    );
    await ctx.engine.query(BT_ACTIVITY);
    await this.addCounterTrack(
      ctx,
      'ACL Classic Active Count',
      'select ts, dur, acl_active_count as value from bt_activity',
      groupName,
    );
    await this.addCounterTrack(
      ctx,
      'ACL Classic Sniff Count',
      'select ts, dur, acl_sniff_count as value from bt_activity',
      groupName,
    );
    await this.addCounterTrack(
      ctx,
      'ACL BLE Count',
      'select ts, dur, acl_ble_count as value from bt_activity',
      groupName,
    );
    await this.addCounterTrack(
      ctx,
      'Advertising Instance Count',
      'select ts, dur, advertising_count as value from bt_activity',
      groupName,
    );
    await this.addCounterTrack(
      ctx,
      'LE Scan Duty Cycle Maximum',
      'select ts, dur, le_scan_duty_cycle as value from bt_activity',
      groupName,
      {unit: '%'},
    );
    await this.addSliceTrack(
      ctx,
      'Inquiry Active',
      "select ts, dur, 'Active' as name from bt_activity where inquiry_active",
      groupName,
    );
    await this.addSliceTrack(
      ctx,
      'SCO Active',
      "select ts, dur, 'Active' as name from bt_activity where sco_active",
      groupName,
    );
    await this.addSliceTrack(
      ctx,
      'A2DP Active',
      "select ts, dur, 'Active' as name from bt_activity where a2dp_active",
      groupName,
    );
    await this.addSliceTrack(
      ctx,
      'LE Audio Active',
      "select ts, dur, 'Active' as name from bt_activity where le_audio_active",
      groupName,
    );
    await this.addCounterTrack(
      ctx,
      'Controller Idle Time',
      'select ts, dur, controller_idle_pct as value from bt_activity',
      groupName,
      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
    );
    await this.addCounterTrack(
      ctx,
      'Controller TX Time',
      'select ts, dur, controller_tx_pct as value from bt_activity',
      groupName,
      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
    );
    await this.addCounterTrack(
      ctx,
      'Controller RX Time',
      'select ts, dur, controller_rx_pct as value from bt_activity',
      groupName,
      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
    );
    await this.addSliceTrack(
      ctx,
      'Quality reports',
      BT_QUALITY_REPORTS,
      groupName,
      BT_QUALITY_REPORTS_COLUMNS,
    );
    await this.addSliceTrack(
      ctx,
      'RSSI Reports',
      BT_RSSI_REPORTS,
      groupName,
      BT_RSSI_REPORTS_COLUMNS,
    );
    await this.addSliceTrack(
      ctx,
      'HAL Crashes',
      BT_HAL_CRASHES,
      groupName,
      BT_HAL_CRASHES_COLUMNS,
    );
    await this.addSliceTrack(
      ctx,
      'Code Path Counter',
      BT_CODE_PATH_COUNTER,
      groupName,
      BT_CODE_PATH_COUNTER_COLUMNS,
    );
  }

  async addContainedTraces(
    ctx: Trace,
    containedTraces: ContainedTrace[],
  ): Promise<void> {
    const bySubscription = new Map<string, ContainedTrace[]>();
    for (const trace of containedTraces) {
      if (!bySubscription.has(trace.subscription)) {
        bySubscription.set(trace.subscription, []);
      }
      bySubscription.get(trace.subscription)!.push(trace);
    }

    for (const [subscription, traces] of bySubscription) {
      await this.addSliceTrack(
        ctx,
        subscription,
        traces
          .map(
            (t) => `SELECT
          CAST(${t.ts} * 1e6 AS int) AS ts,
          CAST(${t.dur} * 1e6 AS int) AS dur,
          '${t.trigger === '' ? 'Trace' : t.trigger}' AS name,
          'http://go/trace-uuid/${t.uuid}' AS link
        `,
          )
          .join(' UNION ALL '),
        'Other traces',
        ['link'],
      );
    }
  }

  async findFeatures(e: Engine): Promise<Set<string>> {
    const features = new Set<string>();

    const addFeatures = async (q: string) => {
      const result = await e.query(q);
      const it = result.iter({feature: 'str'});
      for (; it.valid(); it.next()) {
        features.add(it.feature);
      }
    };

    await addFeatures(`
      select distinct 'atom.' || s.name as feature
      from track t join slice s on t.id = s.track_id
      where t.name = 'Statsd Atoms'`);

    await addFeatures(`
      select distinct
        case when name like '%wlan%' then 'net.wifi'
            when name like '%rmnet%' then 'net.modem'
            else 'net.other'
        end as feature
      from track
      where name like '%Transmitted' or name like '%Received'`);

    await addFeatures(`
      select distinct 'track.' || lower(name) as feature
      from track where name in ('RIL', 'suspend_backoff') or name like 'battery_stats.%'`);

    await addFeatures(`
      select distinct 'track.battery_stats.*' as feature
      from track where name like 'battery_stats.%'`);

    return features;
  }

  async addTracks(ctx: Trace): Promise<void> {
    const features: Set<string> = await this.findFeatures(ctx.engine);

    const containedTraces = (ctx.openerPluginArgs?.containedTraces ??
      []) as ContainedTrace[];

    await ctx.engine.query(PACKAGE_LOOKUP);
    await this.addNetworkSummary(ctx, features);
    await this.addBluetooth(ctx, features);
    await this.addAtomCounters(ctx);
    await this.addAtomSlices(ctx);
    await this.addModemDetail(ctx, features);
    await this.addKernelWakelocks(ctx, features);
    await this.addWakeups(ctx, features);
    await this.addDeviceState(ctx, features);
    await this.addHighCpu(ctx, features);
    await this.addContainedTraces(ctx, containedTraces);
  }

  async onTraceLoad(ctx: Trace): Promise<void> {
    await this.addTracks(ctx);
  }
}
