import {errors} from 'appium/driver';
import type {StringRecord} from '@appium/types';
import type {AndroidDriver} from '../driver';
import type {StatusBarCommand, WindowProperties} from './types';
import {util} from '@appium/support';

const WINDOW_TITLE_PATTERN = /^\s+Window\s#\d+\sWindow\{[0-9a-f]+\s\w+\s([\w-]+)\}:$/;
const FRAME_PATTERN = /\bm?[Ff]rame=\[([0-9.-]+),([0-9.-]+)\]\[([0-9.-]+),([0-9.-]+)\]/;
// https://github.com/appium/appium-uiautomator2-driver/issues/904
const STATUS_BAR_TYPE_PATTERN = /\bty=STATUS_BAR\b/;
const NAVIGATION_BAR_TYPE_PATTERN = /\bty=NAVIGATION_BAR\b/;
const VIEW_VISIBILITY_PATTERN = /\bmViewVisibility=(0x[0-9a-fA-F]+)/;
// https://developer.android.com/reference/android/view/View#VISIBLE
const VIEW_VISIBLE = 0x0;
const STATUS_BAR_WINDOW_NAME_PREFIX = 'StatusBar';
const NAVIGATION_BAR_WINDOW_NAME_PREFIX = 'NavigationBar';
const DEFAULT_WINDOW_PROPERTIES: WindowProperties = {
  visible: false,
  x: 0,
  y: 0,
  width: 0,
  height: 0,
};

interface SystemBarsResult {
  statusBar?: WindowProperties;
  navigationBar?: WindowProperties;
}

/**
 * Gets the system bars (status bar and navigation bar) properties.
 *
 * @returns Promise that resolves to an object containing statusBar and navigationBar properties.
 * @throws {Error} If system bars details cannot be retrieved or parsed.
 */
export async function getSystemBars(this: AndroidDriver): Promise<StringRecord> {
  let stdout: string;
  try {
    stdout = await this.adb.shell(['dumpsys', 'window', 'windows']);
  } catch (e) {
    throw new Error(
      `Cannot retrieve system bars details. Original error: ${(e as Error).message}`,
      {
        cause: e,
      },
    );
  }
  return parseWindows.bind(this)(stdout);
}

/**
 * Performs a status bar command.
 *
 * @param command The status bar command to perform.
 * @param component The name of the tile component.
 * It is only required for `(add|remove|click)Tile` commands.
 * Example value: `com.package.name/.service.QuickSettingsTileComponent`
 * @returns Promise that resolves to the command output string.
 * @throws {errors.InvalidArgumentError} If the command is unknown.
 */
export async function mobilePerformStatusBarCommand(
  this: AndroidDriver,
  command: StatusBarCommand,
  component?: string,
): Promise<string> {
  const toStatusBarCommandCallable =
    (cmd: string, argsCallable?: () => string[] | string) => async (): Promise<string> => {
      let argsCallableArray = argsCallable ? argsCallable() : [];
      if (!Array.isArray(argsCallableArray)) {
        argsCallableArray = [argsCallableArray];
      }
      return await this.adb.shell(['cmd', 'statusbar', cmd, ...argsCallableArray]);
    };
  const tileCommandArgsCallable = () => component as string;
  const statusBarCommands = Object.fromEntries(
    (
      [
        ['expandNotifications', ['expand-notifications']],
        ['expandSettings', ['expand-settings']],
        ['collapse', ['collapse']],
        ['addTile', ['add-tile', tileCommandArgsCallable]],
        ['removeTile', ['remove-tile', tileCommandArgsCallable]],
        ['clickTile', ['click-tile', tileCommandArgsCallable]],
        ['getStatusIcons', ['get-status-icons']],
      ] as const
    ).map(([name, args]) => [name, toStatusBarCommandCallable(args[0], args[1])]),
  ) as Record<StatusBarCommand, () => Promise<string>>;

  const action = statusBarCommands[command];
  if (!action) {
    throw new errors.InvalidArgumentError(
      `The '${command}' status bar command is unknown. Only the following commands ` +
        `are supported: ${Object.keys(statusBarCommands)}`,
    );
  }
  return await action();
}

/**
 * Parses window properties from adb dumpsys output
 *
 * @param name The name of the window whose properties are being parsed
 * @param props The list of particular window property lines.
 * Check the corresponding unit tests for more details on the input format.
 * @returns Parsed properties object
 * @throws {Error} If there was an issue while parsing the properties string
 */
export function parseWindowProperties(
  this: AndroidDriver,
  name: string,
  props: string[],
): WindowProperties {
  const result = structuredClone(DEFAULT_WINDOW_PROPERTIES);
  const propLines = props.join('\n');
  const frameMatch = FRAME_PATTERN.exec(propLines);
  if (!frameMatch) {
    this.log.debug(propLines);
    throw new Error(`Cannot parse the frame size from '${name}' window properties`);
  }
  result.x = parseFloat(frameMatch[1]);
  result.y = parseFloat(frameMatch[2]);
  result.width = parseFloat(frameMatch[3]) - result.x;
  result.height = parseFloat(frameMatch[4]) - result.y;
  const visibilityMatch = VIEW_VISIBILITY_PATTERN.exec(propLines);
  if (!visibilityMatch) {
    this.log.debug(propLines);
    throw new Error(`Cannot parse the visibility value from '${name}' window properties`);
  }
  result.visible = parseInt(visibilityMatch[1], 16) === VIEW_VISIBLE;
  return result;
}

/**
 * Extracts status and navigation bar information from the window manager output.
 *
 * @param lines Output from dumpsys command.
 * Check the corresponding unit tests for more details on the input format.
 * @return An object containing two items where keys are statusBar and navigationBar,
 * and values are corresponding WindowProperties objects
 * @throws {Error} If no window properties could be parsed
 */
export function parseWindows(this: AndroidDriver, lines: string): SystemBarsResult {
  const windows: StringRecord<string[]> = {};
  let currentWindowName: string | null = null;
  for (const line of lines.split('\n').map((line) => String(line)?.trimEnd())) {
    const match = WINDOW_TITLE_PATTERN.exec(line as any);
    if (match) {
      currentWindowName = match[1];
      windows[currentWindowName] = [];
      continue;
    }
    if (String(line)?.trim()?.length === 0) {
      currentWindowName = null;
      continue;
    }

    if (currentWindowName && Array.isArray(windows[currentWindowName])) {
      windows[currentWindowName].push(line);
    }
  }
  if (util.isEmpty(windows)) {
    this.log.debug(lines);
    throw new Error('Cannot parse any window information from the dumpsys output');
  }

  const result: SystemBarsResult = {};
  for (const [name, props] of Object.entries(windows)) {
    if (
      name.startsWith(STATUS_BAR_WINDOW_NAME_PREFIX) ||
      props.some((line: string) => STATUS_BAR_TYPE_PATTERN.test(line))
    ) {
      result.statusBar = parseWindowProperties.bind(this)(name, props);
    } else if (
      name.startsWith(NAVIGATION_BAR_WINDOW_NAME_PREFIX) ||
      props.some((line: string) => NAVIGATION_BAR_TYPE_PATTERN.test(line))
    ) {
      result.navigationBar = parseWindowProperties.bind(this)(name, props);
    }
  }
  const unmatchedWindows = (
    [
      ['statusBar', STATUS_BAR_WINDOW_NAME_PREFIX],
      ['navigationBar', NAVIGATION_BAR_WINDOW_NAME_PREFIX],
    ] as const
  ).filter(([name]) => result[name as keyof SystemBarsResult] == null);
  for (const [window, namePrefix] of unmatchedWindows) {
    this.log.info(
      `No windows have been found whose title matches to ` +
        `'${namePrefix}'. Assuming it is invisible. ` +
        `Only the following windows are available: ${Object.keys(windows)}`,
    );
    result[window as keyof SystemBarsResult] = structuredClone(DEFAULT_WINDOW_PROPERTIES);
  }
  return result;
}
