'use strict';

/* global require, __, Article, Headlines, Filters, fox */
/* global xhr, PluginHost, Notify, Feeds, Cookie */
/* global CommonDialogs, Plugins */

const App = {
   _initParams: [],
	_rpc_seq: 0,
	hotkey_prefix: 0,
	hotkey_prefix_pressed: false,
	hotkey_prefix_timeout: 0,
   global_unread: -1,
   _widescreen_mode: false,
   _loading_progress: 0,
   _night_mode_retry_timeout: false,
   hotkey_actions: {},
   is_prefs: false,
   LABEL_BASE_INDEX: -1024,
	UserAccessLevels: {
		ACCESS_LEVEL_READONLY: -1
	},
   _translations: {},
   Hash: {
      get: function() {
         return dojo.queryToObject(window.location.hash.substring(1));
      },
      set: function(params) {
         const obj = dojo.queryToObject(window.location.hash.substring(1));
         window.location.hash = dojo.objectToQuery({...obj, ...params});
      }
   },
   l10n: {
      ngettext: function(msg1, msg2, n) {
         return self.__((parseInt(n) > 1) ? msg2 : msg1);
      },
      __: function(msg) {
         return App._translations[msg] ? App._translations[msg] : msg;
      }
   },
   FormFields: {
      attributes_to_string: function(attributes) {
         return Object.keys(attributes).map((k) =>
            `${App.escapeHtml(k)}="${App.escapeHtml(attributes[k])}"`)
            .join(" ");
      },
      hidden_tag: function(name, value, attributes = {}, id = "") {
         return `<input id="${App.escapeHtml(id)}" dojoType="dijit.form.TextBox" ${this.attributes_to_string(attributes)}
            style="display : none" name="${name}" value="${App.escapeHtml(value)}"></input>`
      },
      // allow html inside because of icons
      button_tag: function(value, type, attributes = {}) {
         return `<button dojoType="dijit.form.Button" ${this.attributes_to_string(attributes)}
            type="${type}">${value}</button>`

      },
      icon: function(icon, attributes = {}) {
         return `<i class="material-icons" ${this.attributes_to_string(attributes)}>${icon}</i>`;
      },
      submit_tag: function(value, attributes = {}) {
         return this.button_tag(value, "submit", {...{class: "alt-primary"}, ...attributes});
      },
      cancel_dialog_tag: function(value, attributes = {}) {
         return this.button_tag(value, "", {...{onclick: "App.dialogOf(this).hide()"}, ...attributes});
      },
      checkbox_tag: function(name, checked = false, value = "", attributes = {}, id = "") {
         // checked !== '0' prevents mysql "boolean" false to be implicitly cast as true
         return `<input dojoType="dijit.form.CheckBox" type="checkbox" name="${App.escapeHtml(name)}"
                     ${checked !== '0' && checked ? "checked" : ""}
                     ${value ? `value="${App.escapeHtml(value)}"` : ""}
                     ${this.attributes_to_string(attributes)} id="${App.escapeHtml(id)}">`
      },
      select_tag: function(name, value, values = [], attributes = {}, id = "") {
         value = String(value);

         return `
            <select name="${name}" dojoType="fox.form.Select" id="${App.escapeHtml(id)}" ${this.attributes_to_string(attributes)}>
               ${values.map((v) => {
                  v = String(v);
                  return `<option ${v === value ? 'selected="selected"' : ''} value="${App.escapeHtml(v)}">${App.escapeHtml(v)}</option>`
               }).join("")}
            </select>
         `
      },
      select_hash: function(name, value, values = {}, attributes = {}, id = "", params = {}) {
			let keys = Object.keys(values);
			value = String(value);

			if (params.numeric_sort)
				keys = keys.sort((a,b) => a - b);

         return `
            <select name="${name}" dojoType="fox.form.Select" id="${App.escapeHtml(id)}" ${this.attributes_to_string(attributes)}>
               ${keys.map((vk) =>
                     `<option ${vk === value ? 'selected="selected"' : ''} value="${App.escapeHtml(vk)}">${App.escapeHtml(values[vk])}</option>`
               ).join("")}
            </select>
         `
      }
   },
   Scrollable: {
		scrollByPages: function (elem, page_offset) {
			if (!elem) return;

			/* keep a line or so from the previous page  */
			const offset = (elem.offsetHeight - (page_offset > 0 ? 50 : -50)) * page_offset;

			this.scroll(elem, offset);
		},
		scroll: function(elem, offset) {
			if (!elem) return;

			elem.scrollTop += offset;
		},
		isChildVisible: function(elem, ctr) {
			if (!elem) return;

			const ctop = ctr.scrollTop;
			const cbottom = ctop + ctr.offsetHeight;

			const etop = elem.offsetTop;
			const ebottom = etop + elem.offsetHeight;

			return etop >= ctop && ebottom <= cbottom ||
				etop < ctop && ebottom > ctop || ebottom > cbottom && etop < cbottom;
		},
		fitsInContainer: function (elem, ctr) {
			if (!elem) return;

			return elem.offsetTop + elem.offsetHeight <= ctr.scrollTop + ctr.offsetHeight &&
				elem.offsetTop >= ctr.scrollTop;
		},
      scrollTo: function (elem, ctr, params = {}) {
         const force_to_top = params.force_to_top || false;

         if (!elem || !ctr) return;

         if (force_to_top || !App.Scrollable.fitsInContainer(elem, ctr)) {
            ctr.scrollTop = elem.offsetTop;
         }
      }
   },
   /** @deprecated use document.getElementById */
   byId: function(id) {
      return document.getElementById(id);
   },
   /** @deprecated use document.querySelector */
   find: function(query) {
      return document.querySelector(query)
   },
   /** @deprecated use document.querySelectorAll */
   findAll: function(query) {
      return document.querySelectorAll(query);
   },
   dialogOf: function (elem) {

      // elem could be a Dijit widget
      elem = elem.domNode ? elem.domNode : elem;

      return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
   },
   getPhArgs(plugin, method, args = {}) {
      return {...{op: "PluginHandler", plugin: plugin, method: method}, ...args};
   },
   label_to_feed_id: function(label) {
      return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
   },
   feed_to_label_id: function(feed) {
      return this.LABEL_BASE_INDEX - 1 + Math.abs(feed);
   },
   getInitParam: function(k) {
		return this._initParams[k];
	},
	setInitParam: function(k, v) {
		this._initParams[k] = v;
	},
	nightModeChanged: function(is_night, link, retry = 0) {
		if (link) {
			if (retry < 15) {
				window.clearTimeout(this._night_mode_retry_timeout);

				this._night_mode_retry_timeout = window.setTimeout(
					() => this.nightModeChanged(is_night, link, ++retry),
					3000);
			}

			xhr.post("backend.php", {op: "RPC", method: "getRuntimeInfo"}, () => {
				const css_override = is_night ? App.getInitParam("default_dark_theme") : App.getInitParam("default_light_theme");

				link.setAttribute("href", css_override + "?" + Date.now());

				window.clearTimeout(this._night_mode_retry_timeout);
			});
		}
	},
	setupNightModeDetection: function(callback) {
		if (!document.getElementById("theme_css")) {
			const mql = window.matchMedia('(prefers-color-scheme: dark)');

			try {
				mql.addEventListener("change", () => {
					this.nightModeChanged(mql.matches, document.getElementById("theme_auto_css"));
				});
			} catch {
				console.warn("exception while trying to set MQL event listener");
			}

			const link = document.createElement("link");
         link.rel = "stylesheet";
         link.id = "theme_auto_css";

			if (callback) {
						link.onload = function() {
							document.body.classList.remove('css_loading');
							callback();
						};

						link.onerror = function() {
							alert("Fatal error while loading application stylesheet: " + link.getAttribute("href"));
						}
					}

			this.nightModeChanged(mql.matches, link);

			document.head.prepend(link);
		} else {
			document.body.classList.remove('css_loading');

			if (callback) callback();
		}
	},
   postCurrentWindow: function(target, params) {
      const form = document.createElement("form");

      form.setAttribute("method", "post");
      form.setAttribute("action", App.getInitParam("self_url_prefix") + "/" + target);

      for (const [k,v] of Object.entries(params)) {
         const field = document.createElement("input");

         field.setAttribute("name", k);
         field.setAttribute("value", v);
         field.setAttribute("type", "hidden");

         form.appendChild(field);
      }

      document.body.appendChild(form);

      form.submit();

      form.parentNode.removeChild(form);
   },
   postOpenWindow: function(target, params) {
      const w = window.open("");

		if (w) {
			w.opener = null;

			const form = document.createElement("form");

			form.setAttribute("method", "post");
			form.setAttribute("action", App.getInitParam("self_url_prefix") + "/" + target);

			for (const [k,v] of Object.entries(params)) {
				const field = document.createElement("input");

				field.setAttribute("name", k);
				field.setAttribute("value", v);
				field.setAttribute("type", "hidden");

				form.appendChild(field);
			}

			w.document.body.appendChild(form);
			form.submit();
		}

   },
	urlParam: function(name) {
		try {
         const results = new RegExp('[?&]' + name + '=([^&#]*)').exec(window.location.href);
         return decodeURIComponent(results[1].replace(/\+/g, " ")) || 0;
      } catch {
         return 0;
      }
	},
	next_seq: function() {
		this._rpc_seq += 1;
		return this._rpc_seq;
	},
	get_seq: function() {
		return this._rpc_seq;
	},
	setLoadingProgress: function(p) {
		this._loading_progress += p;

		if (dijit.byId("loading_bar"))
			dijit.byId("loading_bar").update({progress: this._loading_progress});

		if (this._loading_progress >= 90) {
			document.getElementById("overlay").hide();
		}

	},
	isCombinedMode: function() {
		return !!this.getInitParam("combined_display_mode");
	},
	setCombinedMode: function(combined) {
		const value = combined ? "true" : "false";

		xhr.post("backend.php", {op: "RPC", method: "setpref", key: "COMBINED_DISPLAY_MODE", value: value}, () => {
			this.setInitParam("combined_display_mode",
				!this.getInitParam("combined_display_mode"));

			Article.close();
			Headlines.renderAgain();
		})
	},
	isExpandedMode: function() {
		return !!this.getInitParam("cdm_expanded");
	},
	setExpandedMode: function(expand) {
		if (App.isCombinedMode()) {
			const value = expand ? "true" : "false";

			xhr.post("backend.php", {op: "RPC", method: "setpref", key: "CDM_EXPANDED", value: value}, () => {
				this.setInitParam("cdm_expanded", !this.getInitParam("cdm_expanded"));
				Headlines.renderAgain();
			});
		} else {
			alert(__("This function is only available in combined mode."));
		}
	},
	getActionByHotkeySequence: function(sequence) {
		const hotkeys_map = this.getInitParam("hotkeys");

		for (const seq in hotkeys_map[1]) {
			if (Object.prototype.hasOwnProperty.call(hotkeys_map[1], seq)) {
				if (seq === sequence) {
					return hotkeys_map[1][seq];
				}
			}
		}
	},
	keyeventToAction: function(event) {

		const hotkeys_map = this.getInitParam("hotkeys");
		const keycode = event.which;
		const keychar = String.fromCharCode(keycode);

		if (keycode === 27) { // escape and drop prefix
			this.hotkey_prefix = false;
		}

		if (!this.hotkey_prefix && hotkeys_map[0].indexOf(keychar) !== -1) {

			this.hotkey_prefix = keychar;
			document.getElementById("cmdline").innerHTML = keychar;
			Element.show("cmdline");

			window.clearTimeout(this.hotkey_prefix_timeout);
			this.hotkey_prefix_timeout = window.setTimeout(() => {
				this.hotkey_prefix = false;
				Element.hide("cmdline");
			}, 3 * 1000);

			event.stopPropagation();

			return false;
		}

		Element.hide("cmdline");

		let hotkey_name = "";

		if (event.type === 'keydown') {
			hotkey_name = "(" + keycode + ")";

			// ensure ^*char notation
			if (event.shiftKey) hotkey_name = "*" + hotkey_name;
			if (event.ctrlKey) hotkey_name = "^" + hotkey_name;
			if (event.altKey) hotkey_name = "+" + hotkey_name;
			if (event.metaKey) hotkey_name = "%" + hotkey_name;
		} else {
			hotkey_name = keychar ? keychar : "(" + keycode + ")";
		}

		let hotkey_full = this.hotkey_prefix ? this.hotkey_prefix + " " + hotkey_name : hotkey_name;
		this.hotkey_prefix = false;

		let action_name = this.getActionByHotkeySequence(hotkey_full);

		// check for mode-specific hotkey
		if (!action_name) {
			hotkey_full = (this.isCombinedMode() ? "{C}" : "{3}") + hotkey_full;

			action_name = this.getActionByHotkeySequence(hotkey_full);
		}

		return action_name;
	},
	cleanupMemory: function(root) {
		const dijits = dojo.query("[widgetid]", dijit.byId(root).domNode).map(dijit.byNode);

		dijits.forEach(function (d) {
			dojo.destroy(d.domNode);
		});

		document.querySelectorAll(`#${root} *`).forEach(function (i) {
			i.parentNode ? i.parentNode.removeChild(i) : true;
		});
   },
   // htmlspecialchars()-alike for headlines data-content attribute
   escapeHtml: function(p) {
      if (typeof p !== 'string')
         return p;

      const map = {
         '&': '&amp;',
         '<': '&lt;',
         '>': '&gt;',
         '"': '&quot;',
         "'": '&#x27;',
         '/': '&#x2F;',
      };

      return p.replace(/[&<>"'/]/g, m => map[m]);
   },
   /**
    * Sanitize a URL for safe use in href attributes and window.open()
    * @param {string} url - URL to sanitize
    * @param {string} fallback - Optional fallback value if URL is invalid (default: empty string)
    * @return {string} Safe URL or fallback
    */
   sanitizeUrl: function(url, fallback = '') {
      if (!url || typeof url !== 'string') return fallback;

      // Remove NULL bytes and other control characters
      // eslint-disable-next-line no-control-regex
      const cleaned = url.replace(/[\x00-\x1F\x7F]/g, '');

      const trimmed = cleaned.trim();
      if (!trimmed) return fallback;

      return /^https?:\/\/.+/i.test(trimmed) ? trimmed : fallback;
   },
   openUrl: function(url) {
      const sanitized = this.sanitizeUrl(url);
      if (sanitized) {
         const w = window.open(sanitized);
         w.opener = null;
      }
   },
   unescapeHtml: function(p) {
      if (typeof p !== 'string' || p.indexOf('&') === -1)
         return p;

      return p.replace(/&(?:amp|lt|gt|quot|#x27|#x2F|#039|#47);/g, function(entity) {
         switch (entity) {
            case '&amp;': return '&';
            case '&lt;': return '<';
            case '&gt;': return '>';
            case '&quot;': return '"';
            case '&#x27;': case '&#039;': return "'";
            case '&#x2F;': case '&#47;': return '/';
            default: return entity;
         }
      });
   },
   getSelectedText: function() {
      const sel = window.getSelection();
      return sel ? sel.toString().trim() : "";
   },
   displayIfChecked: function(checkbox, elemId) {
      if (checkbox.checked) {
         Element.show(elemId);
      } else {
         Element.hide(elemId);
      }
   },
   hotkeyHelp: function() {
      xhr.post("backend.php", {op: "RPC", method: "hotkeyHelp"}, (reply) => {
         const dialog = new fox.SingleUseDialog({
            title: __("Keyboard shortcuts"),
            content: reply,
         });

         dialog.show();
      });
   },
	handleRpcJson: function(reply) {

		const netalert = document.querySelector('.net-alert');

      if (reply) {
         const error = reply['error'];
         const seq = reply['seq'];
         const message = reply['message'];
         const counters = reply['counters'];
         const runtime_info = reply['runtime-info'];

         if (error && error.code && error.code !== App.Error.E_SUCCESS) {
            console.warn("handleRpcJson: fatal error", error);
            this.Error.fatal(error.code, error.params);
            return false;
         }

         if (seq && this.get_seq() !== seq) {
            console.warn("handleRpcJson: sequence mismatch: ", seq, '!=', this.get_seq());
            return false;
         }

         // not in preferences
         if (typeof Feeds !== 'undefined') {
            if (message === 'UPDATE_COUNTERS') {
               Feeds.requestCounters(reply.feeds);
            }

            if (counters)
               Feeds.parseCounters(counters);
         }

         if (runtime_info)
            this.parseRuntimeInfo(runtime_info);

         if (netalert) netalert.hide();

         return true;
      } else {
         if (netalert) netalert.show();

         Notify.error("Communication problem with server.");

         return false;
		}
	},
	parseRuntimeInfo: function(data) {
		Object.keys(data).forEach((k) => {
         const v = data[k];

         if (k === "daemon_is_running" && v !== 1) {
            Notify.error("Update daemon is not running.", true);
            return;
         }

         if (k === "recent_log_events") {
            const alert = document.querySelector('.log-alert');

            if (alert) {
               v > 0 ? alert.show() : alert.hide();
            }
         }

         if (k === "daemon_stamp_ok" && v !== 1) {
            Notify.error("Update daemon is not updating feeds.", true);
            return;
         }

         if (typeof Feeds !== 'undefined') {
            if (k === "max_feed_id" || k === "num_feeds") {
               if (this.getInitParam(k) && this.getInitParam(k) !== v) {
                  Feeds.reload();
               }
            }
         }

         this.setInitParam(k, v);
		});

		PluginHost.run(PluginHost.HOOK_RUNTIME_INFO_LOADED, data);
	},
	backendSanityCallback: function(reply) {
		console.log("sanity check ok");

		const params = reply['init-params'];

		if (params) {

			Object.keys(params).forEach((k) => {
            switch (k) {
               case "label_base_index":
                  this.LABEL_BASE_INDEX = parseInt(params[k]);
                  break;
               case "cdm_auto_catchup":
                  {
                     const headlines = document.getElementById("headlines-frame");

                  // we could be in preferences
                     if (headlines)
                        headlines.setAttribute("data-auto-catchup", params[k] ? "true" : "false");
                  }
                  break;
               case "hotkeys":
                  // filter mnemonic definitions (used for help panel) from hotkeys map
                  // i.e. *(191)|Ctrl-/ -> *(191)
                  {
                     const tmp = [];

                     Object.keys(params[k][1]).forEach((sequence) => {
                        const filtered = sequence.replace(/\|.*$/, "");
                        tmp[filtered] = params[k][1][sequence];
                     });

                     params[k][1] = tmp;
                  }
                  break;
            }

            this.setInitParam(k, params[k]);
			});

			// PluginHost might not be available on non-index pages
			if (typeof PluginHost !== 'undefined')
				PluginHost.run(PluginHost.HOOK_PARAMS_LOADED, this._initParams);
		}

      const translations = reply['translations'];

      if (translations) {
         App._translations = translations;
      }

		this.initSecondStage();
	},
	Error: {
      E_SUCCESS: "E_SUCCESS",
      E_UNAUTHORIZED: "E_UNAUTHORIZED",
      E_SCHEMA_MISMATCH: "E_SCHEMA_MISMATCH",
      E_URL_SCHEME_MISMATCH: "E_URL_SCHEME_MISMATCH",
		fatal: function (error, params = {}) {
         if (error === App.Error.E_UNAUTHORIZED) {
            window.location.href = "index.php";
            return;
         } else if (error === App.Error.E_SCHEMA_MISMATCH) {
            window.location.href = "public.php?op=dbupdate";
            return;
         } else if (error === App.Error.E_URL_SCHEME_MISMATCH) {
            params.description = __("URL scheme reported by your browser (%a) doesn't match server-configured SELF_URL_PATH (%b), check X-Forwarded-Proto.")
               .replace("%a", params.client_scheme)
               .replace("%b", params.server_scheme);
            params.info = `SELF_URL_PATH: ${params.self_url_path}\nCLIENT_LOCATION: ${document.location.href}`
         }

			return this.report(error,
				{...{title: __("Fatal error")}, ...params});
		},
		report: function(error, params = {}) {
			if (!error) return;

			console.error("error.report:", error, params);

			const message = params.message ? params.message : error.toString();

			try {
				xhr.post("backend.php",
					{op: "RPC", method: "log",
						file: params.filename ? params.filename : error.fileName,
						line: params.lineno ? params.lineno : error.lineNumber,
						msg: message,
						context: error.stack});
			} catch (re) {
				console.error("[Error.report] exception while saving logging error on server", re);
			}

			try {
				const dialog = new fox.SingleUseDialog({
					title: params.title || __("Unhandled exception"),
					content: `
               <div class='exception-contents'>
                  <h3>${message}</h3>

                  ${params.description ? `<p>${params.description}</p>` : ''}

                  ${error.stack ?
                  `<header>${__('Stack trace')}</header>
                  <section>
                     <textarea readonly='readonly'>${error.stack}</textarea>
                  </section>` : ''}

                  ${params && params.info ?
                     `
                     <header>${__('Additional information')}</header>
                     <section>
                        <textarea readonly='readonly'>${params.info}</textarea>
                     </section>
                     ` : ''}
               </div>
               <footer class='text-center'>
                  <button dojoType="dijit.form.Button" class='alt-primary' type='submit'>
                     ${__('Close this window')}
                  </button>
               </footer>
            </div>`
				});

				dialog.show();
			} catch (de) {
				console.error("[Error.report] exception while showing error dialog", de);

				alert(error.stack ? error.stack : message);
			}

		},
		onWindowError: function (message, filename, lineno, colno, error) {
			// called without context (this) from window.onerror
			App.Error.report(error,
				{message: message, filename: filename, lineno: lineno, colno: colno});
		},
	},
	isPrefs() {
		return this.is_prefs;
   },
   audioCanPlay: function(ctype) {
      const a = document.createElement('audio');
      return a.canPlayType(ctype);
   },
	init: function(parser, is_prefs) {
		require(['dojo/aspect'], function(aspect) {
			aspect.before(dojo, 'xhrPost', function(args) {
				return [{...args, content: { ...args?.content, csrf_token: __csrf_token }}];
			});
		});

      this.is_prefs = is_prefs;
      window.onerror = this.Error.onWindowError;

      /* global __default_dark_theme, __default_light_theme */
      this.setInitParam("csrf_token", __csrf_token);
      this.setInitParam("default_light_theme", __default_light_theme);
      this.setInitParam("default_dark_theme", __default_dark_theme);

      this.setupNightModeDetection(() => {
         parser.parse();

         if (!this.checkBrowserFeatures())
            return;

         this.setLoadingProgress(30);
         this.initHotkeyActions();

         const params = {
            op: "RPC",
            method: "sanityCheck",
            clientTzOffset: new Date().getTimezoneOffset() * 60,
            hasSandbox: "sandbox" in document.createElement("iframe"),
            clientLocation: window.location.href
         };

         xhr.json("backend.php", params, (reply) => {
            try {
               this.backendSanityCallback(reply);
            } catch (e) {
               this.Error.report(e);
            }
         });
      });
   },
   checkBrowserFeatures: function() {
      let errorMsg = "";

      ['MutationObserver', 'requestIdleCallback'].forEach((t) => {
         if (!(t in window)) {
            errorMsg = `Browser check failed: <code>window.${t}</code> not found.`;
            throw new Error(errorMsg);
         }
      });

      if (typeof Promise.allSettled === "undefined") {
         errorMsg = `Browser check failed: <code>Promise.allSettled</code> is not defined.`;
         throw new Error(errorMsg);
      }

      return errorMsg === "";
   },
   updateRuntimeInfo: function() {
      xhr.json("backend.php", {op: "RPC", method: "getruntimeinfo"}, () => {
         // handled by xhr.json()
      });
   },
   initSecondStage: function() {

      document.onkeydown = (event) => this.hotkeyHandler(event);
      document.onkeypress = (event) => this.hotkeyHandler(event);

      if (this.is_prefs) {

         this.setLoadingProgress(70);
         Notify.close();

         let tab = this.urlParam('tab');

         if (tab) {
            tab = dijit.byId(tab + "Tab");
            if (tab) {
               dijit.byId("pref-tabs").selectChild(tab);

               const method = this.urlParam("method");

               if (method) {
                  switch (method) {
                     case "editfeed":
                        window.setTimeout(() => {
                           CommonDialogs.editFeed(this.urlParam('methodparam'))
                        }, 100);
                        break;
                     default:
                        console.warn("initSecondStage, unknown method:", method);
                  }
               }
            }
         } else {
            let tab = localStorage.getItem("ttrss:prefs-tab");

            if (tab) {
               tab = dijit.byId(tab);
               if (tab) {
                  dijit.byId("pref-tabs").selectChild(tab);
               }
            }
         }

         dojo.connect(dijit.byId("pref-tabs"), "selectChild", function (elem) {
            localStorage.setItem("ttrss:prefs-tab", elem.id);
            App.updateRuntimeInfo();
         });

      } else {

         Feeds.reload();
         Article.close();

         if (parseInt(Cookie.get("ttrss_fh_width")) > 0) {
            dijit.byId("feeds-holder").domNode.setStyle(
               {width: Cookie.get("ttrss_fh_width") + "px"});
         }

         dijit.byId("main").resize();

         dojo.connect(dijit.byId('feeds-holder'), 'resize',
            (args) => {
               if (args && args.w >= 0) {
                  Cookie.set("ttrss_fh_width", args.w, this.getInitParam("cookie_lifetime"));
               }
            });

         dojo.connect(dijit.byId('content-insert'), 'resize',
            (args) => {
               if (args && args.w >= 0 && args.h >= 0) {
						const cookie_suffix = this._widescreen_mode ? "wide" : "normal";

                  Cookie.set("ttrss_ci_width:" + cookie_suffix, args.w, this.getInitParam("cookie_lifetime"));
                  Cookie.set("ttrss_ci_height:" + cookie_suffix, args.h, this.getInitParam("cookie_lifetime"));
               }
            });

         dijit.byId('toolbar-main').setValues({
            view_mode: this.getInitParam("default_view_mode"),
            order_by: this.getInitParam("default_view_order_by")
         });

         this.setLoadingProgress(50);

         this._widescreen_mode = this.getInitParam("widescreen");
         this.setWideScreenMode(this.isWideScreenMode(), true);

         Headlines.initScrollHandler();

         if (this.getInitParam('check_for_updates')) {
			window.setTimeout(() => {
              this.checkForUpdates();
              window.setInterval(() => {
                 this.checkForUpdates();
              }, 3600 * 1000);
            }, 60 * 1000);
         }

         PluginHost.run(PluginHost.HOOK_INIT_COMPLETE, null);
      }

      if (!this.getInitParam("bw_limit"))
         window.setInterval(() => {
            App.updateRuntimeInfo();
         }, 60 * 1000)

		if (App.getInitParam("safe_mode") && this.isPrefs()) {
			CommonDialogs.safeModeWarning();
		}

      console.log("second stage ok");

   },
	checkForUpdates: function() {
		xhr.json("backend.php", {op: 'RPC', method: 'checkforupdates'})
			.then((reply) => {
				const ttrss_icon_a = document.getElementById('updates-available');
				const plugin_icon_a = document.getElementById('plugin-updates-available');

				if (reply.changeset.id) {
					ttrss_icon_a.href = reply.changeset.compare_url;
					ttrss_icon_a.show();
				} else {
					ttrss_icon_a.hide();
				}

				if (reply.plugins.length)
					plugin_icon_a.show()
				else
					plugin_icon_a.hide();
			});
	},
   updateTitle: function() {
      let tmp = "Tiny Tiny RSS";

      if (this.global_unread > 0) {
         tmp = "(" + this.global_unread + ") " + tmp;
      }

      document.title = tmp;
   },
	hotkeyHandler: function(event) {
		if (event.target.nodeName === 'TEXTAREA')
			return;

		if (event.target.nodeName === 'INPUT') {
			const type = (event.target.type || 'text').toLowerCase();
			const text_input_types = ['text', 'password', 'email', 'search', 'tel', 'url', 'number', 'date', 'datetime-local', 'month', 'time', 'week'];

			if (text_input_types.includes(type))
				return;
		}

		// Arrow buttons and escape are not reported via keypress, handle them via keydown.
		if (event.type === 'keydown'
			&& !['Escape', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'PageUp', 'PageDown', 'Insert', 'Delete'].includes(event.key))
			return;

		const action_name = this.keyeventToAction(event);

		if (action_name) {
			const action_func = this.hotkey_actions[action_name];

			if (typeof action_func === 'function') {
				action_func(event);
				event.stopPropagation();
				return false;
			}
		}
	},
	isWideScreenMode: function() {
		return !!this._widescreen_mode;
	},
   setWideScreenMode: function(wide, quiet = false) {

		if (this.isCombinedMode() && !quiet) {
			alert(__("Widescreen is not available in combined mode."));
			return;
		}

		this._widescreen_mode = wide;

      const article_id = Article.getActive();
      const headlines_frame = document.getElementById("headlines-frame");
      const content_insert = dijit.byId("content-insert");

      // TODO: setStyle stuff should probably be handled by CSS

      if (wide) {
         dijit.byId("headlines-wrap-inner").attr("design", 'sidebar');
         content_insert.attr("region", "trailing");

         content_insert.domNode.setStyle({width: '50%',
            height: 'auto',
            borderTopWidth: '0px' });

         if (parseInt(Cookie.get("ttrss_ci_width:wide")) > 0) {
            content_insert.domNode.setStyle(
               {width: Cookie.get("ttrss_ci_width:wide") + "px" });
         }

         headlines_frame.setStyle({ borderBottomWidth: '0px' });

      } else {

         content_insert.attr("region", "bottom");

         content_insert.domNode.setStyle({width: 'auto',
            height: '50%',
            borderTopWidth: '0px'});

         if (parseInt(Cookie.get("ttrss_ci_height:normal")) > 0) {
            content_insert.domNode.setStyle(
               {height: Cookie.get("ttrss_ci_height:normal") + "px" });
         }

         headlines_frame.setStyle({ borderBottomWidth: '1px' });
      }

      headlines_frame.setAttribute("data-is-wide-screen", wide ? "true" : "false");

      Article.close();

      if (article_id) Article.view(article_id);

      xhr.post("backend.php", {op: "RPC", method: "setWidescreen", wide: wide ? 1 : 0});
   },
   initHotkeyActions: function() {
      if (this.is_prefs) {

         this.hotkey_actions["feed_subscribe"] = () => {
            CommonDialogs.subscribeToFeed();
         };

         this.hotkey_actions["create_label"] = () => {
            CommonDialogs.addLabel();
         };

         this.hotkey_actions["create_filter"] = () => {
            Filters.edit();
         };

         this.hotkey_actions["help_dialog"] = () => {
            this.hotkeyHelp();
         };

      } else {

         this.hotkey_actions["next_feed"] = () => {
            const [feed, is_cat] = Feeds.getNextFeed(
               Feeds.getActive(), Feeds.activeIsCat());

				if (feed !== false)
					Feeds.open({feed: feed, is_cat: is_cat, delayed: true})
         };
         this.hotkey_actions["next_unread_feed"] = () => {
            const [feed, is_cat] = Feeds.getNextFeed(
               Feeds.getActive(), Feeds.activeIsCat(), true);

				if (feed !== false)
					Feeds.open({feed: feed, is_cat: is_cat, delayed: true})
         };
         this.hotkey_actions["prev_feed"] = () => {
            const [feed, is_cat] = Feeds.getPreviousFeed(
               Feeds.getActive(), Feeds.activeIsCat());

				if (feed !== false)
					Feeds.open({feed: feed, is_cat: is_cat, delayed: true})
         };
         this.hotkey_actions["prev_unread_feed"] = () => {
            const [feed, is_cat] = Feeds.getPreviousFeed(
               Feeds.getActive(), Feeds.activeIsCat(), true);

				if (feed !== false)
					Feeds.open({feed: feed, is_cat: is_cat, delayed: true})
         };
         this.hotkey_actions["next_article_or_scroll"] = (event) => {
            if (this.isCombinedMode())
               Headlines.scroll(Headlines.line_scroll_offset, event);
            else
               Headlines.move('next');
         };
         this.hotkey_actions["prev_article_or_scroll"] = (event) => {
            if (this.isCombinedMode())
               Headlines.scroll(-Headlines.line_scroll_offset, event);
            else
               Headlines.move('prev');
         };
         this.hotkey_actions["next_article_noscroll"] = () => {
            Headlines.move('next');
         };
         this.hotkey_actions["prev_article_noscroll"] = () => {
            Headlines.move('prev');
         };
         this.hotkey_actions["next_article_noexpand"] = () => {
            Headlines.move('next', {no_expand: true});
         };
         this.hotkey_actions["prev_article_noexpand"] = () => {
            Headlines.move('prev', {no_expand: true});
         };
         this.hotkey_actions["search_dialog"] = () => {
            Feeds.search();
         };
         this.hotkey_actions["cancel_search"] = () => {
            Feeds.cancelSearch();
         };
         this.hotkey_actions["toggle_mark"] = () => {
            Headlines.selectionToggleMarked();
         };
         this.hotkey_actions["toggle_publ"] = () => {
            Headlines.selectionTogglePublished();
         };
         this.hotkey_actions["toggle_unread"] = () => {
            Headlines.selectionToggleUnread({no_error: 1});
         };
         this.hotkey_actions["edit_tags"] = () => {
            const id = Article.getActive();
            if (id) {
               Article.editTags(id);
            }
         };
         this.hotkey_actions["open_in_new_window"] = () => {
            if (Article.getActive()) {
               Article.openInNewWindow(Article.getActive());
            }
         };
         this.hotkey_actions["catchup_below"] = () => {
            Headlines.catchupRelativeTo(1);
         };
         this.hotkey_actions["catchup_above"] = () => {
            Headlines.catchupRelativeTo(0);
         };
         this.hotkey_actions["article_scroll_down"] = (event) => {
            if (this.isCombinedMode())
               Headlines.scroll(Headlines.line_scroll_offset, event);
            else
               Article.scroll(Headlines.line_scroll_offset, event);
         };
         this.hotkey_actions["article_scroll_up"] = (event) => {
            if (this.isCombinedMode())
               Headlines.scroll(-Headlines.line_scroll_offset, event);
            else
               Article.scroll(-Headlines.line_scroll_offset, event);
         };
         this.hotkey_actions["next_headlines_page"] = (event) => {
            Headlines.scrollByPages(1, event);
         };
         this.hotkey_actions["prev_headlines_page"] = (event) => {
            Headlines.scrollByPages(-1, event);
         };
         this.hotkey_actions["article_page_down"] = (event) => {
            if (this.isCombinedMode())
               Headlines.scrollByPages(1, event);
            else
               Article.scrollByPages(1, event);
         };
         this.hotkey_actions["article_page_up"] = (event) => {
            if (this.isCombinedMode())
               Headlines.scrollByPages(-1, event);
            else
               Article.scrollByPages(-1, event);
         };
         this.hotkey_actions["close_article"] = () => {
            if (this.isCombinedMode()) {
               Article.cdmUnsetActive();
            } else {
               Article.close();
            }
         };
         this.hotkey_actions["email_article"] = () => {
            if (typeof Plugins.Mail !== "undefined") {
               Plugins.Mail.onHotkey(Headlines.getSelected());
            } else {
               alert(__("Please enable mail or mailto plugin first."));
            }
         };
         this.hotkey_actions["select_all"] = () => {
            Headlines.select('all');
         };
         this.hotkey_actions["select_unread"] = () => {
            Headlines.select('unread');
         };
         this.hotkey_actions["select_marked"] = () => {
            Headlines.select('marked');
         };
         this.hotkey_actions["select_published"] = () => {
            Headlines.select('published');
         };
         this.hotkey_actions["select_invert"] = () => {
            Headlines.select('invert');
         };
         this.hotkey_actions["select_none"] = () => {
            Headlines.select('none');
         };
         this.hotkey_actions["feed_refresh"] = () => {
            if (typeof Feeds.getActive() !== "undefined") {
               Feeds.open({feed: Feeds.getActive(), is_cat: Feeds.activeIsCat()});
            }
         };
         this.hotkey_actions["feed_unhide_read"] = () => {
            Feeds.toggleUnread();
         };
         this.hotkey_actions["feed_subscribe"] = () => {
            CommonDialogs.subscribeToFeed();
         };
         this.hotkey_actions["feed_debug_update"] = () => {
            if (!Feeds.activeIsCat() && parseInt(Feeds.getActive()) > 0) {

               /* global __csrf_token */
               App.postOpenWindow("backend.php", {op: "Feeds", method: "updatedebugger",
                  feed_id: Feeds.getActive(), csrf_token: __csrf_token});

            } else {
               alert("You can't debug this kind of feed.");
            }
         };

         this.hotkey_actions["feed_debug_viewfeed"] = () => {

				let query = {
					...{op: "Feeds", method: "view", feed: Feeds.getActive(), timestamps: 1,
							debug: 1, cat: Feeds.activeIsCat(), csrf_token: __csrf_token},
					...dojo.formToObject("toolbar-main")
				};

				if (Feeds._search_query) {
					query = Object.assign(query, Feeds._search_query);
				}

				console.log('debug_viewfeed', query);

				App.postOpenWindow("backend.php", query);
         };

         this.hotkey_actions["feed_edit"] = () => {
            if (Feeds.activeIsCat())
               alert(__("You can't edit this kind of feed."));
            else
               CommonDialogs.editFeed(Feeds.getActive());
         };
         this.hotkey_actions["feed_catchup"] = () => {
            if (typeof Feeds.getActive() !== "undefined") {
               Feeds.catchupCurrent();
            }
         };
         this.hotkey_actions["feed_reverse"] = () => {
            Headlines.reverse();
         };
         this.hotkey_actions["feed_toggle_grid"] = () => {
            xhr.json("backend.php", {op: "RPC", method: "togglepref", key: "CDM_ENABLE_GRID"}, (reply) => {
               App.setInitParam("cdm_enable_grid", reply.value);
               Headlines.renderAgain();
            })
         };
         this.hotkey_actions["feed_toggle_vgroup"] = () => {
            xhr.post("backend.php", {op: "RPC", method: "togglepref", key: "VFEED_GROUP_BY_FEED"}, () => {
               Feeds.reloadCurrent();
            })
         };
         this.hotkey_actions["catchup_all"] = () => {
            Feeds.catchupAll();
         };
         this.hotkey_actions["cat_toggle_collapse"] = () => {
            if (Feeds.activeIsCat()) {
               dijit.byId("feedTree").collapseCat(Feeds.getActive());
            }
         };
         this.hotkey_actions["goto_read"] = () => {
            Feeds.open({feed: Feeds.FEED_RECENTLY_READ});
         };
         this.hotkey_actions["goto_all"] = () => {
            Feeds.open({feed: Feeds.FEED_ALL});
         };
         this.hotkey_actions["goto_fresh"] = () => {
            Feeds.open({feed: Feeds.FEED_FRESH});
         };
         this.hotkey_actions["goto_marked"] = () => {
            Feeds.open({feed: Feeds.FEED_STARRED});
         };
         this.hotkey_actions["goto_published"] = () => {
            Feeds.open({feed: Feeds.FEED_PUBLISHED});
         };
         this.hotkey_actions["goto_prefs"] = () => {
            App.openPreferences();
         };
			this.hotkey_actions['select_article_cursor'] = () => {
				const id = Article.getUnderPointer();
				if (id)
					document.getElementById(`RROW-${id}`)?.classList.toggle('Selected');
			};
         this.hotkey_actions["create_label"] = () => {
            CommonDialogs.addLabel();
         };
         this.hotkey_actions["create_filter"] = () => {
            Filters.edit();
         };
         this.hotkey_actions["collapse_sidebar"] = () => {
            Feeds.toggle();
         };
         this.hotkey_actions["toggle_full_text"] = () => {
            if (typeof Plugins.Af_Readability !== "undefined") {
               if (Article.getActive())
                  Plugins.Af_Readability.embed(Article.getActive());
            } else {
               alert(__("Please enable af_readability first."));
            }
         };
         this.hotkey_actions["toggle_widescreen"] = () => {
				this.setWideScreenMode(!this.isWideScreenMode());
         };
         this.hotkey_actions["help_dialog"] = () => {
            this.hotkeyHelp();
         };
         this.hotkey_actions["toggle_combined_mode"] = () => {
				App.setCombinedMode(!App.isCombinedMode());
         };
         this.hotkey_actions["toggle_cdm_expanded"] = () => {
				App.setExpandedMode(!App.isExpandedMode());
         };
         this.hotkey_actions["article_span_grid"] = () => {
            Article.cdmToggleGridSpan(Article.getActive());
         };
      }
   },
   openPreferences: function(tab) {
      document.location.href = "prefs.php" + (tab ? "?tab=" + tab : "");
   },
   onActionSelected: function(opid) {
      switch (opid) {
         case "qmcPrefs":
            App.openPreferences();
            break;
         case "qmcLogout":
            App.postCurrentWindow("public.php", {op: "logout", csrf_token: __csrf_token});
            break;
         case "qmcSearch":
            Feeds.search();
            break;
         case "qmcFilterFeeds":
            Feeds.filter();
            break;
         case "qmcAddFeed":
            CommonDialogs.subscribeToFeed();
            break;
         case "qmcDigest":
            window.location.href = "backend.php?op=Digest";
            break;
         case "qmcEditFeed":
            if (Feeds.activeIsCat())
               alert(__("You can't edit this kind of feed."));
            else
               CommonDialogs.editFeed(Feeds.getActive());
            break;
         case "qmcRemoveFeed":
            {
               const actid = Feeds.getActive();

               if (!actid) {
                  alert(__("Please select some feed first."));
                  return;
               }

               if (Feeds.activeIsCat()) {
                  alert(__("You can't unsubscribe from the category."));
                  return;
               }

               const fn = Feeds.getName(actid);

               if (confirm(__("Unsubscribe from %s?").replace("%s", fn))) {
                  CommonDialogs.unsubscribeFeed(actid);
               }
            }
            break;
         case "qmcCatchupAll":
            Feeds.catchupAll();
            break;
         case "qmcShowOnlyUnread":
            Feeds.toggleUnread();
            break;
         case "qmcToggleWidescreen":
            App.setWideScreenMode(!App.isWideScreenMode());
            break;
			case "qmcToggleCombined":
				App.setCombinedMode(!App.isCombinedMode());
				break;
			case "qmcToggleExpanded":
				App.setExpandedMode(!App.isExpandedMode());
				break;
         case "qmcHKhelp":
            this.hotkeyHelp()
            break;
         default:
            console.warn("quickMenuGo: unknown action:", opid);
      }
   },
}

