// Copyright (C) 2025 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 type {Trace} from '../../public/trace';
import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
import type {PerfettoPlugin} from '../../public/plugin';
import {
  STR,
  LONG,
  UNKNOWN,
  LONG_NULL,
} from '../../trace_processor/query_result';
import {SourceDataset} from '../../trace_processor/dataset';
import SupportPlugin from '../com.android.AndroidLongBatterySupport';

function bleScanDataset(condition: string) {
  return new SourceDataset({
    src: `
      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
    `,
    schema: {
      ts: LONG,
      dur: LONG_NULL,
      name: STR,
    },
  });
}

const BLE_RESULTS_DATASET = new SourceDataset({
  src: `
    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
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
  },
});

const BT_A2DP_AUDIO_DATASET = new SourceDataset({
  src: `
    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'
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
  },
});

const BT_CONNS_ACL_DATASET = new SourceDataset({
  src: `
    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
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
  },
});

const BT_CONNS_SCO_DATASET = new SourceDataset({
  src: `
    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
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
  },
});

const BT_LINK_LEVEL_EVENTS_DATASET = new SourceDataset({
  src: `
    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
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
    direction: UNKNOWN,
    type: UNKNOWN,
    hci_cmd: UNKNOWN,
    hci_event: UNKNOWN,
    hci_ble_event: UNKNOWN,
    cmd_status: UNKNOWN,
    reason_code: UNKNOWN,
    metric_id: UNKNOWN,
  },
});

const BT_QUALITY_REPORTS_DATASET = new SourceDataset({
  src: `
    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
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
    quality_report_id: UNKNOWN,
    packet_types: UNKNOWN,
    connection_handle: UNKNOWN,
    connection_role: UNKNOWN,
    tx_power_level: UNKNOWN,
    rssi: UNKNOWN,
    snr: UNKNOWN,
    unused_afh_channel_count: UNKNOWN,
    afh_select_unideal_channel_count: UNKNOWN,
    lsto: UNKNOWN,
    connection_piconet_clock: UNKNOWN,
    retransmission_count: UNKNOWN,
    no_rx_count: UNKNOWN,
    nak_count: UNKNOWN,
    flow_off_count: UNKNOWN,
    buffer_overflow_bytes: UNKNOWN,
    buffer_underflow_bytes: UNKNOWN,
  },
});

const BT_RSSI_REPORTS_DATASET = new SourceDataset({
  src: `
    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
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
    connection_handle: UNKNOWN,
    hci_status: UNKNOWN,
    rssi: UNKNOWN,
    metric_id: UNKNOWN,
  },
});

const BT_CODE_PATH_COUNTER_DATASET = new SourceDataset({
  src: `
    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
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
    key: UNKNOWN,
    number: UNKNOWN,
  },
});

const BT_HAL_CRASHES_DATASET = new SourceDataset({
  src: `
    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
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
    metric_id: UNKNOWN,
    error_code: UNKNOWN,
    vendor_error_code: UNKNOWN,
  },
});

const BT_BYTES_DATASET = new SourceDataset({
  src: `
    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)
  `,
  schema: {
    ts: LONG,
    dur: LONG_NULL,
    name: STR,
  },
});

// 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 = 'com.android.Bluetooth';
  static readonly dependencies = [StandardGroupsPlugin, SupportPlugin];

  private support(ctx: Trace) {
    return ctx.plugins.getPlugin(SupportPlugin);
  }

  async onTraceLoad(ctx: Trace): Promise<void> {
    const support = this.support(ctx);
    const features = await support.features(ctx.engine);
    if (
      !Array.from(features.values()).some(
        (f) => f.startsWith('atom.bluetooth_') || f.startsWith('atom.ble_'),
      )
    ) {
      return;
    }

    const groupName = 'Bluetooth';
    await support.addSliceTrack(
      ctx,
      'BLE Scans (opportunistic)',
      bleScanDataset('opportunistic'),
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'BLE Scans (filtered)',
      bleScanDataset('filtered'),
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'BLE Scans (unfiltered)',
      bleScanDataset('not filtered'),
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'BLE Scan Results',
      BLE_RESULTS_DATASET,
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'Connections (ACL)',
      BT_CONNS_ACL_DATASET,
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'Connections (SCO)',
      BT_CONNS_SCO_DATASET,
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'Link-level Events',
      BT_LINK_LEVEL_EVENTS_DATASET,
      groupName,
    );

    await support.addSliceTrack(
      ctx,
      'A2DP Audio',
      BT_A2DP_AUDIO_DATASET,
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'Bytes Transferred (L2CAP/RFCOMM)',
      BT_BYTES_DATASET,
      groupName,
    );
    await ctx.engine.query(BT_ACTIVITY);
    await support.addCounterTrack(
      ctx,
      'ACL Classic Active Count',
      'select ts, dur, acl_active_count as value from bt_activity',
      groupName,
    );
    await support.addCounterTrack(
      ctx,
      'ACL Classic Sniff Count',
      'select ts, dur, acl_sniff_count as value from bt_activity',
      groupName,
    );
    await support.addCounterTrack(
      ctx,
      'ACL BLE Count',
      'select ts, dur, acl_ble_count as value from bt_activity',
      groupName,
    );
    await support.addCounterTrack(
      ctx,
      'Advertising Instance Count',
      'select ts, dur, advertising_count as value from bt_activity',
      groupName,
    );
    await support.addCounterTrack(
      ctx,
      'LE Scan Duty Cycle Maximum',
      'select ts, dur, le_scan_duty_cycle as value from bt_activity',
      groupName,
      {unit: '%'},
    );
    await support.addSliceTrack(
      ctx,
      'Inquiry Active',
      new SourceDataset({
        src: `SELECT
                ts,
                dur,
                'Active' as name
              FROM bt_activity
              WHERE inquiry_active`,
        schema: {
          ts: LONG,
          dur: LONG_NULL,
          name: STR,
        },
      }),
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'SCO Active',
      new SourceDataset({
        src: `SELECT
                ts,
                dur,
                'Active' as name
              FROM bt_activity
              WHERE sco_active`,
        schema: {
          ts: LONG,
          dur: LONG_NULL,
          name: STR,
        },
      }),
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'A2DP Active',
      new SourceDataset({
        src: `SELECT
                ts,
                dur,
                'Active' as name
              FROM bt_activity
              WHERE a2dp_active`,
        schema: {
          ts: LONG,
          dur: LONG_NULL,
          name: STR,
        },
      }),
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'LE Audio Active',
      new SourceDataset({
        src: `SELECT
                ts,
                dur,
                'Active' as name
              FROM bt_activity
              WHERE le_audio_active`,
        schema: {
          ts: LONG,
          dur: LONG_NULL,
          name: STR,
        },
      }),
      groupName,
    );
    await support.addCounterTrack(
      ctx,
      'Controller Idle Time',
      'select ts, dur, controller_idle_pct as value from bt_activity',
      groupName,
      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
    );
    await support.addCounterTrack(
      ctx,
      'Controller TX Time',
      'select ts, dur, controller_tx_pct as value from bt_activity',
      groupName,
      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
    );
    await support.addCounterTrack(
      ctx,
      'Controller RX Time',
      'select ts, dur, controller_rx_pct as value from bt_activity',
      groupName,
      {yRangeSharingKey: 'bt_controller_time', unit: '%'},
    );
    await support.addSliceTrack(
      ctx,
      'Quality reports',
      BT_QUALITY_REPORTS_DATASET,
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'RSSI Reports',
      BT_RSSI_REPORTS_DATASET,
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'HAL Crashes',
      BT_HAL_CRASHES_DATASET,
      groupName,
    );
    await support.addSliceTrack(
      ctx,
      'Code Path Counter',
      BT_CODE_PATH_COUNTER_DATASET,
      groupName,
    );
  }
}
