'use strict';

ChromeUtils.defineESModuleGetters(this, {
  xPref: 'chrome://userchromejs/content/xPref.sys.mjs',
  Management: 'resource://gre/modules/Extension.sys.mjs',
  AppConstants: 'resource://gre/modules/AppConstants.sys.mjs',
});

let UC = {
  webExts: new Map(),
  sidebar: new Map(),
  sandboxes: new WeakMap()
};

let _uc = {
  ALWAYSEXECUTE: 'rebuild_userChrome.uc.js',
  BROWSERCHROME: AppConstants.MOZ_APP_NAME == 'thunderbird' ? 'chrome://messenger/content/messenger.xhtml' : 'chrome://browser/content/browser.xhtml',
  BROWSERTYPE: AppConstants.MOZ_APP_NAME == 'thunderbird' ? 'mail:3pane' : 'navigator:browser',
  BROWSERNAME: AppConstants.MOZ_APP_NAME.charAt(0).toUpperCase() + AppConstants.MOZ_APP_NAME.slice(1),
  PREF_ENABLED: 'userChromeJS.enabled',
  PREF_SCRIPTSDISABLED: 'userChromeJS.scriptsDisabled',

  chromedir: Services.dirsvc.get('UChrm', Ci.nsIFile),
  scriptsDir: '',

  sss: Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService),

  getScripts: function () {
    this.scripts = {};
    let files = this.chromedir.directoryEntries.QueryInterface(Ci.nsISimpleEnumerator);
    while (files.hasMoreElements()) {
      let file = files.getNext().QueryInterface(Ci.nsIFile);
      if (/\.uc\.js$/i.test(file.leafName)) {
        _uc.getScriptData(file);
      }
    }
  },

  getScriptData: function (aFile) {
    let aContent = this.readFile(aFile);
    let header = (aContent.match(/^\/\/ ==UserScript==\s*\n(?:.*\n)*?\/\/ ==\/UserScript==\s*\n/m) || [''])[0];
    let match, rex = {
      include: [],
      exclude: []
    };
    let findNextRe = /^\/\/ @(include|exclude)\s+(.+)\s*$/gm;
    while ((match = findNextRe.exec(header))) {
      rex[match[1]].push(match[2].replace(/^main$/i, _uc.BROWSERCHROME).replace(/\*/g, '.*?'));
    }
    if (!rex.include.length) {
      rex.include.push(_uc.BROWSERCHROME);
    }
    let exclude = rex.exclude.length ? '(?!' + rex.exclude.join('$|') + '$)' : '';

    let def = ['', ''];
    let author = (header.match(/\/\/ @author\s+(.+)\s*$/im) || def)[1];
    let filename = aFile.leafName || '';

    return this.scripts[filename] = {
      filename: filename,
      file: aFile,
      url: Services.io.getProtocolHandler('file').QueryInterface(Ci.nsIFileProtocolHandler).getURLSpecFromDir(this.chromedir) + filename,
      name: (header.match(/\/\/ @name\s+(.+)\s*$/im) || def)[1],
      description: (header.match(/\/\/ @description\s+(.+)\s*$/im) || def)[1],
      version: (header.match(/\/\/ @version\s+(.+)\s*$/im) || def)[1],
      author: (header.match(/\/\/ @author\s+(.+)\s*$/im) || def)[1],
      regex: new RegExp('^' + exclude + '(' + (rex.include.join('|') || '.*') + ')$', 'i'),
      id: (header.match(/\/\/ @id\s+(.+)\s*$/im) || ['', filename.split('.uc.js')[0] + '@' + (author || 'userChromeJS')])[1],
      homepageURL: (header.match(/\/\/ @homepageURL\s+(.+)\s*$/im) || def)[1],
      downloadURL: (header.match(/\/\/ @downloadURL\s+(.+)\s*$/im) || def)[1],
      updateURL: (header.match(/\/\/ @updateURL\s+(.+)\s*$/im) || def)[1],
      optionsURL: (header.match(/\/\/ @optionsURL\s+(.+)\s*$/im) || def)[1],
      startup: (header.match(/\/\/ @startup\s+(.+)\s*$/im) || def)[1],
      shutdown: (header.match(/\/\/ @shutdown\s+(.+)\s*$/im) || def)[1],
      onlyonce: /\/\/ @onlyonce\b/.test(header),
      isRunning: false,
      get isEnabled() {
        return (xPref.get(_uc.PREF_SCRIPTSDISABLED) || '').split(',').indexOf(this.filename) == -1;
      }
    }
  },

  readFile: function (aFile, metaOnly = false) {
    let stream = Cc['@mozilla.org/network/file-input-stream;1'].createInstance(Ci.nsIFileInputStream);
    stream.init(aFile, 0x01, 0, 0);
    let cvstream = Cc['@mozilla.org/intl/converter-input-stream;1'].createInstance(Ci.nsIConverterInputStream);
    cvstream.init(stream, 'UTF-8', 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
    let content = '',
        data = {};
    while (cvstream.readString(4096, data)) {
      content += data.value;
      if (metaOnly && content.indexOf('// ==/UserScript==') > 0) {
        break;
      }
    }
    cvstream.close();
    return content.replace(/\r\n?/g, '\n');
  },

  everLoaded: [],

  loadScript: function (script, win) {
    if (!script.regex.test(win.location.href) || (script.filename != this.ALWAYSEXECUTE && !script.isEnabled)) {
      return;
    }

    if (script.onlyonce && script.isRunning) {
      if (script.startup) {
        Cu.evalInSandbox(`(function(script, win){${script.startup}})`, this.getSandbox(win))(script, win);
      }
      return;
    }

    try {
      Services.scriptloader.loadSubScript(script.url + '?' + script.file.lastModifiedTime,
                                          script.onlyonce ? { window: win } : win);
      script.isRunning = true;
      if (script.startup) {
        Cu.evalInSandbox(`(function(script, win){${script.startup}})`, this.getSandbox(win))(script, win);
      }
      if (!script.shutdown) {
        this.everLoaded.push(script.id);
      }
    } catch (ex) {
      Cu.reportError(ex);
    }
  },

  getSandbox: function (doc) {
    if (!UC.sandboxes) UC.sandboxes = new WeakMap();
    let global = Cu.getGlobalForObject(doc);
    if (UC.sandboxes.has(global))
      return UC.sandboxes.get(global);
    let sb = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
      sandboxPrototype: global,
      sameZoneAs: global,
      wantXrays: false,
      sandboxName: 'UCJS:Sandbox'
    });
    UC.sandboxes.set(global, sb);
    global.addEventListener('unload', () => {
      UC.sandboxes.delete(global);
      Cu.nukeSandbox(sb);
    });
    return sb;
  },

  windows: function (fun, onlyBrowsers = true) {
    let windows = Services.wm.getEnumerator(onlyBrowsers ? this.BROWSERTYPE : null);
    while (windows.hasMoreElements()) {
      let win = windows.getNext();
      if (!win._uc)
        continue;
      if (!onlyBrowsers) {
        let frames = win.docShell.getAllDocShellsInSubtree(Ci.nsIDocShellTreeItem.typeAll, Ci.nsIDocShell.ENUMERATE_FORWARDS);
        let res = frames.some(frame => {
          let fWin = frame.domWindow;
          let {document, location} = fWin;
          if (fun(document, fWin, location))
            return true;
        });
        if (res)
          break;
      } else {
        let {document, location} = win;
        if (fun(document, win, location))
          break;
      }
    }
  },

  createElement: function (doc, tag, atts, XUL = true) {
    let el = XUL ? doc.createXULElement(tag) : doc.createElement(tag);
    for (let att in atts) {
      if (att.startsWith('on'))
        el.addEventListener(att.slice(2), typeof atts[att] == "string" ?
          Cu.evalInSandbox(`(function(event){${atts[att]}})`, this.getSandbox(doc)) : atts[att]);
      else
        el.setAttribute(att, atts[att]);
    }
    return el
  }
};

if (xPref.get(_uc.PREF_ENABLED) === undefined) {
  xPref.set(_uc.PREF_ENABLED, true, true);
}

if (xPref.get(_uc.PREF_SCRIPTSDISABLED) === undefined) {
  xPref.set(_uc.PREF_SCRIPTSDISABLED, '', true);
}

let UserChrome_js = {
  observe: function (aSubject) {
    if (
      AppConstants.MOZ_APP_NAME == "thunderbird" &&
      aSubject?.location?.href.startsWith("chrome://messenger/content")
    ) {
      aSubject.addEventListener("DOMContentLoaded", () => {
        this.load(aSubject);
      }, {once: true})
    } else {
      aSubject.addEventListener('DOMContentLoaded', this, {once: true});
    }
  },

  handleEvent: function (aEvent) {
    let document = aEvent.originalTarget;
    let window = document.defaultView;
    if (window.document.isInitialDocument) {
      this.load(window.parent);
    } else {
      this.load(window);
    }
  },

  load: function (window) {
    let location = window.location;

    if (!this.sharedWindowOpened && location.href == 'chrome://extensions/content/dummy.xhtml') {
      this.sharedWindowOpened = true;

      Management.on('extension-browser-inserted', function (topic, browser) {
        browser.messageManager.addMessageListener('Extension:BackgroundViewLoaded', this.messageListener.bind(this));
      }.bind(this));
    } else if (/^(chrome:(?!\/\/global\/content\/commonDialog\.x?html)|about:(?!blank))/i.test(location.href)) {
      window.UC = UC;
      window._uc = _uc;
      window.xPref = xPref;
      if (window._gBrowser) // bug 1443849
        window.gBrowser = window._gBrowser;

      if (xPref.get(_uc.PREF_ENABLED)) {
        Object.values(_uc.scripts).forEach(script => {
          _uc.loadScript(script, window);
        });
      } else if (!UC.rebuild) {
        _uc.loadScript(_uc.scripts[_uc.ALWAYSEXECUTE], window);
      }
    }
  },

  messageListener: function (msg) {
    const browser = msg.target;
    const { addonId } = browser._contentPrincipal;

    browser.messageManager.removeMessageListener('Extension:BackgroundViewLoaded', this.messageListener);

    if (browser.ownerGlobal.location.href == 'chrome://extensions/content/dummy.xhtml') {
      UC.webExts.set(addonId, browser);
      Services.obs.notifyObservers(null, 'UCJS:WebExtLoaded', addonId);
    } else {
      let win = browser.ownerGlobal.windowRoot.ownerGlobal;
      UC.sidebar.get(addonId)?.set(win, browser) || UC.sidebar.set(addonId, new Map([[win, browser]]));
      Services.obs.notifyObservers(win, 'UCJS:SidebarLoaded', addonId);
    }
  }
};

if (!Services.appinfo.inSafeMode) {
  _uc.chromedir.append(_uc.scriptsDir);
  _uc.getScripts();
  let windows = Services.wm.getEnumerator(null);
  while (windows.hasMoreElements()) {
    let win = windows.getNext();
    if (!('UC' in win))
      UserChrome_js.load(win)
  }
  Services.obs.addObserver(UserChrome_js, 'chrome-document-global-created', false);
}
