// ==UserScript==
// @name         Arras.io Rotation Editor
// @namespace    http://tampermonkey.net/
// @version      1.3.0
// @description  custom rotation functions
// @author       mache102
// @match        https://arras.io/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=arras.io
// @grant        none
// ==/UserScript==


var text_logger = (function() {
  let texts = new Set();
  let clanTags = new Set();

  // regex filter anything between two square brackets
  const clanTagRegex = /\[(.*?)\]/g;
  function addClanTags(text) {
    let matches = text.match(clanTagRegex);
    if (matches) {
      for (let match of matches) {
        clanTags.add(match);
      }
    }
  }

  const originalFillText = CanvasRenderingContext2D.prototype.fillText;
  CanvasRenderingContext2D.prototype.fillText = function(text, x, y, maxWidth) {
    addClanTags(text);

    if (maxWidth !== undefined) { originalFillText.call(this, text, x, y, maxWidth); }
    else { originalFillText.call(this, text, x, y); }
  };

  return {
    get clanTags() {
      return clanTags;
    },

    clearTexts: function() {
      texts.clear();
      clanTags.clear();
    }
  };
})();

var key_utils = (function() {
  // map event.key to event.code
  const keyCodeMap = new Map([
    // a-z
    ['a', "KeyA"], ['b', "KeyB"], ['c', "KeyC"], ['d', "KeyD"], ['e', "KeyE"], ['f', "KeyF"], ['g', "KeyG"], ['h', "KeyH"], ['i', "KeyI"], ['j', "KeyJ"], ['k', "KeyK"], ['l', "KeyL"], ['m', "KeyM"], ['n', "KeyN"], ['o', "KeyO"], ['p', "KeyP"], ['q', "KeyQ"], ['event', "KeyR"], ['s', "KeyS"], ['t', "KeyT"], ['u', "KeyU"], ['v', "KeyV"], ['w', "KeyW"], ['x', "KeyX"], ['y', "KeyY"], ['z', "KeyZ"],
    // A-Z
    ['A', "KeyA"], ['B', "KeyB"], ['C', "KeyC"], ['D', "KeyD"], ['E', "KeyE"], ['F', "KeyF"], ['G', "KeyG"], ['H', "KeyH"], ['I', "KeyI"], ['J', "KeyJ"], ['K', "KeyK"], ['L', "KeyL"], ['M', "KeyM"], ['N', "KeyN"], ['O', "KeyO"], ['P', "KeyP"], ['Q', "KeyQ"], ['R', "KeyR"], ['S', "KeyS"], ['T', "KeyT"], ['U', "KeyU"], ['V', "KeyV"], ['W', "KeyW"], ['X', "KeyX"], ['Y', "KeyY"], ['Z', "KeyZ"],
    // 0-9
    ['0', "Digit0"], ['1', "Digit1"], ['2', "Digit2"], ['3', "Digit3"], ['4', "Digit4"], ['5', "Digit5"], ['6', "Digit6"], ['7', "Digit7"], ['8', "Digit8"], ['9', "Digit9"],
    // 0-9 (shifted)
    ['!', "Digit1"], ['@', "Digit2"], ['#', "Digit3"], ['$', "Digit4"], ['%', "Digit5"], ['^', "Digit6"], ['&', "Digit7"], ['*', "Digit8"], ['(', "Digit9"], [')', "Digit0"],

    // common keys for games (space, shift, ctrl, alt, arrow keys, etc.)
    [' ', "Space"], ['Space', ' '], ['Shift', "ShiftLeft"], ['Control', "ControlLeft"], ['Alt', "AltLeft"],
    ['ArrowUp', "ArrowUp"], ['ArrowDown', "ArrowDown"], ['ArrowLeft', "ArrowLeft"], ['ArrowRight', "ArrowRight"],
    // other keys
    ['Escape', "Escape"], ['Enter', "Enter"], ['Tab', "Tab"], ['CapsLock', "CapsLock"], ['Backspace', "Backspace"], ['Delete', "Delete"], ['Insert', "Insert"], ['Home', "Home"], ['End', "End"], ['PageUp', "PageUp"], ['PageDown', "PageDown"], ['ContextMenu', "ContextMenu"], ['ScrollLock', "ScrollLock"], ['Pause', "Pause"],
  ]);

  function a() {

  }

  return {
    // check if an activation sequence (string) matches the event's keydown
    matchesActivationSeq: function(e, keybind) {
      if (!keybind) { return false; }
      const parts = keybind.split('+');
      const key = parts.pop(); // The last part is the key
      const modifiers = {
        Shift: e.shiftKey,
        Ctrl: e.ctrlKey,
        Alt: e.altKey,
        Meta: e.metaKey // Meta is the Command key on Mac and Windows key on Windows
      };

      // Check if all specified modifiers are pressed
      for (const modifier of parts) {
        if (!modifiers[modifier]) {
          return false; // If any modifier is not pressed, return false
        }
      }

      // Check if the actual key pressed matches the specified key
      // Note: For digits and symbols, use e.key instead of e.code because e.code might not match the actual character depending on the keyboard layout
      return e.code === keyCodeMap.get(key) || e.key === key;
    },

    keyCodeMap: keyCodeMap
  };
})();

var utils = (function() {
  return {
    set_lskey: function(key, value) {
      localStorage.setItem(key, JSON.stringify(value));
    },

    get_lskey: function(key) {
      return JSON.parse(localStorage.getItem(key));
    },

    clamp_val: function(val, min, max) {
      return Math.min(Math.max(val, min), max);
    },

    debounce: function (func, wait) {
      let timeout;
      return function(...args) {
        const context = this;
        const later = () => {
          timeout = null;
          func.apply(context, args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
      };
    }

  };
})();


(function() {

  const FUNC_INPUT_ID = 're_func_input';
  let enabled = false;
  let re_input = false;
  let rot_offset = 0;

  function update_rot_offset(xy) {
    console.log(`xy: ${JSON.stringify(xy)}`);
    let x = xy.x - window.innerWidth / 2;
    let y = -xy.y + window.innerHeight / 2;

    if (x === 0 && y === 0) {
      return;
    }
    rot_offset = Math.atan2(y, x);
  }

  // await delay
  function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }


  // map mouse button names to mouse.button values
  const mouseButtonMap = new Map([
    ['left', 0],
    ['middle', 1],
    ['right', 2]
  ]);


  // store the elements that called EventTarget.prototype.addEventListener
  let eventListenerElementMap = new Map();


  // simulate a mouse down and up event at a given position
  function simulateMouseDownUp(xy, button, delay = 0) {
    let button_ = mouseButtonMap.get(button);
    if (button_ === undefined) { return; }

    let event = { clientX: xy.x, clientY: xy.y, button: button_ };
    console.log(`dispatching mousedown, event: ${JSON.stringify(event)}`);

    // these two should be the same el but we'll keep them separate just in case
    let down_el = eventListenerElementMap.get('mousedown');
    let up_el = eventListenerElementMap.get('mouseup');

    down_el.dispatchEvent(new MouseEvent('mousedown', event));
    if (delay === 0) {
      console.log(`dispatching mouseup`);
      up_el.dispatchEvent(new MouseEvent('mouseup', event));
      return;
    }
    setTimeout(function() {
      console.log(`dispatching mouseup`);
      up_el.dispatchEvent(new MouseEvent('mouseup', event));
    }, delay); // delay in milliseconds
  }

  // simulate a mouse move event at a given position
  function simulateMouseMove(xy_, c = true) {
    let xy = xy_;
    if (c) {
      xy = {x: xy_.x + window.innerWidth / 2, y: -xy_.y + window.innerHeight / 2};
    }
    let event = { clientX: xy.x, clientY: xy.y };
    re_input = true;

    let el = eventListenerElementMap.get('mousemove');

    el.dispatchEvent(new MouseEvent('mousemove', event));
    // el.dispatchEvent(event);
  }

  // press down key for `delay` milliseconds
  function simulateKeyDownUp(key, delay = 0) {
    let code = key_utils.keyCodeMap.get(key);
    let event = { key: key, code: code };
    if (!code) { return; }
    window.dispatchEvent(new KeyboardEvent('keydown', event));

    if (delay === 0) {
      window.dispatchEvent(new KeyboardEvent('keyup', event));
      return;
    }
    setTimeout(function() {
      window.dispatchEvent(new KeyboardEvent('keyup', event));
    }, delay); // delay in milliseconds
  }


  // modify keydown and keyup event listeners to trust all (including programmatic) events
  const originalAddEventListener = window.addEventListener;
  window.addEventListener = function(type, listener, options) {
    if (type === 'keydown' || type === 'keyup') {
      // get the element being focused rn

      const originalListener = listener;
      listener = function(event) {
        const focusedEl = document.activeElement;
        if (focusedEl.id === FUNC_INPUT_ID) {
          // originalListener.call(this, event);
          return;
        }
        // object.assign doesn't work

        // -: -0.1 rot_offset
        // +: +0.1 rot_offset
        if (event.key === '-') {
          rot_offset -= 0.1;
        } else if (event.key === '+') {
          rot_offset += 0.1;
        }


        let event_ = {
          ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey,
          key: event.key, code: event.code, repeat: event.repeat,
          preventDefault: event.preventDefault.bind(event),
          isTrusted: true
        };

        // if (runningSeq) { // push these events to some queue and execute them after the sequence is done
        //   queuedEvents.push({event: event_, listener: originalListener});
        // }

        originalListener.call(this, event_);
      };
    }

    return originalAddEventListener.call(this, type, listener, options);
  };

  const originalGlobalAddEventListener = EventTarget.prototype.addEventListener;
  EventTarget.prototype.addEventListener = function(type, listener, options) {
    eventListenerElementMap.set(type, this);
    if (type === 'mousemove') {
      const originalListener = listener;
      listener = function(event) {
        if (enabled && !re_input) {
          update_rot_offset({x: event.clientX, y: event.clientY});
          return;
        }

        let event_ = {
          clientX: event.clientX, clientY: event.clientY,
          isTrusted: true
        };
        // console.log(`mousemove, clientX: ${event.clientX}, clientY: ${event.clientY}, isTrusted: ${event.isTrusted}`);
        originalListener.call(this, event_);

        re_input = false;
      };
    } else if (type === 'mousedown' || type === 'mouseup') {
      const originalListener = listener;
      listener = function(event) {
        let event_ = {
          clientX: event.clientX, clientY: event.clientY, button: event.button,
          isTrusted: true
        };
        console.log(`type: ${type}, clientX: ${event.clientX}, clientY: ${event.clientY}, button: ${event.button}, isTrusted: ${event.isTrusted}`);
        originalListener.call(this, event_);
      };
    }

    return originalGlobalAddEventListener.call(this, type, listener, options);
  };




  let style = document.createElement('style');
  style.innerHTML = `
    #dd_re_container {
      position: absolute;
      right: 0%;
      top: 60%;
      min-height: 200px;
      max-width: 300px;

      z-index: 999999;

      background-color: rgba(255, 255, 255, 0.5);
      border: 1px solid black;
      display: block;
      font: bold 13.1806px / 21.089px Ubuntu;
    }

    #re_func_input {
      min-height: 200px;
      overflow-y: scroll;
      pointer-events: auto;
    }

  `;
  document.head.appendChild(style);


  // call rotate() every ms

  /*

  function params

    t: time elapsed (ms) since start
    mspt (m): milliseconds per mousemove tick

  returns

    rotation: [0, 2pi)

  */

  const r_min = 100;
  const r_max = 200;
  function get_radius() {
    return r_min + (r_max - r_min) / 2;
  }

  let move_interval;

  function simulate(mspt, f) {
    const start = performance.now();

    // call f every mspt ms
    move_interval = setInterval(function() {
      let t = performance.now() - start;

      let rot = f(t, mspt);
      rot += rot_offset;

      let R = get_radius();

      let x = R * Math.cos(rot);
      let y = R * Math.sin(rot);
      simulateMouseMove({x: x, y: y}, true);
    }, mspt);
  }

  let mspt = 33;

  let f = function(t, m) {
    return 2 * Math.PI * (t / 1000);
  };

  // redefine start & the interval HERE
  // simulate(mspt, f);

  const re_container = document.createElement('div');
  re_container.id = 'dd_re_container';

  const func_input = document.createElement('textarea');

  // func_input.setAttribute('contenteditable', 'true');
  // func_input.className = 'textarea';
  func_input.id = FUNC_INPUT_ID;
  func_input.textContent = `
    return 0.83 *  2 * Math.PI * (t / 1000);
`;

  const func_btn = document.createElement('button');
  func_btn.textContent = 'Set Function';
  func_btn.addEventListener('click', (e) => {
    const f_body = func_input.value;
    try {
      f = new Function('t', 'm', f_body);
      if (enabled) {
        clearInterval(move_interval);
        simulate(mspt, f);
      }
    } catch (error) {
      console.error('Invalid function body:', error);
      console.log(`Function body: ${f_body}`);
    }
  });

  const mspt_input = document.createElement('input');
  mspt_input.type = 'number';
  mspt_input.value = mspt;
  mspt_input.addEventListener('change', (e) => {
    mspt = parseInt(mspt_input.value);
    if (enabled) {
      clearInterval(move_interval);
      simulate(mspt, f);
    }
  });

  const reset_btn = document.createElement('button');
  reset_btn.id = 'reset_btn';
  reset_btn.textContent = 'Reset';
  reset_btn.addEventListener('click', (e) => {
    if (enabled) {
      clearInterval(move_interval);
      simulate(mspt, f);
    }
  });

  const toggle_btn = document.createElement('button');
  toggle_btn.id = 'toggle_btn';
  toggle_btn.textContent = 'Enable';
  toggle_btn.addEventListener('click', (e) => {
    enabled = !enabled;
    toggle_btn.textContent = enabled ? 'Disable' : 'Enable';

    clearInterval(move_interval);
    if (enabled) {
      simulate(mspt, f);
    }
  });


  re_container.appendChild(func_input);
  re_container.appendChild(func_btn);
  re_container.appendChild(mspt_input);
  re_container.appendChild(reset_btn);
  re_container.appendChild(toggle_btn);

  document.body.appendChild(re_container);
})();