#!/usr/bin/perl
# Copyright (c) 2025 Jonas van den Berg
# This file is licensed under the BSD 3-Clause License.

# For usage information read below or run the script without arguments.

use strict;
use warnings;
use DynaLoader;
use File::Spec;
use File::Basename;

sub print_help() {
  print <<'HELP';
Usage:
  mediaremote-adapter.pl FRAMEWORK_PATH [TEST_CLIENT_PATH]
                         [FUNCTION [PARAMS|OPTIONS...]]

FRAMEWORK_PATH:
  Absolute path to the MediaRemoteAdapter.framework directory

TEST_CLIENT_PATH: (optional)
  Absolute path to the MediaRemoteAdapterTestClient executable. Only for "test"

FUNCTION:
  stream   Streams now playing information (as diff by default)
  get      Prints now playing information once with all available metadata
  send     Sends a command to the now playing application
  seek     Seeks to a specific timeline position
  shuffle  Sets the shuffle mode
  repeat   Sets the repeat mode
  speed    Sets the playback speed
  test     Tests if the adapter is entitled to use the MediaRemote framework.
           An exit code other than 0 indicates the adapter is non-functional

PARAMS:
  send(command)
    command: The MRCommand ID as a number (e.g. kMRPlay = 0)
  seek(position)
    position: The timeline position in microseconds
  shuffle(mode)
    mode: The shuffle mode
  repeat(mode)
    mode: The repeat mode
  speed(speed)
    speed: The playback speed

OPTIONS:
  get
    --now: Adds an "elapsedTimeNow" key with an estimation of the current
      elapsed playback time. This estimation may be off by up to a second.
      To determine a more accurate time without polling "get" continuously,
      calculate it using the "elapsedTime" and "timestamp" keys. "elapsedTime"
      contains the elapsed time at the time that is stored in "timestamp".
  stream
    --no-diff: Disable diffing and always dump all metadata
    --debounce=N: Delay in milliseconds to prevent spam (0 by default)
  get, stream
    --micros: Replaces the following time keys with microsecond equivalents:
      "duration" -> "durationMicros"
      "elapsedTime" -> "elapsedTimeMicros"
      "elapsedTimeNow" -> "elapsedTimeNowMicros"
      "timestamp" -> "timestampEpochMicros" (converted to epoch time)
    --no-artwork: Omits "artworkData" and "artworkMimeType" from the payload.
      Useful for consumers that do not render artwork, since this avoids
      emitting several hundred kilobytes of base64 data per update.
    --human-readable, -h: Makes values human-readable. Use only for debugging.
      The JSON output is pretty-printed and the following keys are adapted:
      "artworkData" -> Binary data is truncated to a shorter representation

Examples (script name and framework path omitted):
  stream --no-diff --debounce=100
  send 2    # Toggles play/pause in the media player (kMRATogglePlayPause)
  repeat 3  # Sets the repeat mode to "playlist" (kMRARepeatModePlaylist)

HELP
  exit 0;
}

if (!defined $ARGV[1]) {
  print_help();
}

sub fail {
  my ($error) = @_;
  print STDERR "$error\n";
  exit 1;
}

fail "Framework path not provided" unless @ARGV >= 1;

my $framework_path = shift @ARGV;

# Optionally accept MEDIAREMOTEADAPTER_TEST_CLIENT_PATH path as second argument
my $maybe_helper_path = $ARGV[0] // '';
if ($maybe_helper_path =~ m{/}){
  my $helper_path = shift @ARGV;
  $ENV{MEDIAREMOTEADAPTER_TEST_CLIENT_PATH} = $helper_path;
}

if (!defined $ARGV[0]) {
  print_help();
}

my $framework_basename = File::Basename::basename($framework_path);
fail "Provided path is not a framework: $framework_path"
  unless $framework_basename =~ s/\.framework$//;

my $framework = File::Spec->catfile($framework_path, $framework_basename);
fail "Framework not found at $framework" unless -e $framework;

my $handle = DynaLoader::dl_load_file($framework, 0)
  or fail "Failed to load framework: $framework";
my $function_name = shift @ARGV or fail "Missing function name";
fail "Invalid function name: '$function_name'"
  unless $function_name eq "stream"
  || $function_name eq "get"
  || $function_name eq "send"
  || $function_name eq "seek"
  || $function_name eq "shuffle"
  || $function_name eq "repeat"
  || $function_name eq "speed"
  || $function_name eq "test";

sub parse_options {
  my ($start_index) = @_;
  my %arg_map;
  my $i = $start_index;
  while ($i <= $#ARGV) {
    my $arg = $ARGV[$i];
    if ($arg =~ /^--([a-z:\.\\-]+)(?:=(.*))?$/) {
      my $key = $1;
      my $value = defined $2 ? $2 : undef;
      $arg_map{$key} = $value;
      splice @ARGV, $i, 1;
    }
    elsif ($arg =~ /^-([a-zA-Z]+)$/) {
      my @flags = split //, $1;
      $arg_map{$_} = undef for @flags;
      splice @ARGV, $i, 1;
    }
    else {
      $i++;
    }
  }
  return \%arg_map;
}

sub env_func {
  my $symbol_name = shift;
  return "${symbol_name}_env";
}

sub set_env_param {
  my ($func, $index, $name, $value) = @_;
  $ENV{"MEDIAREMOTEADAPTER_PARAM_${func}_${index}_${name}"} = "$value";
}

sub set_env_option_unsafe {
  my ($name, $value) = @_;
  $name =~ s/-/_/g;
  $ENV{"MEDIAREMOTEADAPTER_OPTION_${name}"} = defined $value ? "$value" : "";
}

sub set_env_option {
  my ($options, $key) = @_;
  my $value = $options->{$key};
  if (defined $value) {
    fail "Unexpected value for option '$key'";
  }
  set_env_option_unsafe($key, $value);
}

sub set_env_option_value {
  my ($options, $key) = @_;
  my $value = $options->{$key};
  if (!defined $value) {
    fail "Missing value for option '$key'";
  }
  set_env_option_unsafe($key, $value);
}

my $symbol_name = "adapter_$function_name";
if ($function_name eq "send") {
  my $id = shift @ARGV;
  fail "Missing ID for '$function_name' command" unless defined $id;
  set_env_param($symbol_name, 0, "command", "$id");
  $symbol_name = env_func($symbol_name);
}
elsif ($function_name eq "stream") {
  my $options = parse_options(0);
  foreach my $key (keys %{$options}) {
    if ($key eq "no-diff") {
      set_env_option($options, $key);
    }
    elsif ($key eq "debounce") {
      set_env_option_value($options, $key);
    }
    elsif ($key eq "micros") {
      set_env_option($options, $key);
    }
    elsif ($key eq "no-artwork") {
      set_env_option($options, $key);
    }
    elsif ($key eq "human-readable" || $key eq "h") {
      set_env_option($options, "human-readable");
    }
    elsif ($key eq "experimental-peculiar-debounce:com.tidal.desktop") {
      set_env_option_value($options, $key);
    }
    else {
      fail "Unrecognized option '$key'";
    }
  }
  $symbol_name = env_func($symbol_name);
}
elsif ($function_name eq "get") {
  my $options = parse_options(0);
  foreach my $key (keys %{$options}) {
    if ($key eq "micros") {
      set_env_option($options, $key);
    }
    elsif ($key eq "no-artwork") {
      set_env_option($options, $key);
    }
    elsif ($key eq "human-readable" || $key eq "h") {
      set_env_option($options, "human-readable");
    }
    elsif ($key eq "now") {
      set_env_option($options, $key);
    }
    else {
      fail "Unrecognized option '$key'";
    }
  }
  $symbol_name = env_func($symbol_name);
}
elsif ($function_name eq "seek") {
  my $position = shift @ARGV;
  fail "Missing position for '$function_name' command" unless defined $position;
  set_env_param($symbol_name, 0, "position", "$position");
  $symbol_name = env_func($symbol_name);
}
elsif ($function_name eq "shuffle") {
  my $mode = shift @ARGV;
  fail "Missing mode for '$function_name' command" unless defined $mode;
  set_env_param($symbol_name, 0, "mode", "$mode");
  $symbol_name = env_func($symbol_name);
}
elsif ($function_name eq "repeat") {
  my $mode = shift @ARGV;
  fail "Missing mode for '$function_name' command" unless defined $mode;
  set_env_param($symbol_name, 0, "mode", "$mode");
  $symbol_name = env_func($symbol_name);
}
elsif ($function_name eq "speed") {
  my $speed = shift @ARGV;
  fail "Missing speed for '$function_name' command" unless defined $speed;
  set_env_param($symbol_name, 0, "speed", "$speed");
  $symbol_name = env_func($symbol_name);
}
elsif ($function_name eq "test") {
  $symbol_name = "adapter_test";
}

if (defined shift @ARGV) {
  fail "Too many arguments";
}

my $symbol = DynaLoader::dl_find_symbol($handle, "$symbol_name")
  or fail "Symbol '$symbol_name' not found in $framework";
DynaLoader::dl_install_xsub("main::$function_name", $symbol);

eval {
  no strict "refs";
  &{"main::$function_name"}();
};
if ($@) {
  fail "Error executing $function_name: $@";
}
