Google Snake Mod Loader (Standard)

Allows you to run multiple different google snake mods

// ==UserScript==
// @name         Google Snake Mod Loader (Standard)
// @namespace    https://github.com/DarkSnakeGang
// @version      1.0.0
// @description  Allows you to run multiple different google snake mods
// @author       DarkSnakeGang (https://github.com/DarkSnakeGang)
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @run-at       document-start
// @grant        none
// @include      /^https:\/\/(www\.)?google\.(com|ad|ae|com\.af|com\.ag|com\.ai|al|am|co\.ao|com\.ar|as|at|com\.au|az|ba|com\.bd|be|bf|bg|com\.bh|bi|bj|com\.bn|com\.bo|com\.br|bs|bt|co\.bw|by|com\.bz|ca|cd|cf|cg|ch|ci|co\.ck|cl|cm|cn|com\.co|co\.cr|com\.cu|cv|com\.cy|cz|de|dj|dk|dm|com\.do|dz|com\.ec|ee|com\.eg|es|com\.et|fi|com\.fj|fm|fr|ga|ge|gg|com\.gh|com\.gi|gl|gm|gr|com\.gt|gy|com\.hk|hn|hr|ht|hu|co\.id|ie|co\.il|im|co\.in|iq|is|it|je|com\.jm|jo|co\.jp|co\.ke|com\.kh|ki|kg|co\.kr|com\.kw|kz|la|com\.lb|li|lk|co\.ls|lt|lu|lv|com\.ly|co\.ma|md|me|mg|mk|ml|com\.mm|mn|ms|com\.mt|mu|mv|mw|com\.mx|com\.my|co\.mz|com\.na|com\.ng|com\.ni|ne|nl|no|com\.np|nr|nu|co\.nz|com\.om|com\.pa|com\.pe|com\.pg|com\.ph|com\.pk|pl|pn|com\.pr|ps|pt|com\.py|com\.qa|ro|ru|rw|com\.sa|com\.sb|sc|se|com\.sg|sh|si|sk|com\.sl|sn|so|sm|sr|st|com\.sv|td|tg|co\.th|com\.tj|tl|tm|tn|to|com\.tr|tt|com\.tw|co\.tz|com\.ua|co\.ug|co\.uk|com\.uy|co\.uz|com\.vc|co\.ve|vg|co\.vi|com\.vn|vu|ws|rs|co\.za|co\.zm|co\.zw|cat)\/search.*(snake|serpiente|serpent|serpente|%E8%B4%AA%E5%90%83%E8%9B%87|%E0%A4%B8%E0%A4%BE%E0%A4%81%E0%A4%AA|%D8%AB%D8%B9%D8%A8%D8%A7%D9%86|%D0%B7%D0%BC%D0%B5%D0%B9%D0%BA%D0%B0|%E3%83%98%E3%83%93%E3%82%B2%E3%83%BC%E3%83%A0|%E0%A4%B8%E0%A4%BE%E0%A4%AA|y%C4%B1lan|%E0%AE%AA%E0%AE%BE%E0%AE%AE%E0%AF%8D%E0%AE%AA%E0%AF%81|r%E1%BA%AFn\+s%C4%83n\+m%E1%BB%93i|%EC%8A%A4%EB%84%A4%EC%9D%B4%ED%81%AC|%E0%B9%80%E0%B8%81%E0%B8%A1%E0%B8%87%E0%B8%B9|%E1%8B%A8%E1%8A%A5%E1%89%A3%E1%89%A5\+%E1%8C%A8%E1%8B%8B%E1%89%B3|%E0%A4%B8%E0%A4%BE%E0%A4%81%E0%A4%AA|%E1%80%99%E1%80%BC%E1%80%BD%E1%80%B1|w%C4%85%C5%BC|hra\+had|had%C3%AD\+hra|slange|ular).*$/
// @match        https://*.google.com/fbx?fbx=snake_arcade
// @match        https://*.google.ad/fbx?fbx=snake_arcade
// @match        https://*.google.ae/fbx?fbx=snake_arcade
// @match        https://*.google.com.af/fbx?fbx=snake_arcade
// @match        https://*.google.com.ag/fbx?fbx=snake_arcade
// @match        https://*.google.com.ai/fbx?fbx=snake_arcade
// @match        https://*.google.al/fbx?fbx=snake_arcade
// @match        https://*.google.am/fbx?fbx=snake_arcade
// @match        https://*.google.co.ao/fbx?fbx=snake_arcade
// @match        https://*.google.com.ar/fbx?fbx=snake_arcade
// @match        https://*.google.as/fbx?fbx=snake_arcade
// @match        https://*.google.at/fbx?fbx=snake_arcade
// @match        https://*.google.com.au/fbx?fbx=snake_arcade
// @match        https://*.google.az/fbx?fbx=snake_arcade
// @match        https://*.google.ba/fbx?fbx=snake_arcade
// @match        https://*.google.com.bd/fbx?fbx=snake_arcade
// @match        https://*.google.be/fbx?fbx=snake_arcade
// @match        https://*.google.bf/fbx?fbx=snake_arcade
// @match        https://*.google.bg/fbx?fbx=snake_arcade
// @match        https://*.google.com.bh/fbx?fbx=snake_arcade
// @match        https://*.google.bi/fbx?fbx=snake_arcade
// @match        https://*.google.bj/fbx?fbx=snake_arcade
// @match        https://*.google.com.bn/fbx?fbx=snake_arcade
// @match        https://*.google.com.bo/fbx?fbx=snake_arcade
// @match        https://*.google.com.br/fbx?fbx=snake_arcade
// @match        https://*.google.bs/fbx?fbx=snake_arcade
// @match        https://*.google.bt/fbx?fbx=snake_arcade
// @match        https://*.google.co.bw/fbx?fbx=snake_arcade
// @match        https://*.google.by/fbx?fbx=snake_arcade
// @match        https://*.google.com.bz/fbx?fbx=snake_arcade
// @match        https://*.google.ca/fbx?fbx=snake_arcade
// @match        https://*.google.cd/fbx?fbx=snake_arcade
// @match        https://*.google.cf/fbx?fbx=snake_arcade
// @match        https://*.google.cg/fbx?fbx=snake_arcade
// @match        https://*.google.ch/fbx?fbx=snake_arcade
// @match        https://*.google.ci/fbx?fbx=snake_arcade
// @match        https://*.google.co.ck/fbx?fbx=snake_arcade
// @match        https://*.google.cl/fbx?fbx=snake_arcade
// @match        https://*.google.cm/fbx?fbx=snake_arcade
// @match        https://*.google.cn/fbx?fbx=snake_arcade
// @match        https://*.google.com.co/fbx?fbx=snake_arcade
// @match        https://*.google.co.cr/fbx?fbx=snake_arcade
// @match        https://*.google.com.cu/fbx?fbx=snake_arcade
// @match        https://*.google.cv/fbx?fbx=snake_arcade
// @match        https://*.google.com.cy/fbx?fbx=snake_arcade
// @match        https://*.google.cz/fbx?fbx=snake_arcade
// @match        https://*.google.de/fbx?fbx=snake_arcade
// @match        https://*.google.dj/fbx?fbx=snake_arcade
// @match        https://*.google.dk/fbx?fbx=snake_arcade
// @match        https://*.google.dm/fbx?fbx=snake_arcade
// @match        https://*.google.com.do/fbx?fbx=snake_arcade
// @match        https://*.google.dz/fbx?fbx=snake_arcade
// @match        https://*.google.com.ec/fbx?fbx=snake_arcade
// @match        https://*.google.ee/fbx?fbx=snake_arcade
// @match        https://*.google.com.eg/fbx?fbx=snake_arcade
// @match        https://*.google.es/fbx?fbx=snake_arcade
// @match        https://*.google.com.et/fbx?fbx=snake_arcade
// @match        https://*.google.fi/fbx?fbx=snake_arcade
// @match        https://*.google.com.fj/fbx?fbx=snake_arcade
// @match        https://*.google.fm/fbx?fbx=snake_arcade
// @match        https://*.google.fr/fbx?fbx=snake_arcade
// @match        https://*.google.ga/fbx?fbx=snake_arcade
// @match        https://*.google.ge/fbx?fbx=snake_arcade
// @match        https://*.google.gg/fbx?fbx=snake_arcade
// @match        https://*.google.com.gh/fbx?fbx=snake_arcade
// @match        https://*.google.com.gi/fbx?fbx=snake_arcade
// @match        https://*.google.gl/fbx?fbx=snake_arcade
// @match        https://*.google.gm/fbx?fbx=snake_arcade
// @match        https://*.google.gr/fbx?fbx=snake_arcade
// @match        https://*.google.com.gt/fbx?fbx=snake_arcade
// @match        https://*.google.gy/fbx?fbx=snake_arcade
// @match        https://*.google.com.hk/fbx?fbx=snake_arcade
// @match        https://*.google.hn/fbx?fbx=snake_arcade
// @match        https://*.google.hr/fbx?fbx=snake_arcade
// @match        https://*.google.ht/fbx?fbx=snake_arcade
// @match        https://*.google.hu/fbx?fbx=snake_arcade
// @match        https://*.google.co.id/fbx?fbx=snake_arcade
// @match        https://*.google.ie/fbx?fbx=snake_arcade
// @match        https://*.google.co.il/fbx?fbx=snake_arcade
// @match        https://*.google.im/fbx?fbx=snake_arcade
// @match        https://*.google.co.in/fbx?fbx=snake_arcade
// @match        https://*.google.iq/fbx?fbx=snake_arcade
// @match        https://*.google.is/fbx?fbx=snake_arcade
// @match        https://*.google.it/fbx?fbx=snake_arcade
// @match        https://*.google.je/fbx?fbx=snake_arcade
// @match        https://*.google.com.jm/fbx?fbx=snake_arcade
// @match        https://*.google.jo/fbx?fbx=snake_arcade
// @match        https://*.google.co.jp/fbx?fbx=snake_arcade
// @match        https://*.google.co.ke/fbx?fbx=snake_arcade
// @match        https://*.google.com.kh/fbx?fbx=snake_arcade
// @match        https://*.google.ki/fbx?fbx=snake_arcade
// @match        https://*.google.kg/fbx?fbx=snake_arcade
// @match        https://*.google.co.kr/fbx?fbx=snake_arcade
// @match        https://*.google.com.kw/fbx?fbx=snake_arcade
// @match        https://*.google.kz/fbx?fbx=snake_arcade
// @match        https://*.google.la/fbx?fbx=snake_arcade
// @match        https://*.google.com.lb/fbx?fbx=snake_arcade
// @match        https://*.google.li/fbx?fbx=snake_arcade
// @match        https://*.google.lk/fbx?fbx=snake_arcade
// @match        https://*.google.co.ls/fbx?fbx=snake_arcade
// @match        https://*.google.lt/fbx?fbx=snake_arcade
// @match        https://*.google.lu/fbx?fbx=snake_arcade
// @match        https://*.google.lv/fbx?fbx=snake_arcade
// @match        https://*.google.com.ly/fbx?fbx=snake_arcade
// @match        https://*.google.co.ma/fbx?fbx=snake_arcade
// @match        https://*.google.md/fbx?fbx=snake_arcade
// @match        https://*.google.me/fbx?fbx=snake_arcade
// @match        https://*.google.mg/fbx?fbx=snake_arcade
// @match        https://*.google.mk/fbx?fbx=snake_arcade
// @match        https://*.google.ml/fbx?fbx=snake_arcade
// @match        https://*.google.com.mm/fbx?fbx=snake_arcade
// @match        https://*.google.mn/fbx?fbx=snake_arcade
// @match        https://*.google.ms/fbx?fbx=snake_arcade
// @match        https://*.google.com.mt/fbx?fbx=snake_arcade
// @match        https://*.google.mu/fbx?fbx=snake_arcade
// @match        https://*.google.mv/fbx?fbx=snake_arcade
// @match        https://*.google.mw/fbx?fbx=snake_arcade
// @match        https://*.google.com.mx/fbx?fbx=snake_arcade
// @match        https://*.google.com.my/fbx?fbx=snake_arcade
// @match        https://*.google.co.mz/fbx?fbx=snake_arcade
// @match        https://*.google.com.na/fbx?fbx=snake_arcade
// @match        https://*.google.com.ng/fbx?fbx=snake_arcade
// @match        https://*.google.com.ni/fbx?fbx=snake_arcade
// @match        https://*.google.ne/fbx?fbx=snake_arcade
// @match        https://*.google.nl/fbx?fbx=snake_arcade
// @match        https://*.google.no/fbx?fbx=snake_arcade
// @match        https://*.google.com.np/fbx?fbx=snake_arcade
// @match        https://*.google.nr/fbx?fbx=snake_arcade
// @match        https://*.google.nu/fbx?fbx=snake_arcade
// @match        https://*.google.co.nz/fbx?fbx=snake_arcade
// @match        https://*.google.com.om/fbx?fbx=snake_arcade
// @match        https://*.google.com.pa/fbx?fbx=snake_arcade
// @match        https://*.google.com.pe/fbx?fbx=snake_arcade
// @match        https://*.google.com.pg/fbx?fbx=snake_arcade
// @match        https://*.google.com.ph/fbx?fbx=snake_arcade
// @match        https://*.google.com.pk/fbx?fbx=snake_arcade
// @match        https://*.google.pl/fbx?fbx=snake_arcade
// @match        https://*.google.pn/fbx?fbx=snake_arcade
// @match        https://*.google.com.pr/fbx?fbx=snake_arcade
// @match        https://*.google.ps/fbx?fbx=snake_arcade
// @match        https://*.google.pt/fbx?fbx=snake_arcade
// @match        https://*.google.com.py/fbx?fbx=snake_arcade
// @match        https://*.google.com.qa/fbx?fbx=snake_arcade
// @match        https://*.google.ro/fbx?fbx=snake_arcade
// @match        https://*.google.ru/fbx?fbx=snake_arcade
// @match        https://*.google.rw/fbx?fbx=snake_arcade
// @match        https://*.google.com.sa/fbx?fbx=snake_arcade
// @match        https://*.google.com.sb/fbx?fbx=snake_arcade
// @match        https://*.google.sc/fbx?fbx=snake_arcade
// @match        https://*.google.se/fbx?fbx=snake_arcade
// @match        https://*.google.com.sg/fbx?fbx=snake_arcade
// @match        https://*.google.sh/fbx?fbx=snake_arcade
// @match        https://*.google.si/fbx?fbx=snake_arcade
// @match        https://*.google.sk/fbx?fbx=snake_arcade
// @match        https://*.google.com.sl/fbx?fbx=snake_arcade
// @match        https://*.google.sn/fbx?fbx=snake_arcade
// @match        https://*.google.so/fbx?fbx=snake_arcade
// @match        https://*.google.sm/fbx?fbx=snake_arcade
// @match        https://*.google.sr/fbx?fbx=snake_arcade
// @match        https://*.google.st/fbx?fbx=snake_arcade
// @match        https://*.google.com.sv/fbx?fbx=snake_arcade
// @match        https://*.google.td/fbx?fbx=snake_arcade
// @match        https://*.google.tg/fbx?fbx=snake_arcade
// @match        https://*.google.co.th/fbx?fbx=snake_arcade
// @match        https://*.google.com.tj/fbx?fbx=snake_arcade
// @match        https://*.google.tl/fbx?fbx=snake_arcade
// @match        https://*.google.tm/fbx?fbx=snake_arcade
// @match        https://*.google.tn/fbx?fbx=snake_arcade
// @match        https://*.google.to/fbx?fbx=snake_arcade
// @match        https://*.google.com.tr/fbx?fbx=snake_arcade
// @match        https://*.google.tt/fbx?fbx=snake_arcade
// @match        https://*.google.com.tw/fbx?fbx=snake_arcade
// @match        https://*.google.co.tz/fbx?fbx=snake_arcade
// @match        https://*.google.com.ua/fbx?fbx=snake_arcade
// @match        https://*.google.co.ug/fbx?fbx=snake_arcade
// @match        https://*.google.co.uk/fbx?fbx=snake_arcade
// @match        https://*.google.com.uy/fbx?fbx=snake_arcade
// @match        https://*.google.co.uz/fbx?fbx=snake_arcade
// @match        https://*.google.com.vc/fbx?fbx=snake_arcade
// @match        https://*.google.co.ve/fbx?fbx=snake_arcade
// @match        https://*.google.vg/fbx?fbx=snake_arcade
// @match        https://*.google.co.vi/fbx?fbx=snake_arcade
// @match        https://*.google.com.vn/fbx?fbx=snake_arcade
// @match        https://*.google.vu/fbx?fbx=snake_arcade
// @match        https://*.google.ws/fbx?fbx=snake_arcade
// @match        https://*.google.rs/fbx?fbx=snake_arcade
// @match        https://*.google.co.za/fbx?fbx=snake_arcade
// @match        https://*.google.co.zm/fbx?fbx=snake_arcade
// @match        https://*.google.co.zw/fbx?fbx=snake_arcade
// @match        https://*.google.cat/fbx?fbx=snake_arcade
// @match        https://*.google.com/search*snake*
// @match        https://*.google.ad/search*snake*
// @match        https://*.google.ae/search*snake*
// @match        https://*.google.com.af/search*snake*
// @match        https://*.google.com.ag/search*snake*
// @match        https://*.google.com.ai/search*snake*
// @match        https://*.google.al/search*snake*
// @match        https://*.google.am/search*snake*
// @match        https://*.google.co.ao/search*snake*
// @match        https://*.google.com.ar/search*snake*
// @match        https://*.google.as/search*snake*
// @match        https://*.google.at/search*snake*
// @match        https://*.google.com.au/search*snake*
// @match        https://*.google.az/search*snake*
// @match        https://*.google.ba/search*snake*
// @match        https://*.google.com.bd/search*snake*
// @match        https://*.google.be/search*snake*
// @match        https://*.google.bf/search*snake*
// @match        https://*.google.bg/search*snake*
// @match        https://*.google.com.bh/search*snake*
// @match        https://*.google.bi/search*snake*
// @match        https://*.google.bj/search*snake*
// @match        https://*.google.com.bn/search*snake*
// @match        https://*.google.com.bo/search*snake*
// @match        https://*.google.com.br/search*snake*
// @match        https://*.google.bs/search*snake*
// @match        https://*.google.bt/search*snake*
// @match        https://*.google.co.bw/search*snake*
// @match        https://*.google.by/search*snake*
// @match        https://*.google.com.bz/search*snake*
// @match        https://*.google.ca/search*snake*
// @match        https://*.google.cd/search*snake*
// @match        https://*.google.cf/search*snake*
// @match        https://*.google.cg/search*snake*
// @match        https://*.google.ch/search*snake*
// @match        https://*.google.ci/search*snake*
// @match        https://*.google.co.ck/search*snake*
// @match        https://*.google.cl/search*snake*
// @match        https://*.google.cm/search*snake*
// @match        https://*.google.cn/search*snake*
// @match        https://*.google.com.co/search*snake*
// @match        https://*.google.co.cr/search*snake*
// @match        https://*.google.com.cu/search*snake*
// @match        https://*.google.cv/search*snake*
// @match        https://*.google.com.cy/search*snake*
// @match        https://*.google.cz/search*snake*
// @match        https://*.google.de/search*snake*
// @match        https://*.google.dj/search*snake*
// @match        https://*.google.dk/search*snake*
// @match        https://*.google.dm/search*snake*
// @match        https://*.google.com.do/search*snake*
// @match        https://*.google.dz/search*snake*
// @match        https://*.google.com.ec/search*snake*
// @match        https://*.google.ee/search*snake*
// @match        https://*.google.com.eg/search*snake*
// @match        https://*.google.es/search*snake*
// @match        https://*.google.com.et/search*snake*
// @match        https://*.google.fi/search*snake*
// @match        https://*.google.com.fj/search*snake*
// @match        https://*.google.fm/search*snake*
// @match        https://*.google.fr/search*snake*
// @match        https://*.google.ga/search*snake*
// @match        https://*.google.ge/search*snake*
// @match        https://*.google.gg/search*snake*
// @match        https://*.google.com.gh/search*snake*
// @match        https://*.google.com.gi/search*snake*
// @match        https://*.google.gl/search*snake*
// @match        https://*.google.gm/search*snake*
// @match        https://*.google.gr/search*snake*
// @match        https://*.google.com.gt/search*snake*
// @match        https://*.google.gy/search*snake*
// @match        https://*.google.com.hk/search*snake*
// @match        https://*.google.hn/search*snake*
// @match        https://*.google.hr/search*snake*
// @match        https://*.google.ht/search*snake*
// @match        https://*.google.hu/search*snake*
// @match        https://*.google.co.id/search*snake*
// @match        https://*.google.ie/search*snake*
// @match        https://*.google.co.il/search*snake*
// @match        https://*.google.im/search*snake*
// @match        https://*.google.co.in/search*snake*
// @match        https://*.google.iq/search*snake*
// @match        https://*.google.is/search*snake*
// @match        https://*.google.it/search*snake*
// @match        https://*.google.je/search*snake*
// @match        https://*.google.com.jm/search*snake*
// @match        https://*.google.jo/search*snake*
// @match        https://*.google.co.jp/search*snake*
// @match        https://*.google.co.ke/search*snake*
// @match        https://*.google.com.kh/search*snake*
// @match        https://*.google.ki/search*snake*
// @match        https://*.google.kg/search*snake*
// @match        https://*.google.co.kr/search*snake*
// @match        https://*.google.com.kw/search*snake*
// @match        https://*.google.kz/search*snake*
// @match        https://*.google.la/search*snake*
// @match        https://*.google.com.lb/search*snake*
// @match        https://*.google.li/search*snake*
// @match        https://*.google.lk/search*snake*
// @match        https://*.google.co.ls/search*snake*
// @match        https://*.google.lt/search*snake*
// @match        https://*.google.lu/search*snake*
// @match        https://*.google.lv/search*snake*
// @match        https://*.google.com.ly/search*snake*
// @match        https://*.google.co.ma/search*snake*
// @match        https://*.google.md/search*snake*
// @match        https://*.google.me/search*snake*
// @match        https://*.google.mg/search*snake*
// @match        https://*.google.mk/search*snake*
// @match        https://*.google.ml/search*snake*
// @match        https://*.google.com.mm/search*snake*
// @match        https://*.google.mn/search*snake*
// @match        https://*.google.ms/search*snake*
// @match        https://*.google.com.mt/search*snake*
// @match        https://*.google.mu/search*snake*
// @match        https://*.google.mv/search*snake*
// @match        https://*.google.mw/search*snake*
// @match        https://*.google.com.mx/search*snake*
// @match        https://*.google.com.my/search*snake*
// @match        https://*.google.co.mz/search*snake*
// @match        https://*.google.com.na/search*snake*
// @match        https://*.google.com.ng/search*snake*
// @match        https://*.google.com.ni/search*snake*
// @match        https://*.google.ne/search*snake*
// @match        https://*.google.nl/search*snake*
// @match        https://*.google.no/search*snake*
// @match        https://*.google.com.np/search*snake*
// @match        https://*.google.nr/search*snake*
// @match        https://*.google.nu/search*snake*
// @match        https://*.google.co.nz/search*snake*
// @match        https://*.google.com.om/search*snake*
// @match        https://*.google.com.pa/search*snake*
// @match        https://*.google.com.pe/search*snake*
// @match        https://*.google.com.pg/search*snake*
// @match        https://*.google.com.ph/search*snake*
// @match        https://*.google.com.pk/search*snake*
// @match        https://*.google.pl/search*snake*
// @match        https://*.google.pn/search*snake*
// @match        https://*.google.com.pr/search*snake*
// @match        https://*.google.ps/search*snake*
// @match        https://*.google.pt/search*snake*
// @match        https://*.google.com.py/search*snake*
// @match        https://*.google.com.qa/search*snake*
// @match        https://*.google.ro/search*snake*
// @match        https://*.google.ru/search*snake*
// @match        https://*.google.rw/search*snake*
// @match        https://*.google.com.sa/search*snake*
// @match        https://*.google.com.sb/search*snake*
// @match        https://*.google.sc/search*snake*
// @match        https://*.google.se/search*snake*
// @match        https://*.google.com.sg/search*snake*
// @match        https://*.google.sh/search*snake*
// @match        https://*.google.si/search*snake*
// @match        https://*.google.sk/search*snake*
// @match        https://*.google.com.sl/search*snake*
// @match        https://*.google.sn/search*snake*
// @match        https://*.google.so/search*snake*
// @match        https://*.google.sm/search*snake*
// @match        https://*.google.sr/search*snake*
// @match        https://*.google.st/search*snake*
// @match        https://*.google.com.sv/search*snake*
// @match        https://*.google.td/search*snake*
// @match        https://*.google.tg/search*snake*
// @match        https://*.google.co.th/search*snake*
// @match        https://*.google.com.tj/search*snake*
// @match        https://*.google.tl/search*snake*
// @match        https://*.google.tm/search*snake*
// @match        https://*.google.tn/search*snake*
// @match        https://*.google.to/search*snake*
// @match        https://*.google.com.tr/search*snake*
// @match        https://*.google.tt/search*snake*
// @match        https://*.google.com.tw/search*snake*
// @match        https://*.google.co.tz/search*snake*
// @match        https://*.google.com.ua/search*snake*
// @match        https://*.google.co.ug/search*snake*
// @match        https://*.google.co.uk/search*snake*
// @match        https://*.google.com.uy/search*snake*
// @match        https://*.google.co.uz/search*snake*
// @match        https://*.google.com.vc/search*snake*
// @match        https://*.google.co.ve/search*snake*
// @match        https://*.google.vg/search*snake*
// @match        https://*.google.co.vi/search*snake*
// @match        https://*.google.com.vn/search*snake*
// @match        https://*.google.vu/search*snake*
// @match        https://*.google.ws/search*snake*
// @match        https://*.google.rs/search*snake*
// @match        https://*.google.co.za/search*snake*
// @match        https://*.google.co.zm/search*snake*
// @match        https://*.google.co.zw/search*snake*
// @match        https://*.google.cat/search*snake*
// ==/UserScript==

const IS_DEVELOPER_MODE = false;

let modsConfig = {
  moreMenu: {
    displayName: 'More Menu Mod',
    hasUrl: true,
    url: 'https://raw.githubusercontent.com/DarkSnakeGang/GoogleSnakeCustomMenuStuff/main/modloadercode.js'
  },
  levelEditorMod: {
    displayName: 'Level Editor Mod',
    hasUrl: true,
    url: 'https://raw.githubusercontent.com/DarkSnakeGang/GoogleSnakeLevelEditor/main/modloadercode.js'
  },
  mouseMode: {
    displayName: 'Mouse Mode',
    hasUrl: true,
    url: 'https://raw.githubusercontent.com/DarkSnakeGang/GoogleSnakeMouseMode/main/modloadercode.js'
  },
}

if(IS_DEVELOPER_MODE) {
  modsConfig.testMod = {
    displayName: 'Test Mod',
    hasUrl: false
  };
  modsConfig.customUrl = {
    displayName: 'Load from url (see advanced options)',
    customModName: JSON.parse(localStorage.getItem('snakeAdvancedSettings'))?.customModName ?? 'PLEASE_CHOOSE_CUSTOM_NAME_FROM_ADVANCED_SETTINGS',
    url: JSON.parse(localStorage.getItem('snakeAdvancedSettings'))?.customUrl ?? 'PLEASE_CHOOSE_URL_FROM_ADVANCED_SETTINGS',
    hasUrl: true
  }
}

//Replace appendChild so we can intercept it
document.body.appendChildOld = document.body.appendChild;

document.body.appendChild = function(el) {
  if(el.tagName !== 'SCRIPT') return document.body.appendChildOld(el);
  if(el.src === '' || el.src.includes('apis.google.com')) return document.body.appendChildOld(el);

  const currentlySelectedMod = localStorage.getItem('snakeChosenMod');

  //Just do default behaviour if it isn't the snake script or we don't have a mod to run
  const regexForScriptSrc = window.location.href.includes('fbx?fbx=snake_arcade') ? /xjs=s1$/ : /xjs=s2$/;
  if(!(regexForScriptSrc.test(el.src)) || currentlySelectedMod === null || currentlySelectedMod === 'none') return document.body.appendChildOld(el);

  //default behaviour if we can't find any snake images on the webpage
  if(document.body.querySelector('img[src^="//www.google.com/logos/fnbx/snake_arcade"]') === null) return document.body.appendChildOld(el);

  //Log which script(s) go through the ajax process
  console.log(el);

  //Make sure to return the correct thing that appendChild would normally return
  let returnVal = el instanceof DocumentFragment ? new DocumentFragment : el;

  /*
    Run some code depending on what google snake mod was chosen
    Request the text contents of the google snake code.
    Alter the contents of the google snake code (depending on the selected mod) and then eval
    Run some code afterwards depending on what google snake mod was chosen
  */
  const req = new XMLHttpRequest();
  req.open('GET', el.src, false);

  req.onload = function() {
    //Do default behaviour if the source code doesn't look like the google snake code.
    //Set returnVal so it returns the correct thing for document.body.appendChild.
    if(this.responseText.indexOf('trophy') === -1 || this.responseText.indexOf('apple') === -1 || this.responseText.indexOf('snake_arcade') === -1) {
      returnVal = document.body.appendChildOld(el);
      return;
    }

    console.log(`Selected mod: ${currentlySelectedMod}`);
    //Make sure currentlySelectedMod is an allowed value
    if(!(modsConfig.hasOwnProperty(currentlySelectedMod) || currentlySelectedMod === 'none' || currentlySelectedMod === null)) {
      const errMessage = `Bad value of snakeChosenMod: ${currentlySelectedMod}. The current allowed values are ${Object.keys(modsConfig)}. Changing this to the "None" setting for next time.`;
      localStorage.setItem('snakeChosenMod', 'none');
      throw new Error(errMessage);
    } 

    if(modsConfig[currentlySelectedMod].hasUrl) {
      const modUrl = modsConfig[currentlySelectedMod].url;

      //Load and run the code for this mod.
      console.log(`Retrieving code for this mod from ${modUrl}`);

      loadAndRunCodeSynchronous(modUrl);
    }

    //Name of the object that contains runCodeBefore/alterSnakeCode/runCodeAfter methods
    let modObjectName = currentlySelectedMod;
    if(IS_DEVELOPER_MODE && modsConfig[currentlySelectedMod].customModName) {
      modObjectName = modsConfig[currentlySelectedMod].customModName;
    }

    //Skip below if either the code didn't load, or it has the wrong variable
    if(!window[modObjectName]) {
      (0,eval)(this.responseText);
      throw new Error(`We were expecting to find a global variable called window.${modObjectName} but this is missing. Running snake without the mod.`);
    }

    //Mod specific code to run before running google snake script
    if(window[modObjectName].runCodeBefore) window[modObjectName].runCodeBefore();

    let newSnakeCode;
    //Alter google snake code and then run it
    if(window[modObjectName].alterSnakeCode) {
      try {
        newSnakeCode = window[modObjectName].alterSnakeCode(this.responseText);
      } catch (err) {
        let snakeErrEl = document.getElementById('snake-error-message')
        if(snakeErrEl) {snakeErrEl.style.display = 'block';}
        window.showSnakeErrMessage = true;
        console.log('Displaying message to user and then running the google snake script and then throwing the original error that occurred in "alterSnakeCode"');
        (0,eval)(this.responseText);
        throw err;
      }
    }

    (0,eval)(newSnakeCode);

    //Mod specific code to run after running google snake script
    if(window[modObjectName].runCodeAfter) window[modObjectName].runCodeAfter();
  
  };

  req.send();

  return returnVal;
}

//Setup Modal box that lets the user choose which mod to run
let addModSelectorPopup = function() {
  if(document.body.querySelector('img[src^="//www.google.com/logos/fnbx/snake_arcade"]') === null) {
    //We aren't on a page with google snake. Don't show the mod selector dialogue. Exit early.
    return;
  }

  const modCornerIndicatorHTML = `
      Current mod: <span id="mod-name-span" style="background-color: #eeeaca;padding: 2px;border-radius: 3px;font-family: consolas, monospace;"></span>
      <div id="change-mod-button" style="text-align: center;font-size: 0.84em;font-family: arial, sans-serif;color: #069;text-decoration: underline;cursor: pointer;margin-top: 3px;">Change mod</div>
      <div id="snake-error-message" style="font-family: helvetica, sans-serif;color: #f44336;margin-top: 2px;display: ${window.showSnakeErrMessage ? 'block' : 'none'};">
        Error changing snake code.
        <br>
        <a href="https://github.com/DarkSnakeGang/GoogleSnakeModLoader/blob/main/docs/mod_errors.md" target="_blank">Why does this happen?</a>
      </div>
  `;

  let modIndicatorEl = document.createElement('div');
  modIndicatorEl.id = 'mod-indicator';
  modIndicatorEl.style = 'z-index: 9999999;background-color: #fffce0;position: fixed;bottom: 0;right: 0;border-top: 1px solid #cccccc;border-left: 1px solid #cccccc;font-size: 1.2em;border-top-left-radius: 5px;padding: 5px;-webkit-box-shadow: 0px 0px 7px 1px hwb(0deg 0% 100% / 12%);box-shadow: 0px 0px 7px 1px rgb(0 0 0 / 12%);font-family: helvetica, sans-serif;height: initial;user-select:none;';
  modIndicatorEl.innerHTML = modCornerIndicatorHTML;
  document.body.appendChild(modIndicatorEl);

  document.getElementById('change-mod-button').addEventListener('click', ()=>{
    document.getElementById('mod-selector-dialogue-container').style.display = document.getElementById('mod-selector-dialogue-container').style.display === 'none' ? 'block' : 'none';
  });

  let modSelectorRadioOptions = '';
  for(const [key, value] of Object.entries(modsConfig)) {
    modSelectorRadioOptions += `<label><input type="radio" name="mod-selector" value="${key}">${value.displayName}</label><br>`;
  }
  modSelectorRadioOptions += `<label><input type="radio" name="mod-selector" value="none">None</label>`;

  let customUrlOptions = '';
  if(IS_DEVELOPER_MODE) {
    customUrlOptions = `<label><input id="custom-mod-name" type="text"> Custom Mod Name</label><br>
                        <label><input id="custom-url" type="text"> Custom Mod Url</label><br>`;
  }

  const modSelectorModal = `
  <div id="mod-selector-dialogue" style="display: block;margin:40px auto;padding:10px;border: 1px solid rgb(204 204 204);width:316.4px;background-color: #fffce0;border-radius:5px;-webkit-box-shadow: 0px 0px 10px 1px rgba(0,0,0,0.24);box-shadow: 0px 0px 10px 1px rgb(0 0 0 / 20%);font-family: helvetica, sans-serif;">
  <!-- <h1 style="font-size: 2em;font-weight: bold;font-family: &quot;Century Gothic&quot;, sans-serif;margin: 7px 0px 15px 0px;text-align: center;text-shadow: 2px 2px 3px #a2a2a2;">Snake Mod Loader</h1> -->
  <!-- <h1 style="font-size: 2em;font-weight: bold;font-family: &quot;Century Gothic&quot;, sans-serif;margin: 7px 0px 15px 0px;text-align: center;text-shadow: 1px 1px 1px #000000, 1px 1px 1px #000000, 1px 1px 5px #000000;color: #4674e9;">Snake Mod Loader</h1> -->
  <!-- <div style="background-color: #aad751;height: 17px;position: relative;top: -31px;transform: skewX(-21deg);z-index: 5;"></div> -->
  <h1 style="font-size: 2em;font-weight: bold;font-family: &quot;Century Gothic&quot;, sans-serif;margin: 7px 0px 15px 0px;text-align: center;text-shadow: 1px 1px 1px #000000, 1px 1px 1px #000000, 1px 1px 5px #000000;color: #4674e9;border: 5px inset #e4e0be;background-color: #ece9d4;">Snake Mod Loader</h1>
    ${modSelectorRadioOptions}
    <div id="advanced-options" style="display:none">
      <hr>
      <label><input id="fbx-centered-checkbox" type="checkbox">Make fbx centered</label><br>
      <label><input id="timer-starts-on" type="checkbox">Timer starts on</label><br>
      <label><input id="background-color-picker" type="color" value="#FFFFFF"> Background color on fbx</label><br>
      <label><input id="use-custom-theme" type="checkbox">Use custom theme</label><br>
      <div id="custom-theme-pickers">
        <label><input id="custom-theme-col1" type="color" value="#aad751"> Light Tiles</label><br>
        <label><input id="custom-theme-col2" type="color" value="#a2d149"> Dark Tiles</label><br>
        <label><input id="custom-theme-col3" type="color" value="#94bd46"> Shadow</label><br>
        <label><input id="custom-theme-col4" type="color" value="#578a34"> Border</label><br>
        <label><input id="custom-theme-col5" type="color" value="#38630d"> Lock Sign</label><br>
        <label><input id="custom-theme-col6" type="color" value="#4a752c"> Top Bar</label><br>
        <label><input id="custom-theme-col7" type="color" value="#4dc1f9"> Endscreen background</label><br>
      </div>
      ${customUrlOptions}
    </div>
    <br>
    <div style="display:inline-block;padding-top: 15px;margin-bottom: 4px;text-align: center;font-family: arial, sans-serif;color: #069;text-decoration: underline;cursor: pointer;user-select:none" id="advanced-options-toggle">Advanced options</div>
    <div id="apply-mod" class="mod-sel-btn" style="display:inline-block;background-color: hsl(24deg 64% 97%);padding: 4px;margin-top: 7px;border-radius: 3px;border: 2px solid #4caf50;color: #4caf50;font-weight: bold;user-select: none;float:right;cursor: pointer;">Apply</div>
    <div id="close-mod-selector" class="mod-sel-btn" style="display:inline-block;background-color: hsl(24deg 64% 97%);padding: 4px;margin-top: 7px;margin-right:10px;border-radius: 3px;border: 2px solid #606060;color: #606060;font-weight: bold;user-select: none;cursor: pointer;float:right">Close</div>
  </div>
  `;

  //Insert html for custom preset export dialogue box.
  let modSelectorModalContainer = document.createElement('div');
  modSelectorModalContainer.innerHTML = modSelectorModal;
  modSelectorModalContainer.id = 'mod-selector-dialogue-container';
  modSelectorModalContainer.style = 'display:none; position:fixed; width:100%; height:100%; z-index: 999999; left:0; top:0';
  document.body.appendChild(modSelectorModalContainer);

  //Tick the currently selected mod choice according to localStorage. Also, set the mod name in the indicator
  const currentlySelectedMod = localStorage.getItem('snakeChosenMod');
  let newlySelectedMod = currentlySelectedMod;

  if(modsConfig.hasOwnProperty(currentlySelectedMod) && currentlySelectedMod !== null && currentlySelectedMod !== 'none') {
    document.querySelector(`input[name="mod-selector"][value="${currentlySelectedMod}"]`).checked = true;
    document.getElementById('mod-name-span').textContent = modsConfig[currentlySelectedMod].displayName;
  } else {
    document.querySelector('input[name="mod-selector"][value="none"]').checked = true;
    document.getElementById('mod-name-span').textContent = 'None';
  }

  //save the mod selected when clicking on any of the radio buttons
  [...document.querySelectorAll('input[type="radio"][name="mod-selector"]')].forEach(radioEl=>{
    radioEl.addEventListener('click', function(){
      newlySelectedMod = this.value;
    });
  });

  //Load advanced settings
  let advancedSettings = JSON.parse(localStorage.getItem('snakeAdvancedSettings')) ?? {};
  let advancedSettingsOriginal = {...advancedSettings};//Shallow copy, but it's ok as nothing is nested.

  //Make sure the inputs are set the right values when starting up the mod
  updateAdvancedSettingInputs();

  //Event listeners for advanced settings
  document.getElementById('advanced-options-toggle').addEventListener('click', (event)=>{
    document.getElementById('advanced-options').style.display = (document.getElementById('advanced-options').style.display === 'none' ? 'block':'none');
    event.preventDefault();
  });
  document.getElementById('use-custom-theme').addEventListener('change', function() {
    document.getElementById('custom-theme-pickers').style.display = (this.checked ? 'block' : 'none');
    updateAdvancedSetting('useCustomTheme', this.checked);
  });
  document.getElementById('fbx-centered-checkbox').addEventListener('change', function() {
    updateAdvancedSetting('fbxCentered', this.checked);
  });
  document.getElementById('timer-starts-on').addEventListener('change', function() {
    updateAdvancedSetting('timerStartsOn', this.checked);
  });
  document.getElementById('background-color-picker').addEventListener('input', function() {
    updateAdvancedSetting('backgroundColor', this.value);
  });
  document.getElementById('custom-theme-col1').addEventListener('input', function() {
    updateAdvancedSetting('themeCol1', this.value);
  });
  document.getElementById('custom-theme-col2').addEventListener('input', function() {
    updateAdvancedSetting('themeCol2', this.value);
  });
  document.getElementById('custom-theme-col3').addEventListener('input', function() {
    updateAdvancedSetting('themeCol3', this.value);
  });
  document.getElementById('custom-theme-col4').addEventListener('input', function() {
    updateAdvancedSetting('themeCol4', this.value);
  });
  document.getElementById('custom-theme-col5').addEventListener('input', function() {
    updateAdvancedSetting('themeCol5', this.value);
  });
  document.getElementById('custom-theme-col6').addEventListener('input', function() {
    updateAdvancedSetting('themeCol6', this.value);
  });
  document.getElementById('custom-theme-col7').addEventListener('input', function() {
    updateAdvancedSetting('themeCol7', this.value);
  });

  if(IS_DEVELOPER_MODE) {
    document.getElementById('custom-mod-name').addEventListener('input', function() {
      updateAdvancedSetting('customModName', this.value);
    });
    document.getElementById('custom-url').addEventListener('input', function() {
      updateAdvancedSetting('customUrl', this.value);
    });
  }

  //Hide mod selector dialogue when clicking close button
  document.getElementById('close-mod-selector').addEventListener('click', function() {
    document.getElementById('mod-selector-dialogue-container').style.display = 'none';
  });

  //Apply button should save settings and refresh page
  document.getElementById('apply-mod').addEventListener('click', function(event) {
    //Figure out if advanced settings have been changed.
    let shallowEquality = true;
    for(let setting in advancedSettings) {
      if(advancedSettings[setting] !== advancedSettingsOriginal?.[setting]) {
        shallowEquality = false;
        break;
      }
    }

    //Skip if settings/mod chosen are the same as before.
    if(shallowEquality && newlySelectedMod === currentlySelectedMod) {
      alert('Settings are the same as before!')
      return;
    }

    //Save new settings and refresh to run with the new settings
    localStorage.setItem('snakeChosenMod', newlySelectedMod);
    localStorage.setItem('snakeAdvancedSettings',JSON.stringify(advancedSettings));
    location.reload();
  });

  //Set up CSS
  const css = `
    .mod-sel-btn:hover {
      background-color: #fefcfb !important;
    }

    .mod-sel-btn:active {
        background-color: #f0e9e5 !important;
    }
  `;
  document.getElementsByTagName('style')[0].innerHTML = document.getElementsByTagName('style')[0].innerHTML + css;

  let attemptsApplyingAdvancedSettings = 0;
  setTimeout(applyAdvancedSettingsToGame, 300);//Small delay to give the game more time to load.

  function updateAdvancedSettingInputs() {
    if(advancedSettings.hasOwnProperty('fbxCentered')) {
      document.getElementById('fbx-centered-checkbox').checked = advancedSettings.fbxCentered;
    }
    if(advancedSettings.hasOwnProperty('timerStartsOn')) {
      document.getElementById('timer-starts-on').checked = advancedSettings.timerStartsOn;
    }
    if(advancedSettings.hasOwnProperty('useCustomTheme')) {
      document.getElementById('use-custom-theme').checked = advancedSettings.useCustomTheme;
      document.getElementById('custom-theme-pickers').style.display = (advancedSettings.useCustomTheme ? 'block' : 'none');
    } else {
      document.getElementById('custom-theme-pickers').style.display = 'none';
    }
    if(advancedSettings.hasOwnProperty('backgroundColor')) {
      document.getElementById('background-color-picker').value = advancedSettings.backgroundColor;
    }
    if(advancedSettings.hasOwnProperty('themeCol1')) {
      document.getElementById('custom-theme-col1').value = advancedSettings.themeCol1;
    }
    if(advancedSettings.hasOwnProperty('themeCol2')) {
      document.getElementById('custom-theme-col2').value = advancedSettings.themeCol2;
    }
    if(advancedSettings.hasOwnProperty('themeCol3')) {
      document.getElementById('custom-theme-col3').value = advancedSettings.themeCol3;
    }
    if(advancedSettings.hasOwnProperty('themeCol4')) {
      document.getElementById('custom-theme-col4').value = advancedSettings.themeCol4;
    }
    if(advancedSettings.hasOwnProperty('themeCol5')) {
      document.getElementById('custom-theme-col5').value = advancedSettings.themeCol5;
    }
    if(advancedSettings.hasOwnProperty('themeCol6')) {
      document.getElementById('custom-theme-col6').value = advancedSettings.themeCol6;
    }
    if(advancedSettings.hasOwnProperty('themeCol7')) {
      document.getElementById('custom-theme-col7').value = advancedSettings.themeCol7;
    }

    if(IS_DEVELOPER_MODE) {
      if(advancedSettings.hasOwnProperty('customModName')) {
        document.getElementById('custom-mod-name').value = advancedSettings.customModName;
      }
      if(advancedSettings.hasOwnProperty('customUrl')) {
        document.getElementById('custom-url').value = advancedSettings.customUrl;
      }
    }
  }

  function updateAdvancedSetting(settingName, settingValue) {
    advancedSettings[settingName] = settingValue;
  }

  function applyAdvancedSettingsToGame() {
    if(attemptsApplyingAdvancedSettings > 10) {
      //Stop trying to apply advanced setting if we've tried this much and the game still isn't ready
      console.log('window.snake is still not available after retrying many times. Skipping applying advanced settings');
    } else if(!window.snake) {
      //Game not ready. Wait a bit and then try again.
      console.log('window.snake not ready for when we apply advanced settings. Will retry again after waiting.');
      attemptsApplyingAdvancedSettings++;
      setTimeout(applyAdvancedSettingsToGame, 300);
    } else {
      if(advancedSettings.fbxCentered && window.location.href.includes('fbx?fbx=snake_arcade')) {
        //Copied from GoogleSnakeCenteredFBX mod
        document.getElementsByTagName('div')[0].style = 'position:relative;top:50%;transform:translate(0%,-50%);margin:auto;';
      }
      if(advancedSettings.timerStartsOn) {
        window.snake.speedrun();
      }
      if(advancedSettings.useCustomTheme) {
        window.snake.setCustomTheme(
          advancedSettings.themeCol1 ?? '#aad751',
          advancedSettings.themeCol2 ?? '#a2d149',
          advancedSettings.themeCol3 ?? '#94bd46',
          advancedSettings.themeCol4 ?? '#578a34',
          advancedSettings.themeCol5 ?? '#38630d',
          advancedSettings.themeCol6 ?? '#4a752c',
          advancedSettings.themeCol7 ?? '#4dc1f9'
        );
      }
      if(window.location.href.includes('fbx?fbx=snake_arcade')) {
        document.body.style.backgroundColor = advancedSettings.backgroundColor;
      }
    }
  }
}

window.testMod = {};
window.testMod.runCodeBefore = function() {
  
}
window.testMod.alterSnakeCode = function(code) {
  code = code.replaceAll('.66','.36')

  return code;
}
window.testMod.runCodeAfter = function() {

}

function loadAndRunCodeSynchronous(url) {
  let req = new XMLHttpRequest();
  req.open('GET', url, false);
  req.onload = function() {
    if(this.status === 200) {
      (1,eval)(this.responseText);
    } else {
      console.log(`Loading selected mod returned non-200 status. Received: ${this.status}`);
    }
  };
  req.error = function(event) {
    console.error(`Error when attempting to retrieve mod code from ${url}`);
    console.log(event);
  };
  req.send();
}

window.addEventListener('load', addModSelectorPopup);

////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Utility functions below
////////////////////////////////////////////////////////////////////////////////////////////////////////////

window.swapInMainClassPrototype = function(mainClass, functionText) {
  if(/^[$a-zA-Z0-9_]{0,6}=function/.test(functionText)) {
    throw new Error("Error, function is of form abc=function(), but this only works for stuff like s_.abc=function()");
  }
  functionText = assertReplace(functionText, /^[$a-zA-Z0-9_]{0,6}/,`${mainClass}.prototype`);
  return functionText;
}

/*
This function will search for a function/method in some code and return this function as a string

code will usually be the snake source code

functionSignature will be regex matching the beginning of the function/method (must end in $),
for example if we are trying to find a function like s_xD = function(a, b, c, d, e) {......}
then put functionSignature = /[$a-zA-Z0-9_]{0,6}=function\(a,b,c,d,e\)$/

somethingInsideFunction will be regex matching something in the function
for example if we are trying to find a function like s_xD = function(a, b, c, d, e) {...a.Xa&&10!==a.Qb...}
then put somethingInsideFunction = /a\.[$a-zA-Z0-9_]{0,6}&&10!==a\.[$a-zA-Z0-9_]{0,6}/
*/
window.findFunctionInCode = function(code, functionSignature, somethingInsideFunction, logging = false) {
  let functionSignatureSource = functionSignature.source;
  let functionSignatureFlags = functionSignature.flags;//Probably empty string

  /*Check functionSignature ends in $*/
  if (functionSignatureSource[functionSignatureSource.length - 1] !== "$") {
    throw new Error("functionSignature regex should end in $");
  }

  /*Allow line breaks after commas or =. This is bit sketchy, but should be ok as findFunctionInCode is used in a quite limited way*/
  functionSignatureSource.replaceAll(/,|=/g,'$&\\n?');
  functionSignature = new RegExp(functionSignatureSource, functionSignatureFlags);

  /*get the position of somethingInsideFunction*/
  let indexWithinFunction = code.search(somethingInsideFunction);
  if (indexWithinFunction == -1) {
    console.log("%cCouldn't find a match for somethingInsideFunction", "color:red;");
    diagnoseRegexError(code, somethingInsideFunction);
  }

  /*expand outwards from somethingInsideFunction until we get to the function signature, then count brackets
  to find the end of the function*/
  let startIndex = 0;
  for (let i = indexWithinFunction; i >= 0; i--) {
    let startOfCode = code.substring(0, i);
    startIndex = startOfCode.search(functionSignature);
    if (startIndex !== -1) {
      break;
    }
    if (i == 0) {
      throw new Error("Couldn't find function signature");
    }
  }

  let bracketCount = 0;
  let foundFirstBracket = false;
  let endIndex = 0;
  /*Use bracket counting to find the whole function*/
  let codeLength = code.length;
  for (let i = startIndex; i <= codeLength; i++) {
    if (!foundFirstBracket && code[i] == "{") {
      foundFirstBracket = true;
    }

    if (code[i] == "{") {
      bracketCount++;
    }
    if (code[i] == "}") {
      bracketCount--;
    }
    if (foundFirstBracket && bracketCount == 0) {
      endIndex = i;
      break;
    }

    if (i == codeLength) {
      throw new Error("Couldn't pair up brackets");
    }
  }

  let fullFunction = code.substring(startIndex, endIndex + 1);

  /*throw error if fullFunction doesn't contain something inside function - i.e. function signature was wrong*/
  if (fullFunction.search(somethingInsideFunction) === -1) {
    throw new Error("Function signature does not belong to the same function as somethingInsideFunction");
  }

  if (logging) {
    console.log(fullFunction);
  }

  return fullFunction;
}

/*
Same as replace, but throws an error if nothing is changed
*/
window.assertReplace = function(baseText, regex, replacement) {
  if (typeof baseText !== 'string') {
    throw new Error('String argument expected for assertReplace');
  }
  let outputText = baseText.replace(regex, replacement);

  //Throw warning if nothing is replaced
  if (baseText === outputText) {
    diagnoseRegexError(baseText, regex);
  }

  return outputText;
}

/*
Same as replaceAll, but throws an error if nothing is changed
*/
window.assertReplaceAll = function(baseText, regex, replacement) {
  if (typeof baseText !== 'string') {
    throw new Error('String argument expected for assertReplace');
  }
  let outputText = baseText.replaceAll(regex, replacement);

  //Throw warning if nothing is replaced
  if (baseText === outputText) {
    diagnoseRegexError(baseText, regex);
  }

  return outputText;
}

//Alternate way to use assertReplace. Example: code = code.assertReplace('Thing to change', 'New thing');
String.prototype.assertReplace = function(regex, replacement) {
  return assertReplace(this.toString(), regex, replacement);
};

//Same as above for assertReplaceAll.
String.prototype.assertReplaceAll = function(regex, replacement) {
  return assertReplaceAll(this.toString(), regex, replacement);
};

window.diagnoseRegexError = function(baseText, regex) {  
  if(!(regex instanceof RegExp)) {
    throw new Error('Failed to find match using string argument. No more details available');
  }

  //see if removing line breaks works - in that case we can give a more useful error message
  let oneLineText = baseText.replaceAll(/\n/g,'');
  let res = regex.test(oneLineText);

  //If line breaks don't solve the issue then throw a general error
  if (!res) {
    throw new Error('Failed to find match for regex.');
  }

  //Try to suggest correct regex to use for searching
  let regexSource = regex.source;
  let regexFlags = regex.flags;

  //Look at all the spots where line breaks might occur and try adding \n? there to see if it makes a difference
  //It might be easier to just crudely brute force putting \n? at each possible index?
  for(let breakableChar of ["%","&","\\*","\\+",",","-","\\/",":",";","<","=",">","\\?","{","\\|","}"]) {
    for(let pos = regexSource.indexOf(breakableChar); pos !== -1; pos = regexSource.indexOf(breakableChar, pos + 1)) {
      //Remake the regex with a new line at the candidate position
      let candidateRegexSource = `${regexSource.slice(0,pos + breakableChar.length)}\\n?${regexSource.slice(pos + breakableChar.length)}`;
      let candidateRegex;
      
      try{
        candidateRegex = new RegExp(candidateRegexSource, regexFlags);
      } catch(err) {
        continue;
      }

      //See if the new regex works
      let testReplaceResult = candidateRegex.test(baseText);
      if(testReplaceResult) {
        //Success we found the working regex! Give descriptive error message to user and log suggested regex with new line in correct place
        console.log(`Suggested regex improvement:
${candidateRegex}`);
        throw new Error('Suggested improvement found! Error with line break, failed to find match for regex. See logged output for regex to use instead that should hopefully fix this.');
      }
    }
  }

  throw new Error('Line break error! Failed to failed to find match for regex - most likely caused by a new line break. No suggestions provided');
}

window.appendCodeWithinSnakeModule = function(snakeCode, codeToAdd, addSemicolonAfter) {
  if(addSemicolonAfter) {
    codeToAdd += ';';
  }
  var newSnakeCode = snakeCode.replace(/}\)\(this\._s\);\n\/\/ Google Inc\./, codeToAdd + '$&');
  return newSnakeCode;
}

//Turns _.abc into _s.abc
window.swapInSnakeGlobal = function(text) {
  return assertReplace(text, /^_\./, '_s.');
}