#!/bin/bash

PROMPT_LINES=2

input_buffer=""

init_terminal() {
  # Hide cursor to avoid flickering
  tput civis

  # Scroll enough for the prompt in case we're at the
  # bottom of the screen
  for i in $(seq 1 $PROMPT_LINES); do
    echo
  done

  # Move cursor to the top of the room we just made
  tput cuu $((PROMPT_LINES + 1))

  # Save cursor position
  tput sc

  # Exclude prompt from scrollable area
  tput csr 0 "$(($(tput lines) - PROMPT_LINES - 1))"

  # Changing scrollable area moves cursor, so put it back
  tput rc
}

restore_terminal() {
  clear_status
  clear_prompt

  tput csr 0 "$(tput lines)"
  tput rc
  tput cnorm
}

eval_javascript_in_gnome_shell() {
  json=$(mktemp)
  busctl call --user --json=short               \
    org.gnome.Shell                   \
    /org/gnome/Shell                  \
    org.gnome.Shell                   \
    Eval "s" "$1" > "$json"
  result=$(jq '.data[0]' < "$json")
  output=$(jq '.data[1] | select(. != null and . != "") | try fromjson catch .' < "$json")
  rm -f "$json"

  if [ "$result" = "false" ]; then
    echo -e "\e[31m${output:1:-1}\e[0m" > /dev/stderr
    return 1
  fi

  echo "$output"
  return 0
}

eval_javascript_in_looking_glass() {
  # encode the text so we can side-step complicated escaping rules
  ENCODED_TEXT=$(echo -n "$1" | hexdump -v -e '/1 "%02x"')

  eval_javascript_in_gnome_shell "
  const GLib = imports.gi.GLib;
  Main.createLookingGlass();
  const results = Main.lookingGlass._resultsArea;
  Main.lookingGlass._entry.text = '${ENCODED_TEXT}'.replace(/([0-9a-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
  Main.lookingGlass._entry.clutter_text.activate();
  GLib.timeout_add(GLib.PRIORITY_DEFAULT, 125, () => {
    const index = results.get_n_children() - 1;
    if (index < 0)
      return;
      const resultsActor = results.get_children()[index];
      const output = \`\${resultsActor.get_children()[1].get_children()[0].text}\${resultsActor.get_children()[1].get_children()[1].get_children()[0].text}\`;
      Main.lookingGlass._lastEncodedResult = output.split('').map(char => char.charCodeAt(0).toString(16).padStart(2, '0')).join('');
    });
  " > /dev/null

  if [ $? -ne 0 ]; then
    return
  fi

  sleep .250

  OUTPUT=$(echo -e $(eval_javascript_in_gnome_shell 'Main.lookingGlass._lastEncodedResult;' | sed 's/\([[:xdigit:]][[:xdigit:]]\)/\\x\1/g'))

  if [ -z "$OUTPUT" ]; then
    echo -e "\e[31mCould not fetch result from call\e[0m" > /dev/stderr
    return
  fi

  eval_javascript_in_gnome_shell "delete Main.lookingGlass._lastEncodedResult;" > /dev/null

  echo ">>> $1"
  echo "${OUTPUT}"
}

jump_to_prompt() {
  # Move to the bottom of the terminal
  tput cup $(($(tput lines) - PROMPT_LINES)) 0
}

clear_prompt() {
  jump_to_prompt
  tput el
}

jump_to_status() {
  # Move to just below the prompt
  tput cup $(($(tput lines) - PROMPT_LINES + 1)) 0
}

clear_status() {
  jump_to_status
  tput el
}

print_status_line() {
  jump_to_status
  echo -ne "Type quit to exit, ^G to inspect, ^L to clear screen"
  tput rc
}

clear_screen() {
  clear
  ask_user_for_input
}

ask_user_for_input() {
  # Save cursor position
  tput sc

  clear_prompt

  tput cnorm
  read -i "$READLINE_LINE" -p ">>> " -re input_buffer
  STATUS="$?"
  tput civis

  [ $STATUS != 0 ] && exit

  if [ "$input_buffer" = "quit" -o "$input_buffer" = "q" -o "$input_buffer" = "exit" ]; then
    exit
  fi

  # Save input to history
  history -s "$input_buffer"

  # Move cursor back to saved position before output
  tput rc
}

quit_message() {
  print_status_line
  ask_user_for_input
}

load_history() {

  while IFS= read -r line; do
    history -s "$line"
  done < <(eval_javascript_in_gnome_shell 'Main.lookingGlass._history._history.join("\n");' | jq -r '. | select(. != null and . != "") | tostring')
}

check_for_unsafe_mode() {
  unsafe_mode=$(eval_javascript_in_gnome_shell 'global.context.unsafe_mode')

  if [ "$unsafe_mode" != "true" ]; then
    echo -e "Please enable unsafe-mode in the Flags tab of looking glass." > /dev/stderr
    exit
  fi
}

eval_autocomplete_javascript() {
  ENCODED_TEXT=$(echo -n "$1" | hexdump -v -e '/1 "%02x"')
  OUTPUT=$(eval_javascript_in_gnome_shell '
  const AsyncFunction = async function () {}.constructor;

  const command = `
  const JsParse = await import("resource:///org/gnome/shell/misc/jsParse.js");

  function getGlobalState() {
    const keywords = ["true", "false", "null", "new"];
    const windowProperties = Object.getOwnPropertyNames(globalThis).filter(
      a => a.charAt(0) !== "_");
      const headerProperties = JsParse.getDeclaredConstants(commandHeader);

      return keywords.concat(windowProperties).concat(headerProperties);
    }

  const commandHeader = '"'"'const {Clutter, Gio, GLib, GObject, Meta, Shell, St} = imports.gi; const Main = await import("resource:///org/gnome/shell/ui/main.js"); const inspect = Main.lookingGlass.inspect.bind(Main.lookingGlass); const it = Main.lookingGlass.getIt(); const r = Main.lookingGlass.getResult.bind(Main.lookingGlass);'"'"';

  const text="'${ENCODED_TEXT}'".replace(/([0-9a-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
  const completions = await JsParse.getCompletions(text, commandHeader, getGlobalState());
  return {
    "completions": completions[0],
    "attrHead": completions[1]
  };`;
AsyncFunction(command)();
')
if [ $? = 1 ]; then
  echo fail
fi
echo "$OUTPUT"
}

autocomplete() {
  RESULT="$(eval_autocomplete_javascript "$READLINE_LINE")"
  COMPLETIONS="$(echo "$RESULT" | jq '.completions[]' | sed 's/\"//g')"
  ATTR_HEAD=$(echo "$RESULT" | jq '.attrHead' | sed 's/\"//g')
  N_COMPLETIONS=$(echo "$COMPLETIONS" | wc -w)
  if [ $N_COMPLETIONS = 0 ]; then
    return
  elif [ $N_COMPLETIONS = 1 ]; then
    TO_ADD=$(echo $COMPLETIONS | sed s/$ATTR_HEAD//)
    READLINE_LINE+="$TO_ADD"
    (( READLINE_POINT += $(echo "$TO_ADD" | wc -c) ))
  else
    tput rc
    echo
    echo "$COMPLETIONS" | pr -T -2 -o 4
    ask_user_for_input
  fi
}

inspect() {
  eval_javascript_in_gnome_shell '
  const AsyncFunction = async function () {}.constructor;

  delete Main.lookingGlass._lastInspection;
  const command = `
  const Main = await import("resource:///org/gnome/shell/ui/main.js");
  const LookingGlass = await import("resource:///org/gnome/shell/ui/lookingGlass.js");

  Main.lookingGlass._inspector = new LookingGlass.Inspector(Main.lookingGlass);
  const inspector = Main.lookingGlass._inspector;

  inspector.connectObject("target", (i, obj, stageX, stageY) => {
    let command = "inspect(" + Math.round(stageX) + ", " + Math.round(stageY) + ")";
    Main.lookingGlass._lastInspection = command;
  },
"closed", () => {
  delete Main.lookingGlass._inspector;
});
`;
AsyncFunction(command)();
'

while sleep .250; do
  finished=$(eval_javascript_in_gnome_shell 'Main.lookingGlass._inspector? false : true')
  last_inspection=$(eval_javascript_in_gnome_shell 'Main.lookingGlass._lastInspection' | jq -r)

  [ "$finished" = "true" ] && break
  [ -n "$last_inspection" ] && break
done

tput rc
[ -n "$last_inspection" ] && eval_javascript_in_looking_glass "$last_inspection"
eval_javascript_in_gnome_shell 'delete Main.lookingGlass._lastInspection; Main.lookingGlass.setBorderPaintTarget(null);' > /dev/null

ask_user_for_input
}

main_loop() {
  bind -x '"\a"':inspect 2> /dev/null
  bind -x '"\t"':autocomplete 2> /dev/null
  bind -x '"\f"':clear_screen 2> /dev/null
  while true; do
    ask_user_for_input
    eval_javascript_in_looking_glass "$input_buffer"
  done
}

check_for_unsafe_mode

trap 'quit_message' SIGINT
trap restore_terminal EXIT

init_terminal
load_history
print_status_line
main_loop
restore_terminal
