class InitPromise
{
    static addPromise(promise)
    {
        if (typeof promise === 'function')
        {
            let func = promise;
            promise = InitPromise.mainPromise.then(function(app){
                return (func(app));
            });
        }
        InitPromise.promises.push(promise);
    }

    static resolve(app)
    {
        InitPromise.mainResolver(app);
        return InitPromise.mainPromise.then(function(){
            return Promise.all(InitPromise.promises);
        });
    }
}

InitPromise.promises = [];
InitPromise.mainPromise = new Promise(function(resolver){
    InitPromise.mainResolver = resolver;
});

class AndroidBridge {

    static addDefinition(definition, object) {
        if (typeof definition !== "function") {
            if (typeof definition === "string"){
                let parts = definition.split(".");
                for (let i = parts.length-1; i >= 1; i--) {
                    let newObject = {};
                    newObject[parts[i]] = object;
                    object = newObject;
                }
                definition = parts[0];
                // console.log("parts for", definition, parts, object);
            }
            let textDefinition = definition;
            definition = () => {
                // console.log("defining", textDefinition, object);
                window[textDefinition] = object;
            };
        }
        AndroidBridge.definitions.push(definition);
    }

    static applyDefinitions() {
        for (let i = 0; i < AndroidBridge.definitions.length; i++) {
            AndroidBridge.definitions[i]();
        }
        return Promise.resolve();
    }
}

AndroidBridge.definitions = [];
AndroidBridge.addDefinition("InitPromise.addPromise", InitPromise.addPromise);

class MenuAction {
    constructor(title, callback, showFor, order) {
        this.title = Helper.nonNull(title, null);
        this.callback = callback;
        this.showFor = Helper.nonNull(showFor, MenuAction.SHOW_FOR_MEDIUM);
        this.order = Helper.nonNull(order, 1000);
        this._liClass = "";

        this._menu = null;
        this._activated = true;
        this._visible = true;
        this.id = MenuAction.maxId++;
        this._icon = null;
        this._shouldTranslate = true;
        this._copies = [];
    }

    setTitle(title)
    {
        this.title = title;
    }

    setShouldTranslate(shouldTranslate)
    {
        this._shouldTranslate = shouldTranslate;
    }

    getShowFor(){
        return this.showFor;
    }

    getTitle()
    {
        return this.title;
    }

    getShouldTranslate()
    {
        return this._shouldTranslate;
    }

    remove(removeCopies) {
        removeCopies = Helper.nonNull(removeCopies, false);
        if (Helper.isNotNull(this._menu)) {
            console.log(this._menu);
            this._menu.removeAction(this);
            this._menu = null;
        }
        if (removeCopies)
        {
            for (let i = 0; i < this._copies.length; i++) {
                this._copies[i].remove();
            }
        }
    }

    getMenu() {
        return this._menu;
    }

    setMenu(value) {
        this._menu = value;
    }

    getVisible() {
        return this._visible;
    }

    setVisible(value) {
        if (value !== this._visible) {
            this._visible = value;
            this.redraw();
        }
    }

    getActivated() {
        return this._activated;
    }

    getIcon() {
        return this._icon;
    }

    setIcon(value) {
        this._icon = value;
    }

    getId()
    {
        return this.id;
    }

    redraw() {
        if (Helper.isNotNull(this._menu)) {
            this._menu.updateAction(this);
        }
    }
    copy(instance){
        let copy = Helper.nonNull(instance, new MenuAction());
        copy.title = this.title;
        copy.callback = this.callback;
        copy.showFor = this.showFor;
        copy.order = this.order;
        copy._liClass = this._liClass;

        copy._activated = this._activated;
        copy._visible = this._visible;
        copy._icon = this._icon;
        copy._shouldTranslate = this._shouldTranslate;

        copy._menu = null;
        copy.id = MenuAction.maxId++;
        this._copies.push(copy);
        return copy;
    }

    redrawMenu()
    {
        if (Helper.isNotNull(this._menu)) {
            this._menu.redraw();
        }
    }
}
MenuAction.maxId = 0;

MenuAction.SHOW_ALWAYS = "always";
MenuAction.SHOW_FOR_MEDIUM = "medium";
MenuAction.SHOW_FOR_LARGE = "large";
MenuAction.SHOW_NEVER = "never";

class OpenSubmenuAction extends MenuAction {
    constructor(title, menu, showFor, order) {
        super(title, function (action) {
            action.getSubmenu().toggle();
            action.redraw();
        }, showFor, order);

        this.submenu = menu;
        menu.setParentAction(this);
    }

    getSubmenu() {
        return this.submenu;
    }

    copy(instance) {
        instance = super.copy(Helper.nonNull(instance, new OpenSubmenuAction(null, this.submenu.copy())));
        return instance;
    }
}

class Menu {
    constructor(parentElementSelector) {
        this.actions = [];
        this.submenus = [];
        if (typeof parentElementSelector === 'string') {
            this.parentElements = document.querySelectorAll(parentElementSelector);
        }
        else if (Array.isArray(parentElementSelector)) {
            this.parentElements = parentElementSelector;
        }
        else {
            this.parentElements = [parentElementSelector];
        }
    }

    copy(instance) {
        instance = Helper.nonNull(instance, new Menu([]));

        instance.actions = [];
        for (let i = 0, n = this.actions.length; i < n; i++) {
            instance.actions.push(this.actions[i].copy());
        }

        instance.submenus = [];
        for (let i = 0, n = this.submenus.length; i < n; i++) {
            instance.submenus.push(this.submenus[i].copy());
        }

        return instance;
    }

    addAction(action) {
        if (Helper.includesNot(this.actions, action)) {
            this.actions.push(action);
            this.redraw();
            action.setMenu(this);
            if (action instanceof OpenSubmenuAction) {
                this.submenus.push(action.getSubmenu());
            }
        }
    }

    draw() {
        if (Helper.isNotNull(this.parentElements)) {
            this.sortActions();

            let actionElements = [];
            for (let i = 0; i < this.actions.length; i++) {
                let element = this.renderAction(this.actions[i]);
                this.actions[i]._htmlElement = element;
                actionElements.push(element);
            }
            for (let i = 0, n = this.parentElements.length; i < n; i++) {
                this.parentElements[i].removeAllChildren();
                for (let i2 = 0, n2 = actionElements.length; i2 < n2; i2++) {
                    this.parentElements[i].appendChild(Helper.cloneNode(actionElements[i2]));
                }
                this.parentElements[i].onclick = this._getOnClickListener();
            }
        }
    }

    _getOnClickListener() {
        let menu = this;
        return function (event) {
            let _element = event.target;
            if (_element.matches('.action') || _element.matches('.action *')) {
                // while (!_element.matches('.action > a')) {
                //     _element = _element.parentNode;
                // }
                _element = _element.closest(".action");
                let actionId = parseInt(_element.dataset["id"]);
                for (let i = 0, n = menu.actions.length; i < n; i++) {
                    if (menu.actions[i].id === actionId) {
                        if (typeof menu.actions[i].callback === 'function' && menu.actions[i].getActivated()) {
                            menu.actions[i].callback(menu.actions[i], event);
                        }
                        return menu.actions[i];
                    }
                }
                for (let i = 0, n = menu.submenus.length; i < n; i++) {
                    if (menu.submenus[i].click(actionId, event)) {
                        return menu.submenus[i];
                    }
                }
            }
            return null;
        };
    }

    /** @protected */
    renderAction(action) {
        let aElement = document.createElement("a");
        if (typeof action.callback === 'string') {
            aElement.href = action.callback;
        }

        if (Helper.isNotNull(action.getIcon())) {
            let iconElement = document.createElement("img");
            iconElement.src = action.getIcon();
            iconElement.classList.add('action-image');
            if (action.getShouldTranslate()) {
                iconElement.dataset["translationTitle"] = action.title;
            }
            aElement.appendChild(iconElement);
        }
        let title = action.getTitle();
        if (action.getShouldTranslate()) {
            title = Translator.makePersistentTranslation(title);
        }
        else {
            title = document.createTextNode(title);
        }
        aElement.appendChild(title);

        return this.renderLiElement(aElement, action)
    }

    /** @protected */
    renderLiElement(aElement, action) {
        let liElement = document.createElement("li");
        liElement.classList.add('action');
        if (action._liClass.trim() !== "") {
            liElement.classList.add(action._liClass);
        }
        liElement.appendChild(aElement);
        liElement.dataset["id"] = action.id;
        if (Helper.isNotNull(action.getIcon())) {
            liElement.classList.add("img");
        }

        if (!action.getVisible()) {
            liElement.classList.add("hidden");
        }

        if (action instanceof OpenSubmenuAction) {
            action.getSubmenu().draw();
            liElement.appendChild(action.getSubmenu().getParentElement());
            liElement.classList.add("is-dropdown-submenu-parent");
            liElement.classList.add("opens-right");
        }
        return liElement;
    }

    /** @private */
    sortActions() {
        this.actions = this.actions.sort(function (first, second) {
            return first.order - second.order;
        });
    }

    _getElementsForAction(action) {
        let elements = [];
        for (let i = 0; i < this.parentElements.length; i++) {
            let elem = this.parentElements[i].querySelector("[data-id=\"" + action.getId() + "\"]");
            Helper.isNull(elem) || elements.push(elem);
        }
        return elements
    }

    updateAction(action) {
        let oldElements = this._getElementsForAction(action);
        if (oldElements.length === 0) {
            return;
        }

        let element = this.renderAction(action);
        action._htmlElement = element;

        for (let i = 0; i < oldElements.length; i++) {
            oldElements[i].replaceWith(Helper.cloneNode(element));
        }
    }

    removeAction(action) {
        let index = this.actions.indexOf(action);
        if (index > 0) {
            this.actions.splice(index, 1);
            let oldElements = this._getElementsForAction(action);
            for (let i = 0, n = oldElements.length; i < n; i++) {
                oldElements[i].remove();
            }
            if (action instanceof OpenSubmenuAction) {
                let index = this.submenus.indexOf(action.getSubmenu());
                this.submenus.splice(index, 1);
            }
        }
    }

    redraw() {
        this.draw();
    }
}

Menu.SHOW_ALWAYS = "always";
Menu.SHOW_FOR_MEDIUM = "medium";
Menu.SHOW_FOR_SMEDIUM = "smedium";
Menu.SHOW_FOR_LARGE = "large";
Menu.SHOW_NEVER = "never";

class Submenu extends Menu
{
    constructor()
    {
        let menuElement = document.createElement("ul");
        menuElement.classList.add("menu");
        menuElement.classList.add("vertical");
        menuElement.classList.add("submenu");
        menuElement.classList.add("is-dropdown-submenu");
        menuElement.classList.add("first-sub");
        super(menuElement);

        this.parentAction = null;
        this.isOpen = false;
    }

    

    copy(instance)
    {
        instance = super.copy(Helper.nonNull(instance, new Submenu()));
        instance.parentElements = [];
        for (let i = 0, n = this.parentElements.length; i < n; i++) {
            instance.parentElements.push(Helper.cloneNode(this.parentElements[i]));
        }
        instance.parentAction = this.parentAction;
        instance.isOpen = this.isOpen;
        return instance;
    }

    setParentAction(action)
    {
        this.parentAction = action;
    }

    draw()
    {
        super.draw();
        if (Helper.isNotNull(this.parentElements))
        {
            let self = this;
            for (let i = 0; i < this.parentElements.length; i++) {
                let closeListener = document.createElement("div");
                closeListener.classList.add("close-listener");
                closeListener.onclick = function(e){
                    console.log(e);
                    self.close();
                };
                this.parentElements[i].insertBefore(closeListener, this.parentElements[i].firstElementChild);
            }
        }
    }

    getParentElement()
    {
        return this.parentElements[0];
    }

    _getOnClickListener()
    {
        return function () {};
    }

    click(actionId, event)
    {
        for (let i = 0, n = this.actions.length; i < n; i++) {
            if (this.actions[i].id === actionId)
            {
                if (typeof this.actions[i].callback === 'function' && this.actions[i].getActivated()) {
                    this.actions[i].callback(this.actions[i], event);
                }
                this.close();
                return true;
            }
        }
        return false;
    }

    toggle()
    {
        if (this.isOpen)
        {
            this.close();
        }
        else
        {
            this.open();
        }
    }

    open()
    {
        this.isOpen = true;
        for (let i = 0, n = this.parentElements.length; i < n; i++) {
            this.parentElements[i].classList.add("js-dropdown-active");
        }
        if (Helper.isNotNull(this.parentAction))
        {
            this.parentAction.redraw();
        }
    }
    close()
    {
        this.isOpen = false;
        for (let i = 0, n = this.parentElements.length; i < n; i++) {
            this.parentElements[i].classList.remove("js-dropdown-active");
        }
        if (Helper.isNotNull(this.parentAction))
        {
            this.parentAction.redraw();
        }
    }
}

class TranslatorDB {
    constructor() {
        this._indexedDB = indexedDB || mozIndexedDB || webkitIndexedDB || msIndexedDB;
        this._version = 3;

        let self = this;
        this._dbPromise = new Promise(function (resolve, reject) {
            let request = self._indexedDB.open("Translator", self._version);
            request.onupgradeneeded = function (event) {
                let db = event.target.result;
                self._upgradeDb(db);
            };
            request.onsuccess = function (event) {
                let db = event.target.result;
                resolve(db);
            };
            request.onerror = function (event) {
                reject(event);
            };
        }).catch(function(e){
            console.error(e);
        });
    }

    _upgradeDb(db) {
        try {
            db.deleteObjectStore("currentLang");
            db.deleteObjectStore("translations");
        }
        catch (e) {
            console.warn(e);
        }
        let currentLangObjectStore = db.createObjectStore("currentLang", {"keyPath": "id"});
        let translationsObjectStore = db.createObjectStore("translations", {"keyPath": ["lang","key"]});
        translationsObjectStore.createIndex("lang", "lang", {"unique": false});
    }

    setLanguage(lang) {
        this._dbPromise.then(function (db) {
            let transaction = TranslatorDB._openTransaction(["currentLang"], "readwrite", db);
            let currentLangObjectStore = transaction.objectStore("currentLang");
            currentLangObjectStore.put({"id": 1, "lang": lang});
        }).catch(function(e){
            console.error(e);
        });
    }

    saveTranslationsForLang(lang, translations) {
        return this._dbPromise.then(function (db) {
            return new Promise(function (resolve) {
                let transaction = TranslatorDB._openTransaction(["translations"], "readwrite", db);
                let translationsObjectStore = transaction.objectStore("translations");
                for (let k in translations) {
                    translationsObjectStore.put({"lang": lang, "key": k, "translation": translations[k]});
                }
                transaction.oncomplete = function () {
                    resolve();
                };
            });
        }).catch(function(e){
            // console.error(e);
        });
    }

    loadTranslationsForLang(lang) {
        return this._dbPromise.then(function (db) {
            return new Promise(function (resolve) {
                let transaction = TranslatorDB._openTransaction(["translations"], "readonly", db);
                let translationsObjectStore = transaction.objectStore("translations");
                let index = translationsObjectStore.index("lang");
                let request = index.openCursor(IDBKeyRange.only(lang));

                let translations = {};
                request.onsuccess = function (e) {
                    let cursor = e.target.result;
                    if (cursor) {

                        let translation = cursor.value;
                        translations[translation["key"]] = translation["translation"];
                        cursor.continue();
                    }
                };
                transaction.oncomplete = function(){
                    resolve(translations);
                };
            });
        }).catch(function(e){
            console.error(e);
            return {};
        });
    }

    getLanguage() {
        return this._dbPromise.then(function (db) {
            return new Promise(function (resolve) {
                let transaction = TranslatorDB._openTransaction(["currentLang"], "readonly", db);
                let currentLangObjectStore = transaction.objectStore("currentLang");
                let req = currentLangObjectStore.get(1);
                req.onsuccess = function (e) {
                    let data = e.currentTarget.result;
                    if (data)
                    {
                        resolve(data["lang"]);
                    }
                    else
                    {
                        resolve(null);
                    }
                };
                req.onerror = function (e) {
                    resolve(null);
                };
            });
        }).catch(function(e){
            // console.error(e);
        });
    }

    static _openTransaction(name, transactionMode, db) {
        let transaction = null;
        try {
            transaction = db.transaction(name, transactionMode);
        }
        catch (e) {
            console.warn(e);
            transaction = db.transaction(name);
        }
        return transaction;
    }
}

class Translator {
    constructor() {
        this._translations = [];
        this._db = new TranslatorDB();
        this._currentLanguage = null;
        this._supportedLanguages = Translator.supportedLanguages;
        this._baseLanguage = Translator.baseLanguage;
        this._languageBasePath = Translator.languageBasePath;
        this._markUntranslatedTranslations = Translator.markUntranslatedTranslations;
        this._markTranslations = Translator.markTranslations;

        let self = this;
        this._initPromise = this.loadBaseLanguage().then(function () {
            return self.loadUserLanguage();
        });
    }

    _loadLanguage(language) {
        let self = this;
        return fetch(Helper.basePath(this._languageBasePath + language + ".json")).then(function (result) {
            return result.json();
        }).then(function (res) {
            self._translations[language] = Object.assign(res, self._translations[language]);
            self._db.saveTranslationsForLang(language, self._translations[language]);
        }).catch(function (err) {
            console.error("could not load lang " + language + " because of error: ", err);
        });
    }

    loadBaseLanguage() {
        let self = this;
        return this._loadLanguage(this._baseLanguage).then(function () {
            self._currentLanguage = self._baseLanguage;
            if (typeof document !== 'undefined') {
                document.getElementsByTagName("html")[0].setAttribute("lang", self._baseLanguage);
            }
        });
    };

    static setLanguage(language) {
        let instance = Translator.getInstance();
        if (instance) {
            return instance.setLanguage(language);
        }
    }

    setLanguage(language) {
        if (this._currentLanguage === language) {
            this.updateTranslations();
            return Promise.resolve();
        }

        if (this._supportedLanguages.indexOf(language) === -1) {
            return Promise.resolve();
        }

        this._currentLanguage = language;
        if (typeof localStorage !== 'undefined') {
            localStorage.setItem("language", language);
        }
        this._db.setLanguage(language);

        let self = this;
        return this._loadLanguage(language).then(function () {
            if (typeof document !== 'undefined') {
                document.getElementsByTagName("html")[0].setAttribute("lang", language);
            }
            self.updateTranslations();
        });
    }

    static translate(key, args) {
        let instance = Translator.getInstance();
        if (instance) {
            return instance.translate(key, args);
        }
        return "";
    }

    translate(key, args) {
        if (typeof key === 'object' && Helper.isNotNull(key)) {
            key = this.addDynamicTranslation(key);
        }

        let translation = null;
        if (Helper.isNotNull(this._translations[this._currentLanguage]) && Helper.isNotNull(this._translations[this._currentLanguage][key])) {
            translation = this._translations[this._currentLanguage][key];
        }

        if (Helper.isNull(translation)) {
            if (Translator.logMissingTranslations) {
                console.warn("missing translation for language " + this._currentLanguage + " and key " + key);
            }
            if (Helper.isNotNull(this._translations[this._baseLanguage])) {
                translation = this._translations[this._baseLanguage][key];
            }

            if (Helper.isNull(translation)) {
                if (Translator.logMissingTranslations) {
                    console.error("missing base translation for key " + key + ". FIX IT");
                }
                translation = key;
            }
            if (this._markUntranslatedTranslations) {
                translation = "&gt;&gt;" + translation + "&lt;&lt;";
            }
        }

        if (this._markTranslations) {
            translation = "$" + translation + "$";
        }

        if (args !== undefined) {
            translation = translation.format(args);
        }

        return translation;
    }

    static addDynamicTranslation(trans) {
        let instance = Translator.getInstance();
        if (instance) {
            return instance.addDynamicTranslation(trans);
        }
    }

    addDynamicTranslation(trans) {
        let key = trans["key"];
        delete trans["key"];
        for (let lang in trans) {
            if (trans.hasOwnProperty(lang)) {
                if (Helper.isNull(this._translations[lang])) {
                    this._translations[lang] = {};
                }
                this._translations[lang][key] = trans[lang];
            }
        }
        return key;
    }

    updateTranslations() {
        if (typeof document !== 'undefined') {
            let elements = document.querySelectorAll("[data-translation]");
            for (let i = 0, max = elements.length; i < max; i++) {
                if (elements[i].dataset["translation"] != "") {
                    try {
                        elements[i].innerHTML = this.translate(elements[i].dataset["translation"], (elements[i].dataset["translationArgs"] !== undefined) ? JSON.parse(elements[i].dataset["translationArgs"]) : undefined);
                    }
                    catch (err) {
                        console.error("wrong configured translation: " + err);
                    }
                }
                for (let k in elements[i].dataset) {
                    if (k.startsWith("translation") && !k.endsWith("Args")) {
                        try {
                            elements[i][k.substr(11).toLowerCase()] = this.translate(elements[i].dataset[k], (elements[i].dataset[k + "Args"] !== undefined) ? JSON.parse(elements[i].dataset[k + "Args"]) : undefined);
                        }
                        catch (err) {
                            console.error("wrong configured translation: " + err);
                        }
                    }
                }
            }
        }
    }

    loadUserLanguage() {
        let userLanguage = localStorage.getItem("language");
        if (Helper.isNull(userLanguage) || this._supportedLanguages.indexOf(userLanguage) === -1) {
            let userLanguages = [];
            if (Helper.isNotNull(navigator.languages)) {
                userLanguages = navigator.languages.slice(0); //.slice(0) klont das Array. Behebt einen Bug in Firefox
            }

            if (navigator.language !== undefined) {
                userLanguages.push(navigator.language);
            }
            //sicherstellen, dass überhaupt eine Sprache gefunden wird
            userLanguages.push(this._baseLanguage);

            if (userLanguages !== undefined) {
                for (let i = 0, numLanguages = userLanguages.length; i < numLanguages; i++) {
                    if (this._supportedLanguages.indexOf(userLanguages[i]) !== -1) {
                        userLanguage = userLanguages[i];
                        break;
                    }
                }
            }
        }
        return this.setLanguage(userLanguage.toLowerCase())
    }

    static makePersistentTranslation(key, args, tag) {
        tag = Helper.nonNull(tag, "span");
        if (typeof key === 'object') {
            key = Translator.addDynamicTranslation(key);
        }

        if (typeof document !== 'undefined') {
            let htmlElem = document.createElement(tag);
            htmlElem.dataset["translation"] = key;
            if (args !== undefined) {
                htmlElem.dataset["translationArgs"] = JSON.stringify(args);
            }
            htmlElem.innerHTML = Translator.translate(key, args);
            return htmlElem;
        }
    }

    static generateChangeLanguageMenuAction() {
        let submenu = new Submenu();
        submenu.addAction(new MenuAction("en", function () {
            Translator.getInstance().setLanguage("en");
        }));
        submenu.addAction(new MenuAction("de", function () {
            Translator.getInstance().setLanguage("de");
        }));
        return new OpenSubmenuAction("current-lang", submenu, Menu.SHOW_ALWAYS)
    }

    static init() {
        Translator.instance = new Translator();
        // Translator.loadBaseLanguage().then(function () {
        //     Translator.loadUserLanguage();
        // });
    }

    static getInstance() {
        return Translator.instance;
    }
}
Translator.logMissingTranslations = false;

Translator.instance = null;

Translator.baseLanguage = "en";
Translator.supportedLanguages = [
    "de",
    "en"
];
Translator.markUntranslatedTranslations = true;
Translator.markTranslations = false;
Translator.languageBasePath = "js/lang/";
Translator.currentLanguage = null;
Translator.translations = {};


AndroidBridge.addDefinition("Translator.setLanguage", Translator.setLanguage);

class Helper {
    static init() {
        Helper.heightMmToPxFactor = null;
        Helper.widthMmToPxFactor = null;
    }

    static includesNot(array, value, fromIndex) {
        return -1 === array.indexOf(value, fromIndex);
    }

    static includes(array, value, fromIndex) {
        return !Helper.includesNot(array, value, fromIndex);
    }

    static isSet() {
        if (arguments.length > 0) {
            const object = arguments[0];
            let keys = Array.prototype.slice.call(arguments, 1);
            return (Helper.isNotNull(object) && (keys.length === 0 || Helper.isSet.apply(null, [object[keys[0]]].concat(keys.slice(1)))));
        }
        return false;
    }

    static isNull(variable) {
        return (variable === null || variable === undefined);
    }

    static isNotNull(variable) {
        return !Helper.isNull(variable);
    }

    static nonNull(val1, val2) {
        for (let i = 0; i < arguments.length; i++) {
            if (Helper.isNotNull(arguments[i])) {
                return arguments[i];
            }
        }
        return null;
    }

    static notEmpty(value) {
        return !Helper.empty(value);
    }

    static buildQuery(values) {
        let queryStrings = [];
        for (let k in values) {
            queryStrings.push(encodeURIComponent(k) + "=" + encodeURIComponent(values[k]));
        }
        return "?" + queryStrings.join("&");
    }

    static empty(value) {
        return (Helper.isNull(value) || (typeof value === 'string' && value.trim() === ""))
    }

    static inflateElementsFromString(string) {
        let template = document.createElement('template');
        template.innerHTML = string;
        return template.content.childNodes;
    }

    static createLoadingSymbol() {
        let svgNS = "http://www.w3.org/2000/svg";

        let loader = document.createElement("div");
        loader.className = 'loader';

        let svg = document.createElementNS(svgNS, "svg");
        svg.setAttribute('viewBox', "0 0 32 32");
        svg.setAttribute("widh", "32");
        svg.setAttribute("height", "32");

        let circle = document.createElementNS(svgNS, "circle");
        circle.setAttribute("id", "spinner");
        circle.setAttribute("cx", "16");
        circle.setAttribute("cy", "16");
        circle.setAttribute("r", "14");
        circle.setAttribute("fill", "none");

        svg.appendChild(circle);
        loader.appendChild(svg);

        return loader;
    }

    static basePath(url) {
        return SystemSettings.getBasePath() + url;
    }

    static isMobileApple() {
        return navigator.userAgent.match(/iPhone|iPad|iPod/i);
    }

    static isMobile() {
        return (navigator.userAgent.match(/Android|BlackBerry|Opera Mini|IEMobile/i) !== null || Helper.isMobileApple() || (typeof window.orientation !== "undefined" || window.orientation === false || window.orientation === null));
    }

    static select(e) {
        let range = document.createRange();
        range.selectNodeContents(e);
        let sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }

    static format(number, leadingZeros) {
        number = "" + number;
        while (number.length < leadingZeros) {
            number = "0" + number;
        }
        return number;
    }

    static cloneNode(srcNode) {
        let destNode = srcNode.cloneNode(true);
        destNode.onclick = srcNode.onclick;
        return destNode;
    }

    static encodeToBase(stringToEncode, base) {
        let encodedString = "";
        let charlength = Math.floor(Math.log(265) / Math.log(base));
        for (let i = 0; i <  stringToEncode.length; i++) {
            let value = stringToEncode.charCodeAt(i).toString(base);
            let joinLength = value.length % charlength;
            if (joinLength > 0) {
                let joinArray = new Array(charlength + 1 - (joinLength)); //+1, da join nur zwischen elemente einfügt
                value = joinArray.join("0") + value;
            }
            encodedString += value;
        }
        return encodedString;
    }

    static decodeToBase(stringToDecode, base) {
        let charlength = Math.floor(Math.log(265) / Math.log(base));
        let values = stringToDecode.match(new RegExp(".{1," + charlength + "}", "g")) || [];
        let encodedString = "";
        for (let i = 0, n = values.length; i < n; i++) {
            encodedString += String.fromCharCode(parseInt(values[i], base));
        }
        return encodedString;
    }

    static toggleVisibility(elem) {
        if (elem.style.display === "none") {
            elem.style.display = "";
            return true;
        }
        else {
            elem.style.display = "none";
            return false;
        }
    }

    static print(content) {
        let printContent = document.getElementById("print-content");
        if (content instanceof Element) {
            printContent.removeAllChildren();
            printContent.appendChild(content);
        }
        else {
            printContent.innerHTML = content;
        }
        window.print();
    }

    static strftime(sFormat, date, useUTC) {
        if (!(date instanceof Date)) date = new Date(date);
        useUTC = Helper.nonNull(useUTC, false);
        let nDay = (useUTC) ? date.getUTCDay() : date.getDay(),
            nDate = (useUTC) ? date.getUTCDate() : date.getDate(),
            nMonth = (useUTC) ? date.getUTCMonth() : date.getMonth(),
            nYear = (useUTC) ? date.getUTCFullYear() : date.getFullYear(),
            nHour = (useUTC) ? date.getUTCHours() : date.getHours(),
            aDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
            aMonths = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
            aDayCount = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334],
            isLeapYear = function () {
                if ((nYear & 3) !== 0) return false;
                return nYear % 100 !== 0 || nYear % 400 === 0;
            },
            getThursday = function () {
                let target = new Date(date);
                target.setDate(nDate - ((nDay + 6) % 7) + 3);
                return target;
            },
            zeroPad = function (nNum, nPad) {
                return ('' + (Math.pow(10, nPad) + nNum)).slice(1);
            };

        return sFormat.replace(/%[a-z]/gi, function (sMatch) {
            return {
                '%a': Translator.makePersistentTranslation(aDays[nDay].slice(0, 3)).outerHTML,
                '%A': Translator.makePersistentTranslation(aDays[nDay]).outerHTML,
                '%b': Translator.makePersistentTranslation(aMonths[nMonth].slice(0, 3)).outerHTML,
                '%B': Translator.makePersistentTranslation(aMonths[nMonth]).outerHTML,
                '%c': date.toUTCString(),
                '%C': Math.floor(nYear / 100),
                '%d': zeroPad(nDate, 2),
                '%e': nDate,
                '%f': zeroPad(date.getTime() % 1000, 4),
                '%F': date.toISOString().slice(0, 10),
                '%G': getThursday().getFullYear(),
                '%g': ('' + getThursday().getFullYear()).slice(2),
                '%H': zeroPad(nHour, 2),
                '%I': zeroPad((nHour + 11) % 12 + 1, 2),
                '%j': zeroPad(aDayCount[nMonth] + nDate + ((nMonth > 1 && isLeapYear()) ? 1 : 0), 3),
                '%k': '' + nHour,
                '%l': (nHour + 11) % 12 + 1,
                '%m': zeroPad(nMonth + 1, 2),
                '%M': zeroPad(date.getMinutes(), 2),
                '%p': (nHour < 12) ? 'AM' : 'PM',
                '%P': (nHour < 12) ? 'am' : 'pm',
                '%s': Math.round(date.getTime() / 1000),
                '%S': zeroPad(date.getSeconds(), 2),
                '%u': nDay || 7,
                '%V': (function () {
                    let target = getThursday(),
                        n1stThu = target.valueOf();
                    target.setMonth(0, 1);
                    let nJan1 = target.getDay();
                    if (nJan1 !== 4) target.setMonth(0, 1 + ((4 - nJan1) + 7) % 7);
                    return zeroPad(1 + Math.ceil((n1stThu - target) / 604800000), 2);
                })(),
                '%w': '' + nDay,
                '%x': date.toLocaleDateString(),
                '%X': date.toLocaleTimeString(),
                '%y': ('' + nYear).slice(2),
                '%Y': nYear,
                '%z': date.toTimeString().replace(/.+GMT([+-]\d+).+/, '$1'),
                '%Z': date.toTimeString().replace(/.+\((.+?)\)$/, '$1')
            }[sMatch] || sMatch;
        });
    }

    static cloneJson(obj) {
        // https://stackoverflow.com/questions/4120475/how-to-create-and-clone-a-json-object/17502990#17502990
        let i;

        // basic type deep copy
        if (Helper.isNull(obj) || typeof obj !== 'object') {
            return obj
        }
        // array deep copy
        if (obj instanceof Array) {
            let cloneA = [];
            for (i = 0; i < obj.length; ++i) {
                cloneA[i] = Helper.cloneJson(obj[i]);
            }
            return cloneA;
        }
        if (obj instanceof Date)
        {
            return new Date(obj.getTime());
        }
        // object deep copy
        let cloneO = {};
        for (i in obj) {
            cloneO[i] = Helper.cloneJson(obj[i]);
        }
        return cloneO;
    }

    static htmlspecialcharsDecode(text) {
        const map = {
            '&amp;': '&',
            '&#038;': "&",
            '&lt;': '<',
            '&gt;': '>',
            '&quot;': '"',
            '&#039;': "'",
            '&#8217;': "’",
            '&#8216;': "‘",
            '&#8211;': "–",
            '&#8212;': "—",
            '&#8230;': "…",
            '&#8221;': '”'
        };

        if (Helper.isNotNull(text) && typeof text.replace === "function") {
            return text.replace(/\&[\w\d\#]{2,5}\;/g, function (m) {
                return map[m];
            });
        }
        return text;
    }

    static formDataFromObject(obj) {
        let formData = new FormData();
        for (let k in obj) {
            formData.set(k, obj[k]);
        }
        return formData;
    }

    static scaleContentRecursive(element, content) {

        let elementStyle = window.getComputedStyle(element);
        let contentStyle = window.getComputedStyle(content);

        if (contentStyle.height > elementStyle.height || contentStyle.width > elementStyle.width) {
            return Helper.scaleDownContentRecursive(element, content);
        }
    }

    static scaleDownContentRecursive(element, content) {
        Helper.convertChildrenToRelativeRecursive(element);

        let elementStyle = window.getComputedStyle(element);
        let contentStyle = window.getComputedStyle(content);

        let runs = 0;
        let fontSize = parseFloat(contentStyle.getPropertyValue("font-size"));
        let width = contentStyle.width;
        let height = contentStyle.height;
        while (contentStyle.height > elementStyle.height || contentStyle.width > elementStyle.width) {
            fontSize *= 0.95;

            if (height > elementStyle.height) {
                height *= 0.95;
            }
            if (width > contentStyle.width) {
                width *= 0.95;
            }
            content.style["font-size"] = fontSize + "px";
            content.style["max-height"] = height + "px";
            content.style["max-width"] = width + "px";

            runs++;
            if (runs > 2000) {
                console.log("breaked");
                break;
            }
        }
        Helper.convertToRelative(content);

        contentStyle = window.getComputedStyle(content);
        content.style["font-size"] = (parseFloat(contentStyle.getPropertyValue("font-size")) / parseFloat(document.documentElement.clientHeight) * 100) + "vh";
    }

    static convertChildrenToRelativeRecursive(element) {
        let children = element.childNodes;
        for (let i = 0, n = children.length; i < n; i++) {
            if (children[i] instanceof Element) {
                Helper.convertToRelative(children[i]);
                Helper.convertChildrenToRelativeRecursive(children[i]);
            }
        }
    }

    static convertToRelative(element) {
        let hasTransitionClass = (element.classList.contains("no-transtition"));

        element.classList.add("no-transition");

        let parent = element.parentNode;

        console.log(element);
        let elementStyle = window.getComputedStyle(element);
        let parentStyle = window.getComputedStyle(parent);

        let fontSize = parseFloat(elementStyle.getPropertyValue("font-size")) / parseFloat(parentStyle.getPropertyValue("font-size"));

        let maxHeight = elementStyle.height;
        let maxWidth = elementStyle.width;

        let pHeight = parentStyle.height;
        let pWidth = parentStyle.width;

        let relativeAttributes = element.style;
        relativeAttributes['max-height'] = Math.floor(maxHeight / pHeight * 100) + "%";
        relativeAttributes['margin-left'] = Math.floor(parseFloat(elementStyle.getPropertyValue('margin-left')) / pWidth * 100) + "%";
        relativeAttributes['margin-right'] = Math.floor(parseFloat(elementStyle.getPropertyValue('margin-right')) / pWidth * 100) + "%";
        relativeAttributes['margin-top'] = Math.floor(parseFloat(elementStyle.getPropertyValue('margin-top')) / pHeight * 100) + "%";
        relativeAttributes['margin-bottom'] = Math.floor(parseFloat(elementStyle.getPropertyValue('margin-bottom')) / pHeight * 100) + "%";
        relativeAttributes['max-width'] = Math.floor(maxWidth / pWidth * 100) + "%";
        relativeAttributes["font-size"] = fontSize + "em";
        // console.log(relativeAttributes);
        // element.css(relativeAttributes);

        if (!hasTransitionClass) {
            element.classList.remove("no-transition");
        }
    }

    static isChrome() {
        let isChromium = window.chrome,
            winNav = window.navigator,
            vendorName = winNav.vendor,
            isOpera = winNav.userAgent.indexOf("OPR") > -1,
            isIEedge = winNav.userAgent.indexOf("Edge") > -1,
            isIOSChrome = winNav.userAgent.match("CriOS");

        if (isIOSChrome) {
            return true;
        } else {
            return isChromium !== null &&
                typeof isChromium !== "undefined" &&
                vendorName === "Google Inc." &&
                isOpera === false &&
                isIEedge === false;
        }
    }

    static getIndexedObject(array, keyValue) {
        let obj = {};
        for (let i = 0, n = array.length; i < n; i++) {
            obj[array[i][keyValue]] = array[i];
        }
        return obj;
    }

    static invertKeyValues(obj) {
        let new_obj = {};

        for (let prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                new_obj[obj[prop]] = prop;
            }
        }

        return new_obj;
    }

    static toArray(object) {
        let res = [];
        for (let k in object) {
            res.push(object[k]);
        }
        return res;
    }
}
Helper.init();

class SystemSettings {
    static setBasePath(basePath) {
        SystemSettings._basePath = basePath;
    }

    static getBasePath() {
        return SystemSettings._basePath;
    }

    static set(key, value) {
        SystemSettings._settings[key] = value;
    }

    static get(key, defaultValue) {
        return Helper.nonNull(SystemSettings._settings[key], defaultValue);
    }

    static has(key){
        return Helper.nonNull(SystemSettings._settings[key]);
    }
}

SystemSettings.setBasePath("/");
SystemSettings._settings = {};

class ThemeManager {
    static init() {
        ThemeManager.loadCurrentTheme();
    }

    static changeCurrentTheme(newTheme) {
        let theme = null;
        if (typeof newTheme === 'string') {
            let themes = ThemeManager.themes.filter(function (theme) {
                return theme._name === newTheme;
            });
            if (themes.length > 0) {
                theme = themes[0];
            }
        }
        else if (ThemeManager.themes.indexOf(newTheme) !== -1) {
            theme = newTheme;
        }
        if (Helper.isNotNull(theme)) {
            localStorage.setItem("currentTheme", theme._name);
            let themePromise = new Promise(function (resolve) {
                document.querySelector("nav.top-bar").addEventListener("transitionend", function(){
                    resolve();
                });
            });
            document.body.className = theme._className;
            ThemeManager.currentTheme = theme;
            for (let i = 0, n = ThemeManager.changeListeners.length; i < n; i++) {
                ThemeManager.changeListeners[i](ThemeManager.currentTheme, themePromise);
            }
        }
    }

    static addTheme(theme) {
        ThemeManager.themes.push(theme);
    }

    static loadCurrentTheme() {
        ThemeManager.changeCurrentTheme(localStorage.getItem("currentTheme"));
        if (Helper.isNull(ThemeManager.currentTheme)) {
            let className = document.body.className;
            let themes = ThemeManager.themes.filter(function (theme) {
                return theme._className === className;
            });
            if (themes.length > 0) {
                ThemeManager.changeCurrentTheme(themes[0]);
            }
            else if (ThemeManager.themes.length > 0) {
                ThemeManager.changeCurrentTheme(ThemeManager.themes[0]);
            }
        }
    }

    static generateChangeThemeMenuAction() {
        return new MenuAction(ThemeManager.currentTheme._name, function (action) {
            let currentThemeIndex = ThemeManager.themes.indexOf(ThemeManager.currentTheme);
            let nextIndex = (currentThemeIndex + 1) % ThemeManager.themes.length;
            ThemeManager.changeCurrentTheme(ThemeManager.themes[nextIndex]);
            action.title = ThemeManager.currentTheme._name;
            action._menu.redraw();
        }, Menu.SHOW_ALWAYS)
    }

    static addChangeListener(listener) {
        ThemeManager.changeListeners.push(listener);
    }

    static getCurrentTheme(){
        return ThemeManager.currentTheme;
    }
}

ThemeManager.currentTheme = null;
ThemeManager.themes = [];
ThemeManager.changeListeners = [];

AndroidBridge.addDefinition("ThemeManager", {
    "addChangeListener": ThemeManager.addChangeListener,
    "getCurrentTheme": ThemeManager.getCurrentTheme,
});

class CookieCompliance {

    static async showIfNeeded(cookieContainer) {
        let cookieCompliance = new CookieCompliance(cookieContainer);
        return cookieCompliance.showIfNeeded();
    }

    constructor(cookieContainerId) {
        this.cookieContainerId = cookieContainerId;
        this.dropCookie = true;
        this.cookieDuration = 365 * 10;
        this.cookieName = 'complianceCookie';
        this.cookieValue = 'true';
    }

    async showIfNeeded() {
        if (CookieCompliance.checkCookie(this.cookieName) !== this.cookieValue) {
            return this.show();
        }
        return Promise.resolve();
    }

    removeMe() {
        this.createCookie(this.cookieName, this.cookieValue, this.cookieDuration);
    }

    createCookie(name, value, days) {
        let expires;
        if (Helper.isNotNull(days)) {
            const date = new Date();
            date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
            expires = "; expires=" + date.toGMTString();
        }
        else {
            expires = "";
        }
        if (this.dropCookie) {
            document.cookie = name + "=" + value + expires + "; path=/";
        }
    }

    eraseCookie(name) {
        this.createCookie(name, "", -1);
    }

    static checkCookie(name) {
        const nameEQ = name + "=";
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            let c = cookies[i];
            while (c.charAt(0) === ' ') {
                c = c.substring(1, c.length);
            }
            if (c.indexOf(nameEQ) === 0) {
                return c.substring(nameEQ.length, c.length);
            }
        }
        return null;
    }

    show() {
        let cookieCompliance = this;
        const cookieMessage = document.getElementById(this.cookieContainerId);
        cookieMessage.style.display = 'block';
        return new Promise(r => {
            cookieMessage.querySelector("#close-cookie-msg").onclick = function () {
                cookieCompliance.removeMe();
                cookieMessage.remove();
                r();
            };
        });
    }
}

class ActionBarMenu extends Menu {
    static init() {
        function parseStyleToObject(str) {
            let styleObject = {};

            if (typeof str !== 'string') {
                return styleObject;
            }

            str = str.trim().slice(1, -1); // browsers re-quote string style values

            if (!str) {
                return styleObject;
            }

            styleObject = str.split('&').reduce(function (ret, param) {
                const parts = param.replace(/\+/g, ' ').split('=');
                let key = parts[0];
                let val = parts[1];
                key = decodeURIComponent(key);

                // missing `=` should be `null`:
                // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
                val = val === undefined ? null : decodeURIComponent(val);

                if (!ret.hasOwnProperty(key)) {
                    ret[key] = val;
                } else if (Array.isArray(ret[key])) {
                    ret[key].push(val);
                } else {
                    ret[key] = [ret[key], val];
                }
                return ret;
            }, {});

            return styleObject;
        }

        let cssStyle = document.getElementsByClassName('foundation-mq');
        if (cssStyle.length === 0) {
            return;
        }
        let queries = [];
        cssStyle = parseStyleToObject(window.getComputedStyle(cssStyle[0]).getPropertyValue('font-family'));
        for (let key in cssStyle) {
            if (cssStyle.hasOwnProperty(key)) {
                queries.push({
                    _name: key,
                    value: 'only screen and (min-width: ' + cssStyle[key] + ')'
                });
            }
        }

        window.addEventListener('resize', function () {
            if (Helper.isNotNull(ActionBarMenu.currentMenu)) {
                ActionBarMenu.currentMenu.updateToggleButton();
            }
        });
        let responsiveMenu = document.getElementById("responsive-menu");
        document.getElementById("responsive-menu-toggle").onclick = function () {
            if (window.getComputedStyle(responsiveMenu).getPropertyValue('display') === 'none') {
                responsiveMenu.style.display = 'block';
            }
            else if (Helper.isNotNull(ActionBarMenu.currentMenu)) {
                ActionBarMenu.currentMenu.close();

            }
        };
        responsiveMenu.firstElementChild.addEventListener("click", function (e) {
                if (e.target === responsiveMenu.firstElementChild && Helper.isNotNull(ActionBarMenu.currentMenu)) {
                    ActionBarMenu.currentMenu.close();
                }
            }
        );
        ActionBarMenu.queries = queries;
    }

    static _getCurrentSize() {
        let matched;

        for (let i = 0; i < ActionBarMenu.queries.length; i++) {
            let query = ActionBarMenu.queries[i];

            if (matchMedia(query.value).matches) {
                matched = query;
            }
        }

        if (typeof matched === 'object') {
            return matched._name;
        } else {
            return matched;
        }
    }

    static filterVisibleElements(elements) {
        let visibleElements = [];
        for (let i = 0, n = elements.length; i < n; i++) {
            if (!elements[i].classList.contains("hidden")) {
                visibleElements.push(elements[i]);
            }
        }
        return visibleElements;
    }

    renderLiElement(aElement, action) {
        let liElement = super.renderLiElement(aElement, action);
        liElement.classList.add(action.getShowFor());
        return liElement;
    }

    updateToggleButton() {
        let size = ActionBarMenu._getCurrentSize();
        let firstParentElement = this.parentElements[0];
        if ((size === "medium" || size === "smedium" || size === "small") && ActionBarMenu.filterVisibleElements(firstParentElement.getElementsByClassName(Menu.SHOW_FOR_LARGE)).length > 0 ||
            (size === "smedium" || size === "small") && ActionBarMenu.filterVisibleElements(firstParentElement.getElementsByClassName(Menu.SHOW_FOR_MEDIUM)).length > 0 ||
            (size === "small") && ActionBarMenu.filterVisibleElements(firstParentElement.getElementsByClassName(Menu.SHOW_FOR_SMEDIUM)).length > 0 ||
            ActionBarMenu.filterVisibleElements(firstParentElement.getElementsByClassName(Menu.SHOW_NEVER)).length > 0) {
            document.getElementById("responsive-menu-toggle").style.display = 'block';
        } else {
            document.getElementById("responsive-menu-toggle").style.display = 'none';
            if (Helper.isNotNull(ActionBarMenu.currentMenu)) {
                ActionBarMenu.currentMenu.close();
            }
        }
    }

    _getOnClickListener() {
        let superListener = super._getOnClickListener();
        return function (event) {
            let action = superListener(event);
            if (!(action instanceof OpenSubmenuAction) && Helper.isNotNull(ActionBarMenu.currentMenu)) {
                ActionBarMenu.currentMenu.close();
            }
        }
    }

    draw(parentElement) {
        let returnValue = super.draw(parentElement);
        this.updateToggleButton();

        ActionBarMenu.currentMenu = this;
        return returnValue;
    }

    close() {
        document.getElementById("responsive-menu").style.display = 'none';
        for (let i = 0, n = this.submenus.length; i < n; i++) {
            this.submenus[i].close();
        }
    }

    removeAction(action) {
        let res = super.removeAction(action);
        this.updateToggleButton();
        return res;
    }
}

ActionBarMenu.queries = [];
ActionBarMenu.currentMenu = null;
ActionBarMenu.init();

class ViewInflater {
    static inflate(viewUrl, parentUrls) {
        parentUrls = Helper.nonNull(parentUrls, []).slice(0);

        let resultPromise = Promise.resolve();
        if (viewUrl instanceof Element) {
            resultPromise = Promise.resolve(viewUrl);
        }
        else {
            if (parentUrls.indexOf(viewUrl) !== -1) {
                return Promise.reject("views are in a circuit! cannot resolve view for url " + parentUrls[0] + "! url " + viewUrl + " is in stack before!");
            }
            parentUrls.push(viewUrl);
            resultPromise = fetch(Helper.basePath(viewUrl), {credentials: "same-origin"}).then(function (result) {
                return result.text();
            }).then(function (htmlText) {
                let doc = (new DOMParser()).parseFromString(htmlText, "text/html");
                if (Helper.isNull(doc)) {
                    doc = document.implementation.createHTMLDocument('');
                    doc.body.innerHTML = htmlText;
                }
                return doc.body.firstChild
            });
        }

        return resultPromise.then(function (parentElement) {
            let promises = [];
            let childViews = parentElement.querySelectorAll("[data-view]");

            for (let i = 0, n = childViews.length; i < n; i++) {
                promises.push(ViewInflater.inflate(childViews[i].dataset["view"], parentUrls).then(function (element) {
                    childViews[i].replaceWith(element);
                }));
            }
            return Promise.all(promises).then(function () {
                return parentElement;
            });
        });
    }
}

class Context {
    constructor(view) {
        let self = this;
        this._siteContent = null;
        this.firstStart = true;
        this.inflatePromise = new Promise(function (resolver) {
            self.inflatePromiseResolver = resolver;
        });
        this.fragments = {};

        if (Helper.isNotNull(view)) {
            this.inflateView(view);
        }
    }

    onConstruct() {
        let results = [];
        for (let k in this.fragments) {
            results.push(this.fragments[k].onConstruct.apply(this.fragments[k], arguments));
            results.push(this.fragments[k].inflatePromise);
        }
        return Promise.all(results);
    }

    onStart() {
        if (this.firstStart) {
            this.onFirstStart();
            this.firstStart = false;
        }

        for (let k in this.fragments) {
            let fragment = this.fragments[k];
            fragment.onStart.apply(this.fragments[k], arguments);
            this.fragments[k].inflatePromise.then(function (fragmentView) {
                if (fragment.isActive()) {
                    fragmentView.classList.remove("hidden");
                }
                else {
                    fragmentView.classList.add("hidden");
                }
            });
        }
    }

    onFirstStart() {
        // for (let k in this.fragments) {
        //     this.fragments[k].onFirstStart.apply(this.fragments[k], arguments);
        // }
    }

    onPause() {
        for (let k in this.fragments) {
            this.fragments[k].onPause.apply(this.fragments[k], arguments);
        }
    }

    onDestroy() {
        for (let k in this.fragments) {
            this.fragments[k].onDestroy.apply(this.fragments[k], arguments);
        }
    }

    addFragment(viewQuery, fragment) {
        this.fragments[viewQuery] = fragment;
        this.inflatePromise = this.inflatePromise.then(function (siteContent) {
            return fragment.inflatePromise.then(function (fragmentView) {
                siteContent.querySelector(viewQuery).appendChild(fragmentView);
                return siteContent;
            });
        });
    }

    /** @protected */
    inflateView(link) {
        let self = this;
        this.inflatePromiseResolver(ViewInflater.inflate(link).then(function (siteContent) {
            self._siteContent = siteContent;
            return siteContent;
        }));

        return this.inflatePromise;
    }

    findBy(query, all, asPromise) {
        all = Helper.nonNull(all, false);
        asPromise = Helper.nonNull(asPromise, false);

        let getVal = function (root) {
            let res = null;
            if (all) {
                res = root.querySelectorAll(query);
                if (root.matches(query)) {
                    res.push(root);
                }
            }
            else {
                if (root.matches(query)) {
                    res = root;
                }
                else {
                    res = root.querySelector(query);
                }
            }
            return res;
        };

        if (asPromise) {
            return this.inflatePromise.then(function (rootView) {
                return getVal(rootView);
            });
        }
        return getVal(this._siteContent);
    }
}

class AbstractSite extends Context {
    constructor(siteManager, view, deepLink) {
        super(view);

        this.isVisible = false;
        this.siteManager = siteManager;
        this.isFinishing = false;
        this.actionMenu = null;
        this.url = "";
        this.deepLink = deepLink;
        this.startArgs = {};
        this.title = siteManager.getDefaultTitle();
    }

    setTitle(titleElement, title) {
        if (typeof titleElement === "string") {
            title = titleElement;
            titleElement = document.createTextNode(titleElement);
        }
        this.title = {
            element: titleElement
        };
        this.title["title"] = Helper.nonNull(title, this.title["title"]);

        if (this.isVisible) {
            this.siteManager.updateTitle();
        }
    }

    startStartsite() {
        return this.startSite(this.siteManager.getStartSiteName());
    }

    inflateView(link) {
        let self = this;
        return super.inflateView(link).then(function (res) {
            let promises = [];
            for (let i = 0, n = self.fragments.length; i < n; i++) {
                promises.push(self.fragments[i].inflatePromise);
            }
            return Promise.all(promises).then(function () {
                return res;
            });
        });
    }

    onConstruct(args) {
        this.startArgs = args;
        if (Helper.isNotNull(this.deepLink)) {
            this.setUrlFromParams(args);
        }
        return super.onConstruct(args);
    }

    onStart(args) {
        this.isVisible = true;
        let res = super.onStart(args);
        this.actionMenu.redraw();
        return res;
    }

    onPause(args) {
        super.onPause(args);
        this.isVisible = false;
    }

    finish(result) {
        if (!this.isFinishing) {
            this.isFinishing = true;
            this.siteManager.endSite(this, result);
        }
    }

    startSite(siteName, args) {
        return this.siteManager.startSite(siteName, args);
    }

    toForeground() {
        this.siteManager.toForeground(this);
    }

    finishAndStartNext(siteName, startParams, finishResult) {
        this.startSite(siteName, startParams);
        this.finish(finishResult);
    }

    createActionBarMenu(menu) {
        let defaultActions = this.siteManager.getDefaultActions();
        for (let i = 0, n = defaultActions.length; i < n; i++) {
            menu.addAction(defaultActions[i].copy());
        }
        return menu;
    }

    setUrl(url) {
        this.url = url;
        this.siteManager.updateUrl(this);
    }

    setUrlFromParams(params) {
        this.setUrl(this.deepLink + Helper.buildQuery(params));
    }

    updateUrlParams(params) {
        this.startArgs = Object.assign(this.startArgs, params);
        this.setUrlFromParams(this.startArgs);
    }

    getUrl() {
        return this.url;
    }

    getFullUrl() {
        return Helper.basePath(this.url);
    }

    onBackPressed() {
    }

    addListener(event, selector, listenerFunction) {
        this.siteManager.addListener(this, event, selector, listenerFunction);
    }

    addKeyListener(keycode, listenerFunction)
    {
        this.siteManager.addKeyListener(this, keycode, listenerFunction);
    }

    addKeyAndEventListener(keycode, event, selector, listenerFunction) {
        this.siteManager.addKeyAndEventListener(this, keycode, event, selector, listenerFunction);
    }
}

class SiteContainer {
    constructor(site, finishResolver) {
        this._site = site;
        this._siteContent = null;
        this._pauseParameters = {};
        this._startParameters = {};
        this._finishResolver = finishResolver;
    }
    getSite() {
        return this._site;
    }

    setSite(site) {
        if (site instanceof AbstractSite) {
            this._site = site;
        }
    }

    getSiteContent() {
        return this._siteContent;
    }

    setSiteContent(value) {
        this._siteContent = value;
    }

    getPauseParameters() {
        return this._pauseParameters;
    }

    setPauseParameters(value) {
        this._pauseParameters = value;
    }

    getStartParameters() {
        return this._startParameters;
    }

    setStartParameters(value) {
        this._startParameters = value;
    }

    getFinishResolver() {
        return this._finishResolver;
    }

    setFinishResolver(value) {
        this._finishResolver = value;
    }
}

class SiteManager {
    constructor(siteDivId, actionBarMenuSelector, app) {
        this.siteDiv = document.getElementById(siteDivId);
        this.siteContainerStack = [];
        this.currentSiteContainerToShow = null;
        this.actionBarMenuSelector = Helper.nonNull(actionBarMenuSelector, '.action-bar');

        this.siteStartingPromise = Promise.resolve();
        this.defaultActions = [];
        this.startSiteName = null;
        this.app = app;

        this.titleElement = document.querySelector(".top-bar-title");

        const defaultTitleElem = document.createElement("span");
        while (this.titleElement.childNodes.length > 0) {
            const child = this.titleElement.firstChild;
            child.remove();
            defaultTitleElem.appendChild(child);
        }

        this.defaultTitle = {
            element: defaultTitleElem,
            title: document.title
        };

        let siteManager = this;
        window.onpopstate = function (e) {
            if (siteManager.siteContainerStack.length >= 1) {
                let site = siteManager.siteContainerStack[siteManager.siteContainerStack.length - 1].getSite();
                if (site.onBackPressed() !== false) {
                    siteManager.endSite(site);
                }
            }
        };
    }

    getDefaultTitle() {
        return this.defaultTitle;
    }

    setStartSiteName(startSiteName) {
        this.startSiteName = startSiteName;
    }

    getStartSiteName() {
        return this.startSiteName;
    }

    addDefaultAction(action) {
        this.defaultActions.push(action);
    }

    getDefaultActions() {
        return this.defaultActions;
    }

    getCurrentSite(){
        if (Helper.isNotNull(this.currentSiteContainerToShow)){
            return this.currentSiteContainerToShow.getSite();
        }
        return null;
    }

    async findSite(filter){
        for (let i = this.siteContainerStack.length-1; i >= 0; i--) {
            if (await filter(this.siteContainerStack[i].getSite())){
                return this.siteContainerStack[i].getSite();
            }
        }
        return null;
    }

    async startSite(siteConstructor, paramsPromise) {
        if (!(siteConstructor.prototype instanceof AbstractSite)) {
            throw {
                "error": "wrong class given! Expected AbstractSite, given " + siteConstructor.name
            };
        }

        let site = new siteConstructor(this);
        let resolver = {};
        let finishPromise = new Promise(function (resolve, reject) {
            resolver.resolve = resolve;
            resolver.reject = reject;
        });
        let siteContainer = new SiteContainer(site, resolver);
        this.siteDiv.removeAllChildren().appendChild(Helper.createLoadingSymbol());

        this.siteStartingPromise = Promise.resolve(paramsPromise).then(async (params) => {
            siteContainer.setStartParameters(params);
            await Promise.all([site.onConstruct(params), site.inflatePromise]);

            site.actionMenu = site.createActionBarMenu(this.buildActionBarMenu());
            return this.show(siteContainer);
        }).catch((e) => {
            console.error("site start error for site ", siteConstructor.name, e);
        });

        return finishPromise;
    }

    endSite(site, result) {
        let manager = this;
        this.siteStartingPromise.then(function () {
            let index = manager.findContainerIndexBySite(site);
            let container = manager.siteContainerStack.splice(index, 1);
            container = container[0];

            let showSiteContainer = null;
            if (container === manager.currentSiteContainerToShow) {
                manager.currentSiteContainerToShow.getSite().onPause();
                manager.currentSiteContainerToShow = null;
                let newSiteContainerIndex = manager.siteContainerStack.length - 1;
                if (newSiteContainerIndex < 0) {

                    manager.showAppEndedMessage();
                    app.endApp();
                    // manager.startSite(manager.startSiteName);
                    return;
                }
                manager.siteDiv.removeAllChildren().appendChild(Helper.createLoadingSymbol());
                showSiteContainer = manager.siteContainerStack[newSiteContainerIndex];
            }
            container.getSite().onDestroy();

            Promise.resolve(result).then(function (resValue) {
                container.getFinishResolver().resolve(resValue);
                if (Helper.isNotNull(showSiteContainer)) {
                    manager.show(showSiteContainer);
                }
            });
        });
    }

    addListener(site, event, _selector, listener) {
        this.siteDiv.addEventListener(event, function (_event) {
            let _element = _event.target;
            if (site.isVisible && _element.matches(_selector)) {
                listener(_element, _event);
            }
        });
    }

    addKeyAndEventListener(site, keycode, event, selector, listener) {
        this.addListener(site, event, selector, listener);
        this.addKeyListener(site, keycode, listener);
    }

    addKeyListener(site, keycode, listener) {
        window.addEventListener("keydown", function (e) {
            if (site.isVisible && e.which === keycode) {
                listener(this, e);
            }
        });
    }

    toForeground(site) {
        let index = this.findContainerIndexBySite(site);
        let container = this.siteContainerStack.splice(index, 1);
        container = container[0];

        this.show(container);
    }

    refreshCurrentSite() {
        return this.show(this.currentSiteContainerToShow);
    }

    /** @private */
    show(siteContainer) {
        if (Helper.isNotNull(this.currentSiteContainerToShow)) {
            this.currentSiteContainerToShow.setPauseParameters(this.currentSiteContainerToShow.getSite().onPause());
            this.currentSiteContainerToShow.setSiteContent(this.siteDiv.innerHTML);
        }
        this.siteDiv.removeAllChildren().appendChild(Helper.createLoadingSymbol());

        let siteManager = this;
        this.currentSiteContainerToShow = siteContainer;
        if (-1 === this.siteContainerStack.indexOf(siteContainer)) {
            this.siteContainerStack.push(siteContainer);
        }

        return siteContainer.getSite().inflatePromise.then(function (data) {
            siteContainer.getSite().actionMenu.redraw();
            siteManager.siteDiv.removeAllChildren().appendChild(data);
            siteManager.updateTitle();
            Translator.getInstance().updateTranslations();
            return data;
        }).then(function (data) {
            siteContainer.getSite().onStart(siteContainer.getPauseParameters());
            history.pushState({
                'siteName': siteContainer.getSite().constructor.name,
                'siteData': data.outerHTML,
                'stackPosition': siteManager.siteContainerStack.length - 1
            }, siteContainer.getSite().constructor.name, siteContainer.getSite().getFullUrl());
        });
    }

    updateUrl(site) {
        if (Helper.isNotNull(this.currentSiteContainerToShow) && this.currentSiteContainerToShow.getSite() === site) {
            let self = this;
            history.replaceState({
                'siteName': site.constructor.name,
                'siteData': site._siteContent.outerHTML,
                'stackPosition': self.siteContainerStack.length - 1
            }, site.constructor.name, site.getFullUrl());
        }
    }

    getCurrentSite() {
        if (this.currentSiteContainerToShow != null)
            return this.currentSiteContainerToShow.getSite();
    }

    redrawCurrentActionBar() {
        if (this.currentSiteContainerToShow != null)
            this.currentSiteContainerToShow.getSite().actionMenu.redraw();
    }

    updateTitle() {
        let title = this.getCurrentSite().title;
        this.titleElement.removeAllChildren().appendChild(title.element);
        document.title = Helper.nonNull(title.title, this.defaultTitle.title);
    }

    /** @private */
    findContainerIndexBySite(site) {
        for (let i = 0, n = this.siteContainerStack.length; i < n; i++) {
            if (this.siteContainerStack[i].getSite() === site) {
                return i;
            }
        }
        return -1;
    }

    /** @private */
    findContainerBySite(site) {
        let index = this.findContainerIndexBySite(site);
        if (index === -1) {
            return null;
        }
        return this.siteContainerStack[index];
    }

    /** @private */
    showAppEndedMessage() {
        this.siteDiv.removeAllChildren().appendChild(Translator.makePersistentTranslation("The app has ended! Please close the window."));
    }

    /** @private */
    buildActionBarMenu() {
        return new ActionBarMenu(this.actionBarMenuSelector);
    }
}

class PauseSite extends AbstractSite {
    onConstruct(args) {
        let pausedElement = null;

        if (Helper.isSet(args, "url")) {
            pausedElement = args["url"];
        }
        else {
            pausedElement = document.createElement("div");
            pausedElement.innerHTML = "Paused...";
        }

        this.inflateView(pausedElement);
    }

    onPause() {
        console.log("onPause");
        for (let i = 0; i < PauseSite.onStartListeners.length; i++) {
            if (typeof PauseSite.onStartListeners[i] === "function") {
                PauseSite.onStartListeners[i]();
            }
        }
        return super.onPause();
    }

    onStart() {
        console.log("onStart");
        for (let i = 0; i < PauseSite.onPauseListeners.length; i++) {
            if (typeof PauseSite.onPauseListeners[i] === "function") {
                PauseSite.onPauseListeners[i]();
            }
        }
        return super.onPause();
    }
}

PauseSite.onPauseListeners = [];
PauseSite.onStartListeners = [];

class App {
    constructor() {
        this._siteManager = null;
        this._actionBarMenuSelector = '.action-bar';
        this._basePath = SystemSettings.getBasePath();
        this._siteContentId = 'site-content';
        this._deepLinks = new Map();
        this._defaultActions = [];
        this._addThemeAction = false;
        this._showCookieCompliance = true;
        this._startSite = null;
        this._appEndListener = () => {
            return this.startSite(this._startSite);
        };
    }

    getSiteManager()
    {
        return this._siteManager;
    }

    addDefaultAction(action) {
        this._defaultActions.push(action);
    }

    setAddThemeAction(addThemeAction) {
        this._addThemeAction = addThemeAction;
    }

    getSiteContentId() {
        return this._siteContentId;
    }

    setSiteContentId(value) {
        this._siteContentId = value;
    }

    getActionBarMenuSelector() {
        return this._actionBarMenuSelector;
    }

    setActionBarMenuSelector(value) {
        this._actionBarMenuSelector = value;
    }

    getBasePath() {
        return this._basePath;
    }

    setBasePath(value) {
        this._basePath = value;
    }

    addDeepLink(alias, site) {
        this._deepLinks.set(alias.toLowerCase(), site);
    }

    setShowCookieCompliance(cookieCompliance)
    {
        this._showCookieCompliance = cookieCompliance;
    }

    refreshCurrentSite()
    {
        this._siteManager.refreshCurrentSite();
    }

    pause(elementToShow){
        this.startSite(PauseSite, {"url": elementToShow});
    }

    resume(){
        const currentSite = this._siteManager.getCurrentSite();
        if (currentSite instanceof PauseSite)
        {
            currentSite.finish();
        }
    }

    _resolveDeepLink(deepLink) {
        deepLink = deepLink.toLowerCase();
        if (this._deepLinks.has(deepLink)) {
            return this._deepLinks.get(deepLink);
        }
        return null;
    }

    _getDeepLink() {
        let deepLink = "";
        if (window.location.pathname.search(this._basePath) === 0) {
            deepLink = window.location.pathname.substr(this._basePath.length).trim();
        }
        if (deepLink.charAt(0) === '/') {
            deepLink = deepLink.substr(1).trim(); 
        }
        if (deepLink.charAt(deepLink.length - 1) === '/') {
            deepLink = deepLink.substr(0, deepLink.length - 2).trim();
        }
        if (deepLink.length === 0 && window.location.hash) {
            deepLink = window.location.hash.substr(1).trim();
        }

        return this._resolveDeepLink(deepLink);
    }

    _addDeepLinksListener() {
        let app = this;
        let elements = document.getElementsByClassName("deep-link");
        for (let i = 0, n = elements.length; i < n; i++) {
            elements[i].addEventListener("click", function (e) {
                e.preventDefault();
                app._siteManager.startSite(Helper.nonNull(app._resolveDeepLink(this.dataset["siteName"]), app._startSite), App._extractParams(this.dataset["siteArgs"]));
                return true;
            });
        }
    }

    removeDefaultAction(action)
    {
        let index = this._defaultActions.indexOf(action);
        if (index >= 0)
        {
            this._defaultActions[index].remove(true);
            this._defaultActions.splice(index, 1);
        }
    }
    
    startSite(site, parameter)
    {
        return this._siteManager.startSite(site, parameter);
    }

    start(fallbackStartSite) {
        SystemSettings.setBasePath(this._basePath);
        let startSite = Helper.nonNull(this._getDeepLink(), fallbackStartSite);
        let startParams = App._getStartParams();
        this._startSite = fallbackStartSite;

        Translator.init();
        ThemeManager.init();
        if (this._addThemeAction) {
            this.addDefaultAction(ThemeManager.generateChangeThemeMenuAction());
        }
        this._siteManager = new SiteManager(this._siteContentId, this._actionBarMenuSelector);
        this._siteManager.defaultActions = this._defaultActions;
        this._siteManager.setStartSiteName(fallbackStartSite);
        this._siteManager.startSite(startSite, startParams);
        this._addDeepLinksListener();

        if (this._showCookieCompliance)
        {
            this._cookieClosePromise = CookieCompliance.showIfNeeded('cookie-compliance');
        }
    }

    getCurrentSite(){
        return this._siteManager.getCurrentSite();
    }

    async endApp(){
        if (typeof this._appEndListener === "function"){
            this._appEndListener();
        }
    }

    setAppEndListener(appEndListener){
        this._appEndListener = appEndListener;
    }

    async findSite(filter){
        return this._siteManager.findSite(filter);
    }

    static _extractParams(paramString) {
        if (Helper.isNull(paramString)) {
            return null;
        }
        let result = {}, tmp = [];
        let items = paramString.split("&");
        for (let index = 0; index < items.length; index++) {
            tmp = items[index].split("=");
            if (tmp[0].trim().length > 0) {
                result[tmp[0]] = decodeURIComponent(tmp[1]);
            }
        }
        return result;
    }

    static _getStartParams() {
        return App._extractParams(window.location.search.substr(1));
    }
}

class Dialog {
    constructor(content, title) {
        this.resolver = null;
        this.content = null;
        this.backgroundElement = null;
        this.cancelable = true;
        this.title = Helper.nonNull(title, "");
        this.translatable = true;
        this.additionalClasses = "";
        this.buttons = [];
        this.result = null;

        if (Helper.isNotNull(content)) {
            this.setContent(content);
        }
    }

    setTitle(title) {
        this.title = title;
        return this;
    }

    setTranslatable(translatable) {
        this.translatable = translatable;
    }

    setAdditionalClasses(classes) {
        this.additionalClasses = classes;
    }

    getTitle() {
        return this.title;
    }

    setCancelable(cancelable) {
        this.cancelable = (cancelable === true);
        return this;
    }

    async setContent(content) {
        this.contentPromise = Promise.resolve(content);
        this.content = await this.contentPromise;
        return this;
    }

    addButton(elementOrText, listenerOrResult, shouldClose) {
        shouldClose = Helper.nonNull(shouldClose, true);

        let button = null;
        if (typeof elementOrText === "string") {
            button = document.createElement("button");
            button.classList.add("button");
            button.classList.add("right");
            button.appendChild(Translator.makePersistentTranslation(elementOrText));
        }
        else {
            button = elementOrText;
        }

        let self = this;
        if (typeof listenerOrResult !== "function") {
            let result = listenerOrResult;
            listenerOrResult = function () {
                self.result = result;
            };
        }

        let callback = null;
        if (shouldClose) {
            callback = function (e) {
                if (Helper.isNotNull(listenerOrResult)) {
                    listenerOrResult(e);
                }
                self.close();
            };
        }
        else {
            callback = listenerOrResult;
        }

        if (Helper.isNotNull(callback)) {
            button.addEventListener("click", callback);
        }
        this.buttons.push(button);
    }

    async show() {

        let titleElement = document.createElement("span");
        titleElement.classList.add("title");
        if (this.translatable && this.title !== "") {
            titleElement.appendChild(Translator.makePersistentTranslation(this.title));
        }
        else {
            titleElement.innerHTML = this.title;
        }

        let titleBar = document.createElement("div");
        titleBar.appendChild(titleElement);

        let contentContainer = document.createElement("div");
        contentContainer.classList.add("content-container");

        let modalDialog = document.createElement("div");
        modalDialog.className = this.additionalClasses;
        modalDialog.classList.add("modal");
        modalDialog.appendChild(titleBar);
        modalDialog.appendChild(contentContainer);

        let buttonBar = document.createElement("div");
        buttonBar.classList.add("modal-button-container");

        for (let i = 0, n = this.buttons.length; i < n; i++) {
            buttonBar.appendChild(this.buttons[i]);
        }

        await this.contentPromise;
        if (!(this.content instanceof Node)) {
            this.content = (this.translatable) ? Translator.makePersistentTranslation(this.content) : document.createTextNode(this.content);
        }
        contentContainer.appendChild(this.content);

        this.backgroundElement = document.createElement("div");
        this.backgroundElement.classList.add("background");
        this.backgroundElement.appendChild(modalDialog);

        this.backgroundElement.querySelector(".modal").appendChild(buttonBar);
        this.backgroundElement.style.display = "block";

        let self = this;
        if (this.cancelable) {
            let closeButton = document.createElement("span");
            closeButton.classList.add("close");
            closeButton.innerHTML = "&times;";

            titleBar.appendChild(closeButton);
            closeButton.addEventListener("click", function () {
                self.close();
            });
            window.addEventListener("click", function (e) {
                if (e.target === self.backgroundElement) {
                    self.close();
                }
            });
        }

        document.body.appendChild(this.backgroundElement);
        Translator.getInstance().updateTranslations();

        return new Promise(function (resolve) {
            self.resolver = resolve;
        });
    }

    close() {
        if (Helper.isNotNull(this.backgroundElement)) {
            this.backgroundElement.style.display = "none";
            this.backgroundElement.remove();
            this.backgroundElement = null;
        }
        if (Helper.isNotNull(this.resolver)) {
            this.resolver(this.result);
        }
    }

    addDefaultButton(){
        this.addButton("confirm-button");
    }
}

class ConfirmDialog extends Dialog {
    constructor(content, title) {
        super(content, title);
    }

    async show() {
        this.addButton("confirm-button", true);
        this.addButton("cancel-button", false);

        return super.show();
    }


    close() {
        if (Helper.isNull(this.result))
        {
            this.result = false;
        }
        return super.close();
    }
}

class FlashMessenger {
    static deleteMessage(_idNumber, _delayInMilliSeconds) {
        _delayInMilliSeconds = Helper.nonNull(_delayInMilliSeconds, 0);
        setTimeout(function () {
            let elem = document.getElementById("flashMessage" + _idNumber);
            elem.fadeOut(.2).then(function () {
                elem.remove();
            });
        }, _delayInMilliSeconds);
    }

    static addMessage(messageType, messageText, timeToShow, translate){

        let translationArgs = null;
        if (Helper.isNull(messageText) || typeof messageText === "object")
        {
            translationArgs = messageText;
            messageText = messageType;
            messageType = FlashMessenger.MESSAGE_TYPE_SUCCESS;
            translate = true;
        }
        translate = Helper.nonNull(translate, false);

        let id = FlashMessenger.messageCount;

        let wrapper = document.createElement("div");

        let _flashMessage = document.createElement("div");
        _flashMessage.className = "flashMessage " + messageType;
        _flashMessage.id = "flashMessage" + id;
        _flashMessage.style.opacity = '0';
        _flashMessage.addEventListener("click", function () {
            FlashMessenger.deleteMessage(id);
        });
        _flashMessage.appendChild((translate) ? Translator.makePersistentTranslation(messageText, translationArgs, "span") : document.createTextNode(messageText));

        wrapper.appendChild(_flashMessage);
        document.getElementById("flashMessageContainer").appendChild(wrapper);
        _flashMessage.fadeIn();
        timeToShow = Helper.nonNull(timeToShow, FlashMessenger.defaultTimeToShow);
        if (timeToShow > 0) {
            FlashMessenger.deleteMessage(FlashMessenger.messageCount, timeToShow);
        }
        FlashMessenger.messageCount++;
    }
}

FlashMessenger.messageCount = 0;
FlashMessenger.defaultTimeToShow = 3500;
FlashMessenger.LENGTH_SHORT= 1000;

FlashMessenger.MESSAGE_TYPE_SUCCESS = 'success';
FlashMessenger.MESSAGE_TYPE_ERROR = 'error';
FlashMessenger.MESSAGE_TYPE_DEFAULT = 'default';
FlashMessenger.MESSAGE_TYPE_INFO = 'info';
FlashMessenger.MESSAGE_TYPE_WARNING = 'warning';

class MyDb {
    constructor(dbName, version) {
        let indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB;
        this._conn = indexedDB.open(dbName, version);

        let myDB = this;
        this._conn.onupgradeneeded = function (upgradeEvent) {
            myDB.upgrade(myDB._conn.result, upgradeEvent.oldVersion, upgradeEvent.newVersion, upgradeEvent);
        };
        this.queryPromise = new Promise(function (resolve) {
            myDB._conn.onsuccess = function (e) {
                myDB._db = myDB._conn.result;
                resolve(e);
            };
        });
    }

    openTransaction(name, transactionMode, callback) {
        let myDb = this;
        if (typeof transactionMode === 'function' && Helper.isNull(callback)) {
            callback = transactionMode;
            transactionMode = "read";
        }

        return this.queryPromise.then(function () {
            let res = null;
            try {
                res = myDb._conn.result.transaction(name, transactionMode);
            }
            catch (e) {
                console.warn(e);
                res = myDb._conn.result.transaction(name);
            }
            callback(res);
        });
    }

    openStore(name, transactionMode, callback) {
        if (typeof transactionMode === 'function' && Helper.isNull(callback)) {
            callback = transactionMode;
            transactionMode = "readonly";
        }
        return this.openTransaction(name, transactionMode, function (t) {
            callback(t.objectStore(name));
        });
    }

    saveObj(obj, objectStore) {
        let self = this;
        return new Promise(function (resolve) {
            self.openStore(objectStore, "readwrite", function (store) {
                let request = store.put(obj);
                request.onsuccess = resolve;
                request.onerror = function (e) {
                    throw {
                        "type": "indexed-db-error",
                        "event": e
                    }
                };
            });
        });
    }

    saveMany(manyObj, objectStore) {
        let self = this;
        return new Promise(function (resolve) {
            self.openStore(objectStore, "readwrite", function (store) {
                let promises = [];
                for (let i = 0, n = manyObj.length; i < n; i++) {
                    promises.push(new Promise(function (resolveInner) {
                        let request = store.put(manyObj[i]);
                        request.onsuccess = resolveInner;
                        request.onerror = function (e) {
                            throw {
                                "type": "indexed-db-error",
                                "event": e
                            }
                        };
                    }));
                }
                resolve(Promise.all(promises));
            });
        });
    }

    load(key, objectStore) {
        let self = this;
        return new Promise( (resolve, reject) => {
            self.openStore(objectStore, function (store) {
                let request = store.get(key);
                request.onsuccess = function (e) {
                    resolve(e.currentTarget.result);
                };
                request.onerror = function (e) {
                    console.warn(e);
                    throw {
                        "type": "indexed-db-load-error",
                        "event": e
                    }
                };
            }).catch(e => {
                console.warn(e);
                reject(e);
            });
        });
    }
    
    loadAll(objectStore, query, count)
    {
        let self = this;
        return new Promise((resolve, reject)  => {
            self.openStore(objectStore, function (store) {
                let request = store.getAll(query, count);
                request.onsuccess = function (e) {
                    resolve(e.currentTarget.result);
                };
                request.onerror = function (e) {
                    console.warn(e);
                    throw {
                        "type": "indexed-db-load-error",
                        "event": e
                    }
                };
            }).catch(e => {
                console.warn(e);
                reject(e);
            });
        });
    }

    loadMany(index, value, objectStore, limit, direction) {
        let self = this;
        return new Promise(function (resolve) {
            self.openStore(objectStore, function (store) {
                let indexRequest = store.index(index);
                indexRequest.onerror = function (e) {
                    throw {
                        "type": "indexed-db-index-error",
                        "event": e
                    }
                };
                let request = indexRequest.openCursor(value, direction);
                request.onerror = function (e) {
                    throw {
                        "type": "indexed-db-index-error",
                        "event": e
                    }
                };
                let objects = [];
                let numberResults = 0;
                request.onsuccess = function (e) {
                    let cursor = e.target.result;
                    if (cursor) {
                        objects.push(cursor.value);
                        numberResults++;
                        if (Helper.isNull(limit) || numberResults < limit) {
                            cursor.continue();
                            return;
                        }
                    }
                    resolve(objects);
                };
            });
        });
    }

    remove(id, objectStore) {
        let self = this;
        return new Promise(function (resolve) {
            self.openStore(objectStore, "readwrite", function (store) {
                let deleteRequest = store.delete(id);
                deleteRequest.onerror = function (e) {
                    throw {
                        "type": "indexed-db-delete-error",
                        "event": e
                    }
                };
                deleteRequest.onsuccess = function (e) {
                    resolve();
                };
            });
        });
    }

    removeMany(ids, objectStore) {
        let self = this;
        return new Promise(function (resolve) {
            self.openStore(objectStore, "readwrite", function (store) {
                let promises = [];
                for (let i = 0, n = ids.length; i < n; i++) {
                    let deleteRequest = store.delete(ids[i]);
                    deleteRequest.onerror = function (e) {
                        throw {
                            "type": "indexed-db-delete-error",
                            "event": e
                        }
                    };
                    promises.push(new Promise(function (resolve) {
                        deleteRequest.onsuccess = function () {
                            resolve();
                        };
                    }));
                }
                resolve(Promise.all(promises));
            });
        });
    }

    removeWithIndex(index, value, objectStore) {
        let self = this;
        return new Promise(function (resolve) {
            self.openStore(objectStore, "readwrite", function (store) {
                let indexRequest = store.index(index);
                indexRequest.onerror = function (e) {
                    throw {
                        "type": "indexed-db-index-error",
                        "event": e
                    }
                };
                let request = indexRequest.openCursor(value);
                request.onerror = function (e) {
                    throw {
                        "type": "indexed-db-index-error",
                        "event": e
                    }
                };
                request.onsuccess = function (e) {
                    let cursor = e.target.result;
                    if (cursor) {
                        cursor.delete();
                        cursor.continue();
                    }
                    else {
                        resolve();
                    }
                };
            });
        });
    }

    removeAll(objectStore){
        return new Promise((resolve) => {
            this.openStore(objectStore, "readwrite", (store) => {
                let req = store.clear();
                req.onerror = (e) => {
                    throw {
                        "type":"indexed-db-index-error",
                        "event": e
                    }
                };
                req.onsuccess = resolve;
            });
        })
    }

    upgrade(db) {
    };
}

class ScriptLoader {
    static loadScript(scriptSrc) {
        if (Helper.isNotNull(ScriptLoader.scriptPromises[scriptSrc])) {
            return ScriptLoader.scriptPromises[scriptSrc];
        }
        else {
            let scriptPromise = new Promise(function (resolve) {
                let script = document.createElement("script");
                script.src = Helper.basePath(scriptSrc);
                script.onload = resolve;

                document.body.appendChild(script);
            });
            ScriptLoader.scriptPromises[scriptSrc] = scriptPromise;
            return scriptPromise;
        }
    }
    static loadCss(cssFile, media){
        if (Helper.isNotNull(ScriptLoader.cssPromises[cssFile])) {
            return ScriptLoader.cssPromises[cssFile];
        }
        else {
            media = Helper.nonNull(media, "all");
            let cssPromise = new Promise(function (resolve) {
                let link = document.createElement("link");
                link.rel='stylesheet';
                link.type="text/css";
                link.href = Helper.basePath(cssFile);
                link.media = media;
                link.onload = resolve;

                document.head.appendChild(link);
            });
            ScriptLoader.cssPromises[cssFile] = cssPromise;
            return cssPromise;
        }
    }
}

ScriptLoader.scriptPromises = {};
ScriptLoader.cssPromises = {};

class ShareButton {
    constructor(deviceType, icon, callback, shouldLoadImg)
    {
        this._deviceType = deviceType;
        this._icon = icon;
        this._callback = callback;

        if (Helper.nonNull(shouldLoadImg, false)){
            this._icon = ViewInflater.inflate(this._icon);
        }
    }

    shouldShowFor(deviceType)
    {
        return (deviceType === (deviceType & this._deviceType))
    }

    getIcon()
    {
        return this._icon;
    }

    getCallback()
    {
        return this._callback;
    }
}
ShareButton.TYPE_DESKTOP = 1;
ShareButton.TYPE_MOBILE_APPLE = 2;
ShareButton.TYPE_MOBILE_LEFTOVER = 4;
ShareButton.TYPE_MOBILE = ShareButton.TYPE_MOBILE_APPLE+ShareButton.TYPE_MOBILE_LEFTOVER;
ShareButton.TYPE_ALL = ShareButton.TYPE_DESKTOP+ShareButton.TYPE_MOBILE;

AndroidBridge.addDefinition(() => {
    window["ShareButton"] = ShareButton;
    window["ShareButton"]["TYPE_ALL"] = ShareButton.TYPE_ALL;

});

class MultipleShareButton extends ShareButton{
    constructor(deviceType, icon, callbacks, shouldLoadImg)
    {
        if (Array.isArray(deviceType) && deviceType[0] instanceof ShareButton){
            let btn = deviceType[0];
            callbacks = deviceType;
            deviceType = btn._deviceType;
            icon = btn._icon;
            shouldLoadImg = Helper.nonNull(shouldLoadImg, icon);
        }

        super(deviceType, icon, function (link, element, event) {
            if (!Array.isArray(callbacks)){
                callbacks = [callbacks];
            }
            for (let i = 0; i < callbacks.length; i++) {
                if (callbacks[i] instanceof ShareButton){
                    callbacks[i].getCallback()(link, element, event);
                }
                else {
                    console.log(callbacks, i);
                    callbacks[i](link, element, event);
                }
            }
        }, shouldLoadImg);
    }
}

class ShareManager {
    static init() {
        ShareManager.shareButtons = [];
    }

    static addShareButton(shareButton) {
        ShareManager.shareButtons.push(shareButton);
    }

    static generateDefaultShareElement(shareUrl) {
        return ShareManager.generateShareElement(shareUrl, ShareManager.getDefaultGenerateCallback());
    }

    static generateDefaultShareElementForButtons(shareUrl, buttons) {
        return ShareManager.generateShareElementForButtons(shareUrl, buttons, ShareManager.getDefaultGenerateCallback());
    }

    static generateShareElement(shareUrl, generateCallback) {
        return ShareManager.generateShareElementForButtons(shareUrl, ShareManager.shareButtons, generateCallback);
    }

    static generateShareElementForButtons(shareUrl, buttons, generateCallback) {
        let shareButtonElement = document.createElement("div");
        let currentDeviceType = ShareManager.getCurrentDeviceType();
        for (let i = 0, n = buttons.length; i < n; i++) {
            if (buttons[i].shouldShowFor(currentDeviceType)) {
                let elem = generateCallback(buttons[i], shareUrl);
                elem.onclick = function (event) {
                    buttons[i].getCallback()(shareUrl, this, event);
                };
                shareButtonElement.appendChild(elem);
            }
        }
        return shareButtonElement;
    }

    static getCurrentDeviceType() {
        if (navigator.userAgent.match(/iPhone|iPad|iPod/i)) {
            return ShareButton.TYPE_MOBILE_APPLE;
        }
        else if ((navigator.userAgent.match(/Android|BlackBerry|Opera Mini|IEMobile/i) !== null || (typeof window.orientation !== "undefined"))) {
            return ShareButton.TYPE_MOBILE_LEFTOVER;
        }
        else {
            return ShareButton.TYPE_DESKTOP;
        }
    }

    static getDefaultGenerateCallback() {
        return function (button) {
            let linkElement = document.createElement("a");
            linkElement.classList.add("share-icon");
            let iconUrl = button.getIcon();
            if (typeof iconUrl === "string") {
                let iconElement = document.createElement("img");
                iconElement.src = Helper.basePath(button.getIcon());
                iconElement.classList.add("share-icon");
                linkElement.appendChild(iconElement);
            }
            else {
                Promise.resolve(iconUrl).then(elem => {
                    linkElement.appendChild(elem);
                });
            }

            return linkElement;
        }
    }
}

ShareManager.init();

AndroidBridge.addDefinition("ShareManager.addShareButton", ShareManager.addShareButton);

class SmsShareButton extends ShareButton
{
    constructor(icon, shouldLoadImg) {
        super(ShareButton.TYPE_MOBILE, icon, function (link) {
            let linkToOpen = "";
            if (ShareManager.getCurrentDeviceType() === ShareButton.TYPE_MOBILE_APPLE) {
                linkToOpen = "sms:&body="+encodeURIComponent(link);
            }
            else {
                linkToOpen = "sms:?&body=" + encodeURIComponent(link);
            }
            window.open(linkToOpen, '_blank', "noopener");
        }, shouldLoadImg);
    }
}

class TelegramShareButton extends ShareButton {
    constructor(icon, shouldLoadImg) {
        super(ShareButton.TYPE_ALL, icon, function (link) {
            let linkToOpen = "https://t.me/share/url?url="+encodeURIComponent(link);
            window.open(linkToOpen, '_blank', "noopener");
        }, shouldLoadImg);
    }
}

class WhatsappShareButton extends ShareButton {
    constructor(icon, shouldLoadImg) {
        super(ShareButton.TYPE_ALL, icon, function (link) {
            let linkToOpen = "";
            if (ShareManager.getCurrentDeviceType() === ShareButton.TYPE_DESKTOP) {
                linkToOpen = "https://web.whatsapp.com/send?text="+encodeURIComponent(link);
            }
            else {
                linkToOpen = "whatsapp://send?text=" + encodeURIComponent(link);
            }
            window.open(linkToOpen, '_blank', "noopener");
        }, shouldLoadImg);
    }
}

class Fragment extends Context
{
    constructor(site, view)
    {
        super(view);
        this.site = site;
        this.active = true;
    }

    getSite()
    {
        return this.site;
    }

    isActive()
    {
        return this.active;
    }
}

class Theme
{
    constructor(name, className, icon)
    {
        this._name = name;
        this._className = className;
        this._icon = icon;
    }
}

function applyPolyfills() {
    if (!String.prototype.format) {
        String.prototype["format"] = function (args) {
            return this.replace(/{(\d+)}/g, function (match, number) {
                return args[number] !== undefined
                    ? args[number]
                    : match
                    ;
            });
        };
    }
    Object["assign"] = Helper.nonNull(Object["assign"], function (base, obj) {
        base = Helper.nonNull(base, {});
        if (obj === null || typeof(obj) !== 'object' || 'isActiveClone' in obj)
            return base;

        // if (obj instanceof Date) {
        //     temp = new obj.constructor(); //or new Date(obj);
        // }
        // else {
        //     temp = obj.constructor();
        // }

        for (let key in obj) {
            if (Object.prototype.hasOwnProperty.call(obj, key)) {
                obj['isActiveClone'] = null;
                base[key] = obj[key];
                delete obj['isActiveClone'];
            }
        }

        return base;
    });

    if (typeof window !== 'undefined') {
        if (Helper.isNotNull(window["Node"]) && !window["Node"]["prototype"]["removeAllChildren"]) {
            Node.prototype["removeAllChildren"] = function () {
                while (this.firstChild) {
                    this.removeChild(this.firstChild);
                }
                return this;
            };
        }

        if (HTMLElement) {
            HTMLElement.prototype["fadeOut"] = Helper.nonNull(HTMLElement.prototype["fadeOut"],
                function (time, effect, delay) {
                    time = Helper.nonNull(time, 0.5);
                    effect = Helper.nonNull(effect, "ease-in-out");
                    delay = Helper.nonNull(delay, 0);
                    this.style.transition = "opacity " + time + "s " + effect + " " + delay + "s";
                    let elem = this;
                    let animPromise = new Promise(function (resolve) {
                        let transEndLis = function (e) {
                            elem.removeEventListener("transitionend", transEndLis);
                            elem.removeEventListener("transitioncancel", transCancelledLis);
                            elem.style.opacity = null;
                            elem.style.transition = null;
                            resolve(true, e);
                        };

                        let transCancelledLis = function (e) {
                            elem.removeEventListener("transitionend", transEndLis);
                            elem.removeEventListener("transitioncancel", transCancelledLis);
                            elem.style.opacity = null;
                            elem.style.transition = null;
                            resolve(false, e);
                        };
                        elem.addEventListener("transitionend", transEndLis);
                        elem.addEventListener("transitioncancel", transCancelledLis);
                        //Fallback
                        setTimeout(() => {
                            resolve(false);
                        }, (time + delay) * 1000);
                    });

                    //Nach Seitenneuzeichnen, damit chrome das immer macht (und FF auch)
                    requestAnimationFrame(function () {
                        requestAnimationFrame(function () {
                            elem.style.opacity = 0;
                        });
                    });
                    return animPromise
                });

            HTMLElement.prototype["fadeIn"] = Helper.nonNull(HTMLElement.prototype["fadeIn"], function (time, effect, delay) {
                time = Helper.nonNull(time, 0.5);
                effect = Helper.nonNull(effect, "ease-in-out");
                delay = Helper.nonNull(delay, 0);
                this.style.transition = "opacity " + time + "s " + effect + " " + delay + "s";

                let elem = this;
                let animPromise = new Promise(function (resolve) {
                    let transEndLis = function (e) {
                        elem.removeEventListener("transitionend", transEndLis);
                        elem.removeEventListener("transitioncancel", transCancelledLis);
                        elem.style.opacity = null;
                        elem.style.transition = null;
                        resolve(true, e);
                    };

                    let transCancelledLis = function (e) {
                        elem.removeEventListener("transitionend", transEndLis);
                        elem.removeEventListener("transitioncancel", transCancelledLis);
                        elem.style.opacity = null;
                        elem.style.transition = null;
                        resolve(false, e);
                    };
                    elem.addEventListener("transitionend", transEndLis);
                    elem.addEventListener("transitioncancel", transCancelledLis);

                    if (getComputedStyle(elem).getPropertyValue("opacity") === "1") {
                        resolve(false);
                    }
                    //Fallback
                    setTimeout(() => {
                        resolve(false);
                    }, (time + delay) * 1000);

                    //Nach Seitenneuzeichnen, damit chrome das immer macht (und FF auch)
                    requestAnimationFrame(function () {
                        requestAnimationFrame(function () {
                            elem.style.opacity = 1;
                        });
                    });
                });
                return animPromise;
            });
        }

        if (Node) {
            Node.prototype["replaceWith"] = Helper.nonNull(Node.prototype["replaceWith"], function (elem) {
                this.parentElement.replaceChild(elem, this);
            });
            Node.prototype["remove"] = Helper.nonNull(Node.prototype["remove"], function () {
                this.parentElement.removeChild(this);
            });
        }

        if (Element) {
            Element.prototype.matches = Helper.nonNull(Element.prototype.matches, Helper.nonNull(Element.prototype["matchesSelector"], Element.prototype["webkitMatchesSelector"]));

            window["Element"]["prototype"]["closest"] = Helper.nonNull(window["Element"]["prototype"]["getAll"], function (s) {
                // if (!Element.prototype.matches)
                //     Element.prototype.matches = Element.prototype.msMatchesSelector ||
                //         Element.prototype.webkitMatchesSelector;
                //
                // if (!Element.prototype.closest)
                //     Element.prototype.closest = function(s) {
                let el = this;
                if (!document.documentElement.contains(el)) return null;
                do {
                    if (el.matches(s)) return el;
                    el = el.parentElement;
                } while (el !== null);
                return null;
                // };
            });
        }

        window["IDBObjectStore"]["prototype"]["getAll"] = Helper.nonNull(window["IDBObjectStore"]["prototype"]["getAll"], function () {
            let res = {};
            let items = [];
            this.openCursor().onsuccess = function (e) {
                let cursor = e.target.result;
                if (Helper.isNotNull(cursor)) {
                    items.push(cursor.value);
                    cursor.continue();
                }
                else if (Helper.isNotNull(res.onsuccess)) {
                    res.onsuccess({currentTarget: {result: items}});
                }
            };
            return res;
        });
    }

    String.prototype.startsWith = Helper.nonNull(String.prototype.startsWith, function (searchString, position) {
        position = position || 0;
        return this.indexOf(searchString, position) === position;
    });

    String.prototype.includes = Helper.nonNull(String.prototype.includes, function (searchString) {
        return this.indexOf(searchString) >= 0;
    });

    String.prototype.endsWith = Helper.nonNull(String.prototype.endsWith, function (searchString, position) {
        var subjectString = this.toString();
        if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
            position = subjectString.length;
        }
        position -= searchString.length;
        var lastIndex = subjectString.indexOf(searchString, position);
        return lastIndex !== -1 && lastIndex === position;
    });


    window["fetch"] = Helper.nonNull(window["fetch"], function (url, params) {
        console.log("customFetch", url);
        let request = null;
        if (window.XMLHttpRequest) { // Mozilla, Safari, ...
            request = new XMLHttpRequest();
        } else if (window.ActiveXObject) { // IE
            try {
                request = new ActiveXObject('Msxml2.XMLHTTP');
            }
            catch (e) {
                try {
                    request = new ActiveXObject('Microsoft.XMLHTTP');
                }
                catch (e) {
                }
            }
        }

        let onloadPromise = new Promise((r) => {
            // this.result
            request.onload = r;
            request.onerror = function (err) {
                r(Promise.reject(err));
            };
        });

        let resultPromise = new Promise(function (resolve) {

            let response = {
                "json": function () {
                    request.send();

                    return onloadPromise.then(() => {
                        return JSON.parse(request.responseText)
                    });
                },
                "text": function () {
                    request.send();
                    return onloadPromise.then(() => request.responseText);
                },
                "arrayBuffer": () => {
                    request.responseType = "arraybuffer";
                    request.send();
                    return onloadPromise.then(() => request.response);
                }
            };
            resolve(response);
        });


        request.open('get', url, true);
        // request.send();
        return resultPromise;
    });
}

class Constants{}
Constants.SCRIPTS = {
    CKEDITOR:"version/2/ckeditor/ckeditor.js",
    LIST_JS: "version/1/listjs/list.min.js"
};

class DataManager {
    static async load(url, isCachable, raw) {
        isCachable = Helper.nonNull(isCachable, false);
        raw = Helper.nonNull(raw, false);
        let fullUrl = (isCachable) ? Helper.basePath(DataManager.cachePath + url) : Helper.basePath(DataManager.dataPath + url);

        return this._load(fullUrl, raw);
    }

    static async _load(url, raw) {
        return fetch(url, {"credentials": "same-origin"}).then(function (res) {
            if (raw) {
                return res.text();
            }
            return res.json();
        }).catch(function (e) {
            if (!raw) {
                return {
                    "success": false,
                    "errors": [
                        "not-online"
                    ]
                }
            }
        });
    }

    static async loadStatic(url, raw) {
        raw = Helper.nonNull(raw, false);
        let fullUrl = Helper.basePath(url);

        return this._load(fullUrl, raw);
    }

    static async send(url, params) {
        let fullUrl = Helper.basePath(DataManager.dataPath + url);

        if (!(params instanceof FormData)) {
            let newParams = new FormData();
            for (let k in params) {
                newParams.append(k, params[k]);
            }
            params = newParams;
        }

        return fetch(fullUrl, {
            "credentials": "same-origin",
            "method": "POST",
            "body": params
        }).then(function (res) {
            return res.json();
        }).catch(function (e) {
            console.error("error", e);
            return {
                "success": false,
                "errors": [
                    "not-online"
                ]
            }
        });
    }

    static buildQuery(values) {
        return Helper.buildQuery(values);
    }
}

DataManager.dataPath = "data/";
DataManager.cachePath = "cached/";

class Form {
    constructor(formElem, url, method, isCachable) {
        this.formElem = formElem;
        this.method = Helper.nonNull(method, Helper.nonNull(formElem["method"], "POST"));
        this.isCachable = (Helper.nonNull(isCachable, this.method.toLowerCase() === "get") === true);

        this.isBusy = false;

        if (typeof url === "string")
        {
            this.submitHandler = function(values){
                if (self.method.toLowerCase() === "get") {
                    return (DataManager.load(url + DataManager.buildQuery(values), self.isCachable));
                }
                else {
                    return (DataManager.send(url, values));
                }
            };
        }
        else {
            this.submitHandler = url;
        }

        let self = this;

        this.submitCallback = null;
        this.errorCallback = async function (errors) {
            await self.setErrors(errors);
        };

        formElem.addEventListener("submit", async function (e) {
            console.log("submitting!", e);
            e.preventDefault();
            await self.doSubmit(e);
        });

        for (let i = 0, n = formElem.elements.length; i < n; i++) {
            let elem = formElem.elements[i];
            elem.addEventListener("change", function () {
                if (this.value.trim() !== "") {
                    this.classList.add("notEmpty");
                }
                else {
                    this.classList.remove("notEmpty");
                }
                this.setCustomValidity("");
            });
            elem.addEventListener("keydown", function () {
                this.setCustomValidity("");
            });
        }
    }

    onError(errorHandler, ownHandlerForOptimisticLocking){
        ownHandlerForOptimisticLocking = Helper.nonNull(ownHandlerForOptimisticLocking, true);
        let callback = null;

        if (ownHandlerForOptimisticLocking){
            callback = function(errors){
                if (Array.isArray(errors) && errors.indexOf("optimistic-locking-exception") >= 0){
                    let dialog = new Dialog("optimistic-locking-dialog", "optimistic-locking-dialog-title");
                    dialog.addDefaultButton();
                    dialog.show();
                }
                else
                {
                    errorHandler(errors);
                }
            };
        }
        else
        {
            callback = errorHandler;
        }
        this.errorCallback = callback;
    }

    doSubmit() {
        if (!this.isBusy) {
            let self = this;
            return this.submit().then(function (res) {
                if (res["success"]) {
                    if (self.submitCallback !== null) {
                        return self.submitCallback(res["result"]);
                    }
                }
                else if (Helper.isNotNull(self.errorCallback)) {
                    return self.errorCallback(res["errors"]);
                }
            });
        }
    }

    load(url, isCached) {
        this.setValues(DataManager.load(url, isCached).then(function (values) {
            if (values["success"]) {
                return values["result"];
            }
            return {};
        }));
        return this;
    }

    setValues(valuePromise) {
        this.setIsBusy(true);

        let self = this;
        return Promise.resolve(valuePromise).then(function (values) {
            self.setIsBusy(false);
            for (let k in values) {
                if (Helper.isNotNull(self.formElem.elements[k])) {
                    if (Helper.isNotNull(self.formElem.elements[k].options) && Helper.isNotNull(values[k+"Options"]))
                    {
                        let options = self.formElem.elements[k].options;
                        for (let val in values[k+"Options"])
                        {
                            let option = document.createElement("option");
                            option.value = val;
                            option.innerText = values[k+"Options"][val];
                            options.add(option);
                        }
                    }

                    self.formElem.elements[k].value = Helper.htmlspecialcharsDecode(values[k]);
                    if (Helper.isNotNull(values[k]) && (""+values[k]).trim() !== "") {
                        self.formElem.elements[k].classList.add("notEmpty");
                    }
                    else {
                        self.formElem.elements[k].classList.remove("notEmpty");
                    }
                }
            }
            return self;
        });
    }

    async setErrors(errors) {
        let hasElem = false;
        let firstError = null;

        for (let k in errors) {
            if (Helper.isNotNull(this.formElem.elements[k]) && this.formElem.elements[k].type !== "hidden"
                && Helper.isNull(this.formElem.elements[k].readonly) && (
                    Helper.isNull(this.formElem.elements[k].disabled) || !this.formElem.elements[k].disabled)
            ) {
                this.formElem.elements[k].setCustomValidity(Translator.translate(Helper.nonNull(errors[k], "form-default-error")));
                hasElem = true;
            }
            if (Helper.isNull(firstError)) {
                firstError = Helper.nonNull(errors[k], "form-default-error");
            }
        }
        if (!hasElem && Helper.isNotNull(firstError)) {
            for (let k in this.formElem.elements) {
                if (this.formElem.elements[k].type !== "hidden") {
                    this.formElem.elements[k].setCustomValidity(Translator.translate(firstError));
                    hasElem = true;
                    break;
                }
            }
        }

        if (hasElem) {
            this.formElem.querySelector("input[type=submit]").click();
        }
    }

    setIsBusy(isBusy) {
        this.isBusy = isBusy;
        if (this.isBusy) {
            this.formElem.classList.add("sending");
        }
        else {
            this.formElem.classList.remove("sending");
        }
    }

    submit() {
        let self = this;
        return new Promise(function (resolve) {
            self.setIsBusy(true);
            let values = new FormData(self.formElem);
            resolve(self.submitHandler(values));
        }).then(function (data) {
            self.setIsBusy(false);
            return data;
        });
    }

    onSubmit(callback) {
        this.submitCallback = callback;
    }
}

class SettingsManager {
    static getInstance() {
        if (SettingsManager._instance === null) {
            SettingsManager._instance = new SettingsManager();
        }
        return SettingsManager._instance;
    }

    constructor() {
        this._settings = null;
        this._localStorageKey = "settings";
    }

    getSettings() {
        if (Helper.isNull(this._settings)) {
            this._loadSettings();
        }
        return this._settings;
    }

    getSetting(name, defaultValue) {
        const settings = this.getSettings();

        if (Helper.isNotNull(settings[name])) {
            return settings[name].value;
        }
        else {
            return defaultValue;
        }
    }

    deleteSetting(name) {
        this.getSettings();
        delete this._settings[name];
        this._saveSettings();
    }

    setSetting(name, value) {
        this.getSettings();
        this._settings[name] = {
            date: new Date().getTime(),
            value: value
        };
        this._saveSettings();
    }

    setSettings(settingsObject) {
        this.getSettings();
        for (const k in settingsObject) {
            this._settings[k] = settingsObject[k];
        }
        this._saveSettings();
    }

    hasSetting(name)
    {
        return Helper.nonNull(this._settings[name]);
    }

    _loadSettings() {
        this._settings = localStorage.getItem(this._localStorageKey);
        if (this._settings === null) {
            this._settings = {};
        }
        else {
            this._settings = JSON.parse(this._settings);
        }
    }

    _saveSettings() {
        if (this._settings !== null) {
            localStorage.setItem(this._localStorageKey, JSON.stringify(this._settings));
        }
    }
}

SettingsManager._instance = null;

class LocalStorageSettingsFragment extends Fragment {
    onFirstStart() {
        let res = super.onFirstStart();
        let settings = this.findBy(".setting", true);
        const settingsManager = SettingsManager.getInstance();

        for (let i = 0; i < settings.length; i++) {
            let setting = settings[i];
            const name = setting.name;
            let value;
            if (!setting["dataset"]["raw"]) {
                value = settingsManager.getSetting(name);
            } else {
                value = localStorage.getItem(name);
            }

            let isCheckable = false;
            if (setting instanceof HTMLInputElement && (setting.type === 'checkbox' || setting.type === 'radio')) {
                isCheckable = true;
            }
            if (((!setting["dataset"]["raw"] && !settingsManager.hasSetting(name)) || (setting["dataset"]["raw"] && value === null))
                && Helper.isNotNull(settings[i]["dataset"]["default"])) {
                value = setting["dataset"]["default"];
                if (Helper.isNotNull(setting["dataset"]["defaultTranslateable"])) {

                    setting["dataset"]["translation"] = "";
                    setting["dataset"]["translationValue"] = value;
                    value = Translator.translate(value);
                }
            }

            if (Helper.isNotNull(value)) {
                if (isCheckable) {
                    setting.checked = (value === setting.value);
                }
                else {
                    setting.value = value;
                }
                if (value !== "") {
                    setting.classList.add("notEmpty");
                }
            }

            setting.addEventListener("change", function () {
                let value = this.value;
                if (isCheckable && !this.checked) {
                    value = null;
                }
                if (!setting["dataset"]["raw"]) {
                    settingsManager.setSetting(name, value);
                } else {
                    localStorage.setItem(name, value);
                }
                delete setting["dataset"]["translationValue"];
                delete setting["dataset"]["translation"];
            });
        }
        return res;
    }

    onStart() {
        let res = super.onStart();
        let settings = this.findBy(".setting", true);
        const settingsManager = SettingsManager.getInstance();

        for (let i = 0; i < settings.length; i++) {
            let setting = settings[i];
            const name = setting.name;
            let value;
            if (!setting["dataset"]["raw"]) {
                value = settingsManager.getSetting(name);
            } else {
                value = localStorage.getItem(name);
            }

            let isCheckable = false;
            if (setting instanceof HTMLInputElement && (setting.type === 'checkbox' || setting.type === 'radio')) {
                isCheckable = true;
            }

            if (Helper.isNotNull(value)) {
                if (isCheckable) {
                    setting.checked = (value === setting.value);
                }
                else {
                    setting.value = value;
                }
                if (value !== "") {
                    setting.classList.add("notEmpty");
                }
            }
        }
        return res;
    }
}

class SmartColumn{
    constructor(name, label, translateable){
        this._name = name;
        this._label = label;
        this._translateable = Helper.nonNull(translateable, true);
        this._sortable = true;

        this._index = -1;

        this._clickListener = null;
    }

    setClickListener(listener)
    {
        this._clickListener = listener;
        return this;
    }

    setIndex(index)
    {
        this._index = index;
    }

    getName()
    {
        return this._name;
    }

    getLabel()
    {
        return this._label;
    }

    getHeadElement()
    {
        const headElement = document.createElement("th");
        headElement.appendChild((this._translateable)?Translator.makePersistentTranslation(this._label):document.createTextNode(this._label));

        if (this._sortable)
        {
            headElement.classList.add("sort");
            headElement["dataset"]["sort"] = this._name;
        }

        headElement["dataset"]["column"] = this._index;

        this._headElement = headElement;
        return this._headElement;
    }

    getValueName(){
        return this._name;
    }
    
    prepareData(myData, rowData)
    {
        return myData;
    }

    getItemElement(){
        const element = document.createElement("td");
        element.classList.add(this._name);
        element["dataset"]["column"] = this._index;

        if (Helper.isNotNull(this._clickListener))
        {
            element.classList.add("clickable");
        }

        return element;
    }

    click(tableCell, table, event){
        if (Helper.isNotNull(this._clickListener))
        {
            this._clickListener(tableCell, table, event);
        }
    }
}

class ConstSmartColumn extends SmartColumn{
    constructor(name, label, translatable, valueTranslatable) {
        super(name, label, translatable);
        this._sortable = false;
        this._valueTranslatable = Helper.nonNull(valueTranslatable, false);
    }

    getValueName() {
        return null;
    }

    getItemElement(){
        const element = super.getItemElement();
        element.classList.remove(this._name);
        element.appendChild((this._valueTranslatable)?Translator.makePersistentTranslation(this._name):document.createTextNode(this._name));
        return element;
    }
}

class DataSmartColumn extends SmartColumn{
    constructor(name, label, translateable) {
        translateable = Helper.nonNull(translateable, false);
        super(name, label, translateable);
    }

    getHeadElement() {
        return document.createTextNode("");
    }

    getValueName() {
        return {
            "data":[this._name]
        };
    }

    getItemElement() {
        return document.createTextNode("");
    }
}

class ImgConstSmartColumn extends ConstSmartColumn{
    constructor(name, label, translateable) {
        super(name, label, translateable);
        this._valueTranslatable = false;
    }

    getItemElement() {
        const element = super.getItemElement();
        const imgElement = document.createElement("img");
        imgElement["src"] = this._name;

        element.removeAllChildren().appendChild(imgElement);
        return element;
    }
}

class ListHelper {
    constructor(id, options, values) {
        this._tableElement = id;
        this._options = Helper.nonNull(options, {});
        this._values = values;

        if (typeof this._tableElement === "string") {
            this._tableElement = document.getElementById(this._tableElement);
        }

        this._columns = [];
        if (Array.isArray(options)) {
            this._columns = options;
        }
        else if (Helper.isNotNull(options["columns"])) {
            this._columns = options["columns"];
        }
    }

    prepareData(data) {
        console.log("prepareData", data);
        if (Helper.isNotNull(data)) {
            for (let i = 0, n = data.length; i < n; i++) {
                data[i] = this.prepareDataset(data[i]);
            }
        }
        return data;
    }

    prepareDataset(dataset) {
        console.log("prepareDataset", dataset);
        for (let i = 0, n = this._columns.length; i < n; i++) {
            if (Helper.isNotNull(dataset[this._columns[i].getName()])) {
                dataset[this._columns[i].getName()] = this._columns[i].prepareData(dataset[this._columns[i].getName()], dataset);
            }
        }
        return dataset;
    }

    createTable() {
        if (Helper.isNotNull(this._columns)) {
            this.updateColumns();
        }

        let id = this._tableElement;
        let options = this._options;
        let values = this._values;

        options["item"] = Helper.nonNull(options["item"], id["id"] + "-template-item");
        options["page"] = Helper.nonNull(options["page"], 5);
        options["pagination"] = Helper.nonNull(options["pagination"], {
            "outerWindow": 1,
            "innerWindow": 1
        });

        let template = document.getElementById(options["item"]);
        if (template) {
            options["item"] = template.outerHTML;
            template.remove();
        }

        values = this.prepareData(values);

        const list = new List(id, options, values);
        let self = this;
        id.querySelector("." + Helper.nonNull(options["listClass"], "list")).addEventListener("click", function (e) {
            let columnElem = e.target.closest("td[data-column]");
            const column = parseInt(columnElem["dataset"]["column"]);
            if (self._columns.length > column) {
                self._columns[column].click(columnElem, list, e);
            }
        });
        this.list = list;

        return list;
    }

    updateColumns() {
        const head = document.createElement("tr");
        const item = document.createElement("tr");
        const valueNames = [];

        for (let i = 0, n = this._columns.length; i < n; i++) {
            this._columns[i].setIndex(i);

            head.appendChild(this._columns[i].getHeadElement());
            item.appendChild(this._columns[i].getItemElement());

            const valueName = this._columns[i].getValueName();
            if (Helper.isNotNull(valueName)) {
                valueNames.push(valueName);
            }
        }
        const header = this._tableElement.querySelector("thead");
        const footer = this._tableElement.querySelector("tfoot");

        if (Helper.isNotNull(header)) {
            header.removeAllChildren().appendChild(head);
        }
        if (Helper.isNotNull(footer)) {
            footer.removeAllChildren().appendChild(Helper.cloneNode(head));
        }


        this._options["item"] = item.outerHTML;
        this._options["valueNames"] = valueNames;
    }

    getList() {
        return this.list;
    }

    updateItem(valueName, value, newValues) {
        const items = this.list.get(valueName, value);
        if (Helper.isNotNull(items) && items.length >= 1) {
            newValues = this.prepareDataset(newValues);
            items[0].values(newValues);
        }
    }

    setBusy(isBusy) {
        if (isBusy) {
            this._tableElement.classList.add("sending");
        }
        else {
            this._tableElement.classList.remove("sending");
        }
    }
}

class SettingsSite extends AbstractSite {
    constructor(siteManager) {
        super(siteManager, SettingsSite.template, "settings");
        for (let k in SettingsSite.settingsFragments) {
            this.addSettingsFragment(k, new SettingsSite.settingsFragments[k](this));
        }
        this.active = null;
    }

    addSettingsFragment(name, settingsFragment) {
        this.addFragment("#settings-fragments", settingsFragment);
        delete this.fragments["#settings-fragments"];
        this.fragments[name] = settingsFragment;
    }

    onStart() {
        let res = super.onStart();
        if (Helper.isNotNull(this.active) && !this.fragments[this.active].isActive()) {
            this.setActive(null);
        }

        this.buildList();
        return res;
    }

    setActive(name) {
        if (Helper.isNotNull(this.active)) {
            this.fragments[this.active].inflatePromise.then(function (view) {
                view.classList.remove("active");
            });
            this.findBy("#show-fragment-" + this.active).classList.remove("active");
        }
        this.active = name;
        if (Helper.isNotNull(this.active)) {
            this.fragments[this.active].inflatePromise.then(function (view) {
                view.classList.add("active");
            });
            this.findBy("#show-fragment-" + this.active).classList.add("active");
        }
    }

    buildList() {
        let listNameElem = this.findBy("#settings-fragment-list");
        listNameElem.removeAllChildren();

        let self = this;
        for (let k in this.fragments) {
            if (this.fragments[k].isActive()) {

                let liElement = document.createElement("li");
                liElement.id = "show-fragment-" + k;
                liElement.appendChild(Translator.makePersistentTranslation(k, null, "a"));
                liElement.addEventListener("click", function () {
                    self.setActive(k);
                });
                listNameElem.appendChild(liElement);

                if (Helper.isNull(this.active)) {
                    this.setActive(k);
                }
            }
        }
    }

    static addSettingsFragment(name, settingsFragment) {
        SettingsSite.settingsFragments[name] = settingsFragment;
    }

    static setAddSettingsSite(addLink) {
        SettingsSite.shouldAddSettingsSite = addLink;
    }

    static setTemplate(template) {
        SettingsSite.template = template;
    }
}

SettingsSite.template = 'core/html/settings.html';
SettingsSite.settingsFragments = {};
SettingsSite.shouldAddSettingsSite = true;
SettingsSite.settingsAction = null;
SettingsSite.shouldAddSettingsAction = true;

InitPromise.addPromise(function (app) {
    if (SettingsSite.shouldAddSettingsSite) {
        app.addDeepLink("settings", SettingsSite);

        if (Helper.isNull(SettingsSite.settingsAction)) {
            let settingsAction = new MenuAction("settings", async () => {
                let currentSite = app.getCurrentSite();
                if (currentSite instanceof SettingsSite) {
                    currentSite.finish();
                }
                else {
                    let settingsSite = await app.findSite((site) => {
                        return (site instanceof SettingsSite);
                    });
                    if (Helper.isNotNull(settingsSite)) {
                        settingsSite.toForeground();
                    }
                    else {
                        app.startSite(SettingsSite);
                    }
                }
            }, MenuAction.SHOW_FOR_LARGE, 10000);
            settingsAction.setIcon("img/settings.png");
            SettingsSite.settingsAction = settingsAction;
        }
        if (SettingsSite.shouldAddSettingsAction) {
            app.addDefaultAction(SettingsSite.settingsAction);
        }
    }
});

class UserManager {
    static init(app) {
        UserManager.getMeUrl = null;
        UserManager.userData = {
            online: false,
            id: null,
            accesses: ["default"]
        };
        UserManager.app = app;

        UserManager.fetchMePromise = new Promise(function (resolve) {
            UserManager.fetchMePromiseResolver = resolve;
        });
    }

    static setData(data) {
        UserManager.userData = Object.assign(UserManager.userData, data);
        let siteManager = UserManager.app.getSiteManager();

        if (siteManager)
            siteManager.redrawCurrentActionBar();
    }

    static fetchMe(url) {
        UserManager.getMeUrl = Helper.nonNull(url, UserManager.getMeUrl);
        return DataManager.load(UserManager.getMeUrl).then(function (result) {
            if (result["success"]) {
                UserManager.setData(result["result"]);
            }
            UserManager.fetchMePromiseResolver();
        });
    }

    static logOut() {
        return DataManager.load("u/logout").then(function (data) {
            if (data["success"]) {
                UserManager.setData(data["result"]);
                let siteManager = UserManager.app.getSiteManager();

                if (siteManager)
                    siteManager.refreshCurrentSite();
                FlashMessenger.addMessage(FlashMessenger.MESSAGE_TYPE_SUCCESS, Translator.translate("logged-out-successfully"));
            }
        });
    }

    static hasAccess(access) {
        // console.log("Has access", access, UserManager.userData["accesses"].indexOf(access), UserManager.userData);
        return (UserManager.userData["accesses"].indexOf(access) >= 0)
    }

    static addCurrentUserListener(userId, listener) {
        UserManager.addIsLoggedInListener(function (isLoggedIn) {
            listener(isLoggedIn && UserManager.isCurrentUser(userId));
        });
    }

    static addIsLoggedInListener(listener) {
        this.fetchMePromise.then(function () {
            listener(UserManager.isLoggedIn());
        });
    }

    static isCurrentUser(userId) {
        return UserManager.userData.id === userId;
    }

    static isLoggedIn() {
        return Helper.isNotNull(UserManager.userData) && Helper.isNotNull(UserManager.userData.id);
    }
}

InitPromise.addPromise(function(app){
    UserManager.init(app);
    return UserManager.fetchMe("u/me").then(function(){
        UserManager.addIsLoggedInListener(function (isLoggedIn) {
            if (isLoggedIn) {
                const settingsManager = SettingsManager.getInstance();
                const settings = Helper.cloneJson(settingsManager.getSettings());
                for (let k in settings) {
                    settings[k]["value"] = JSON.stringify(settings[k]["value"]);
                }
                DataManager.send("u/syncSettings", settings).then(function(res){
                    if (res["success"])
                    {
                        for (let k in res["result"])
                        {
                            res["result"][k]["value"] = JSON.parse(res["result"][k]["value"]);
                        }
                        settingsManager.setSettings(res["result"]);
                    }
                });
            }
        });
    });
});

class UserAction extends MenuAction {
    constructor(title, callback, icon, order, access) {
        super(title, callback, icon, order);
        this._access = Helper.nonNull(access, "default");
    }

    getVisible() {
        // console.log("Action-access: ", this._access);
        return (super.getVisible() && UserManager.hasAccess(this._access));
    }

    getAccess() {
        return this._access;
    }

    copy(instance){
        let copy = super.copy(Helper.nonNull(instance, new UserAction()));
        copy._access = this._access;
        return copy;
    }
}

class NotAllowedSite extends AbstractSite{
    constructor(siteManager) {
        super(siteManager, 'userManagement/html/403.html');
    }
}

class UserSite extends AbstractSite {

    constructor(siteManager, view, deepLink, access) {
        super(siteManager, view, deepLink);
        this._access = access;
    }

    onConstruct(args) {
        if (!UserManager.hasAccess(this._access))
        {
            this.startSite(NotAllowedSite);
            this.finish({
                "error":403 
            });
            return;
        }
        return super.onConstruct(args);
    }

    onStart(args) {
        if (!UserManager.hasAccess(this._access))
        {
            this.startSite(NotAllowedSite);
            this.finish({
                "error":403
            });
            return;
        }
        return super.onStart(args);
    }
}

class LoginForm extends Form {

    constructor(formElem, url, method, isCachable) {
        super(formElem, url, method, isCachable);

        let emailElem = formElem.querySelector("#email");
        let passwordElem = formElem.querySelector("#password");

        let listener = function(){
            emailElem.setCustomValidity("");
            passwordElem.setCustomValidity("");
        };

        emailElem.addEventListener("keydown", listener);
        passwordElem.addEventListener("keydown", listener);
    }
}

class RegistrationForm extends Form {
    constructor(formElem, url, method, isCachable) {
        super(formElem, url, method, isCachable);

        // this.pw1 = formElem.querySelector("#password1");
        // this.pw2 = formElem.querySelector("#password2");

        // let self=this;
        // this.pw1.addEventListener("change", function(){
        //     self.checkPw();
        // });
        // this.pw2.addEventListener("change", function(){
        //     self.checkPw();
        // });
    }

    checkPw(){
        // if (this.pw1.value !== this.pw2.value || this.pw1.value.length < 8)
        // {
        //
        // }
    }
}

class UserFragment extends Fragment{

    constructor(site, view, access) {
        super(site, view);
        this._access = access;
    }

    isActive() {
        return super.isActive() && UserManager.hasAccess(this._access);
    }
}

class PasswordSettingsFragment extends UserFragment{
    constructor(site)
    {
        super(site, "userManagement/html/fragments/passwordSettings.html", "online");
    }
    onFirstStart() {
        let res = super.onFirstStart();
        let form = new Form(document.getElementById("change-password-form"), "u/passwordSettings/set", "post");
        form.onSubmit(function(res){
            for (let i = 0, n = res.length; i < n; i++)
            {
                FlashMessenger.addMessage(res[i]);
            }
            form.setValues({
                "oldPassword":"",
                "newPassword1":"",
                "newPassword2":""
            });
        });
        return res;
    }
}
InitPromise.addPromise(function(){
    SettingsSite.addSettingsFragment("password-settings", PasswordSettingsFragment);
});

class UserSettingsFragment extends UserFragment{
    constructor(site)
    {
        super(site, "userManagement/html/fragments/userSettings.html", "online");
    }

    onFirstStart() {
        let res = super.onFirstStart();
        (new Form(document.getElementById("user-settings-form"), "u/userSettings/set", "post")).load('u/userSettings').onSubmit(function(res){
            for (let i = 0, n = res.length; i < n; i++)
            {
                FlashMessenger.addMessage(res[i]);
            }
        });
        return res;
    }
}
InitPromise.addPromise(function(){
    SettingsSite.addSettingsFragment("user-settings", UserSettingsFragment);
});

class EditUserRolesSite extends UserSite {
    constructor(siteManager) {
        super(siteManager, 'userManagement/html/editUserRoles.html', "userRoles", "admin");
    }

    onConstruct(args) {
        let res = super.onConstruct(args);
        this.userId = args["id"];

        let self = this;
        return Promise.all([
            ScriptLoader.loadScript(Constants.SCRIPTS.LIST_JS),
            DataManager.load("u/userRoles" + DataManager.buildQuery({"id": self.userId})).then(function (res) {
                if (!res["success"]) {
                    FlashMessenger.addMessage(res["errors"][0]);
                    self.finish();
                }
                else {
                    self.setUserRoles(res["result"]["userRoles"]);
                    self.setAvailableRoles(res["result"]["availableRoles"]);
                    self.setUsername(res["result"]["username"]);
                }
            })
        ]).then(function () {
            return res;
        });
    }

    onFirstStart() {
        this.findBy("#username").innerHTML = this.username;

        const userRolesElement = this.findBy("#userRoles");
        const availableRolesElement = this.findBy("#availableRoles");

        const imgColumnUserRoles = new ImgConstSmartColumn("img/minus.png", "", false);
        const imgColumnAvailableRoles = new ImgConstSmartColumn("img/plus.png", "", false);

        const userRolesColumns = [
            new DataSmartColumn("id"),
            new SmartColumn("name", "name"),
            new SmartColumn("description", "description"),
            imgColumnUserRoles,
        ];

        const availableRolesColumns = [
            new DataSmartColumn("id"),
            new SmartColumn("name", "name"),
            new SmartColumn("description", "description"),
            imgColumnAvailableRoles,
        ];

        const userRolesListHelper = new ListHelper(userRolesElement, userRolesColumns, this.userRoles);
        const availableRolesListHelper = new ListHelper(availableRolesElement, availableRolesColumns, this.availableRoles);

        const userRolesTable = userRolesListHelper.createTable();
        const availableRolesTable = availableRolesListHelper.createTable();

        let self = this;
        const changeRoleFunction = function (roleId, addRole) {
            userRolesListHelper.setBusy(true);
            availableRolesListHelper.setBusy(true);

            return DataManager.send("u/changeUserRole", {
                "id": roleId,
                "userId": self.userId,
                "add": addRole
            }).then(function (res) {
                userRolesListHelper.setBusy(false);
                availableRolesListHelper.setBusy(false);

                if (!res["success"]) {
                    FlashMessenger.addMessage(res["errors"][0]);
                    return res;
                }

                let removingTable = null;
                let addingTable = null;
                if (res["result"]["hasRole"]) {
                    removingTable = availableRolesTable;
                    addingTable = userRolesTable;
                }
                else {
                    addingTable = availableRolesTable;
                    removingTable = userRolesTable;
                }

                const rowData = removingTable.get("id", roleId);
                if (rowData.length === 1) {
                    addingTable.add(rowData[0].values());
                    removingTable.remove("id", roleId);
                }

                return res;
            });
        };

        imgColumnUserRoles.setClickListener(function (cell) {
            let userRoleId = cell.closest("tr")["dataset"]["id"];
            changeRoleFunction(userRoleId, false);
        });

        imgColumnAvailableRoles.setClickListener(function (cell) {
            let availableRoleId = cell.closest("tr")["dataset"]["id"];
            changeRoleFunction(availableRoleId, true);
        });

    }

    setUserRoles(userRoles) {
        this.userRoles = userRoles;
    }

    setAvailableRoles(availableRoles) {
        this.availableRoles = availableRoles;
    }

    setUsername(username) {
        this.username = username;
    }
}

InitPromise.addPromise(function (app) {
    app.addDeepLink("userRoles", EditUserRolesSite);
    app.addDefaultAction(new UserAction('userRoles', function(){
        app.startSite(EditUserRolesSite);
    }, null, 1100, "admin"));
});

class ForgotPasswordSite extends UserSite{

    constructor(siteManager) {
        super(siteManager, 'userManagement/html/forgotPassword.html', "forgotPassword", "offline");
    }

    onFirstStart() {
        let self = this;
        (new Form(document.getElementById("forgot-password-form"), "u/newPassword", "post")).onSubmit(function(res){
            // UserManager.setData(res);
            // self.startStartsite();
            FlashMessenger.addMessage(FlashMessenger.MESSAGE_TYPE_SUCCESS, Translator.translate("new-password-code-send"));
            self.finish();
        });
    }
}
InitPromise.addPromise(function(app){
    app.addDeepLink("forgotPassword", ForgotPasswordSite);
});

class LoginSite extends UserSite {
    constructor(siteManager) {
        super(siteManager, 'userManagement/html/login.html', "login", "offline");
    }

    onFirstStart() {
        let self = this;
        (new LoginForm(document.getElementById("login-form"), "u/login", "post")).onSubmit(function (res) {
            UserManager.setData(res);
            self.startStartsite();
            FlashMessenger.addMessage(FlashMessenger.MESSAGE_TYPE_SUCCESS, Translator.translate("login-success"));
            self.finish();
        });

        this.findBy("#forgot-password-link").addEventListener("click", function () {
            self.startSite(ForgotPasswordSite);
            self.finish();
        });
    }
}

LoginSite.loginAction = null;
LoginSite.logoutAction = null;
LoginSite.addLoginAction = true;
LoginSite.addLogoutAction = true;

InitPromise.addPromise(function (app) {
    app.addDeepLink("login", LoginSite);

    if (Helper.isNull(LoginSite.loginAction)) {
        LoginSite.loginAction = new UserAction('login', function () {
            app.startSite(LoginSite);
        }, Menu.SHOW_NEVER, 1100, "offline");
    }
    if (Helper.isNull(LoginSite.logoutAction)) {
        LoginSite.logoutAction = new UserAction('logout', function () {
            UserManager.logOut();
        }, Menu.SHOW_NEVER, 1100, "online");
    }
    if (LoginSite.addLoginAction){
        app.addDefaultAction(LoginSite.loginAction);
    }
    if (LoginSite.addLogoutAction){
        app.addDefaultAction(LoginSite.logoutAction);
    }
});

class RegistrationSite extends UserSite {
    constructor(siteManager) {
        super(siteManager, 'userManagement/html/registration.html', "registration", "offline");
    }

    onFirstStart() {
        (new RegistrationForm(document.getElementById("registration-form"), "u/registration", "post")).onSubmit(function (res) {
            FlashMessenger.addMessage(FlashMessenger.MESSAGE_TYPE_SUCCESS, Translator.translate("registration-success"));
        });
    }
}

RegistrationSite.action = null;
RegistrationSite.addAction = true;

InitPromise.addPromise(function (app) {
    app.addDeepLink("registration", RegistrationSite);

    if (Helper.isNull(RegistrationSite.action)) {
        RegistrationSite.action = new UserAction('registration', function () {
            app.startSite(RegistrationSite);
        }, null, 1100, "offline");
    }
    if (RegistrationSite.addAction) {
        app.addDefaultAction(RegistrationSite.action);
    }
});

class SetNewPasswordSite extends UserSite {
    constructor(siteManager) {
        super(siteManager, 'userManagement/html/setNewPassword.html', "newPassword", "offline");
    }

    onConstruct(args) {
        this.code = args["code"];
        return super.onConstruct(args);
    }

    onFirstStart() {
        let formElem = document.getElementById("new-password-form");
        document.getElementById("code").value = this.code;

        let self = this;
        (new Form(formElem, "c/code", "post")).onSubmit(function(res){
            FlashMessenger.addMessage(FlashMessenger.MESSAGE_TYPE_SUCCESS, Translator.translate("password-updated"));
            self.startSite(LoginSite);
            self.finish();
        });
    }
}
InitPromise.addPromise(function(app){
    app.addDeepLink("newPassword", SetNewPasswordSite);
});

class MyStorageManager {
    static getInstance() {
        if (Helper.isNull(MyStorageManager.instance)) {
            MyStorageManager.instance = new MyStorageManager();
        }
        return MyStorageManager.instance;
    }

    async estimate() {
        if ('storage' in navigator && 'estimate' in navigator["storage"]) {
            // We've got the real thing! Return its response.
            return navigator["storage"]["estimate"]();
        }

        if ('webkitTemporaryStorage' in navigator &&
            'queryUsageAndQuota' in navigator["webkitTemporaryStorage"]) {
            // Return a promise-based wrapper that will follow the expected interface.
            return new Promise(function (resolve, reject) {
                navigator["webkitTemporaryStorage"]["queryUsageAndQuota"](
                    function (usage, quota) {
                        resolve({"usage": usage, "quota": quota});
                    },
                );
            });
        }
        // If we can't estimate the values, return a Promise that resolves with NaN.
        return Promise.resolve({"usage": NaN, "quota": NaN});
    }

    async isPersistent(){
        if (this.canPersist()){
            return navigator["storage"]["persisted"]();
        }
        return Promise.resolve(false);
    }

    canEstimateStorage() {
        return ('storage' in navigator && 'estimate' in navigator["storage"]|| 'webkitTemporaryStorage' in navigator &&
            'queryUsageAndQuota' in navigator["webkitTemporaryStorage"]);
    }

    canPersist() {
        return (navigator["storage"] && navigator["storage"]["persist"]);
    }

    persist(){
        if (this.canPersist()){
            return navigator["storage"]["persist"]();
        }
        return Promise.resolve(false);
    }
}

MyStorageManager.instance = null;

class InstallManager {
    static init() {
        window.addEventListener('beforeinstallprompt', e => {
            e.preventDefault();
            this.setDeferredPrompt(e);
        });
    }

    static setDeferredPrompt(e){
        this.deferredPromt = e;
        if (this.canInstallListener) {
            this.canInstallListener(this.deferredPromt);
        }
    }

    static async prompt(){
        if (Helper.isNotNull(this.deferredPromt)){
            this.deferredPromt["prompt"]();
            return this.deferredPromt["userChoice"].then(r => {
                MyStorageManager.getInstance().persist();
                return r;
            });
        }
        return Promise.resolve({
            "outcome":"dismissed",
            "platform":""
        });
    }

    static isInstalled(){
        return matchMedia("(display-mode: standalone)").matches;
    }

    static setCanInstallListener(listener, callIfCanInstall) {
        this.canInstallListener = listener;
        callIfCanInstall = Helper.nonNull(callIfCanInstall, true);

        if (callIfCanInstall && Helper.nonNull(this.deferredPromt)) {
            this.canInstallListener(this.deferredPromt);
        }
    }
}

InstallManager.init();

class Matomo {

    static init() {
        Matomo.isTrackingPromise = new Promise(async (resolve) => {
            let shouldTrack = Helper.nonNull(localStorage.getItem(Matomo.LOCAL_STORAGE_KEY), "1");
            if (Helper.isNull(shouldTrack)) {
                shouldTrack = await Matomo._askIsTracking();
                localStorage.setItem(Matomo.LOCAL_STORAGE_KEY, shouldTrack);
            }
            else {
                shouldTrack = (shouldTrack === "1");
                Matomo.setTrack(shouldTrack);
            }
            resolve(shouldTrack);
        });
        Matomo.isTrackingPromise.then(() => {
            Matomo.push(['trackPageView'], true);
            Matomo.push(['enableLinkTracking'], true);
            Matomo.push(['setTrackerUrl', Matomo.TRACK_SITE + '/piwik.php'], true);
            Matomo.push(['setSiteId', Matomo.SIDE_ID + ""], true);

            let d = document, g = d.createElement('script'), s = d.getElementsByTagName('head')[0];
            g.type = 'text/javascript';
            g.async = true;
            g.defer = true;
            g.src = Matomo.TRACK_SITE + '/piwik.js';
            s.appendChild(g);
        });
    }

    static update(title) {
        if (Helper.nonNull(Matomo.currentUrl)) {
            Matomo.push(['setReferrerUrl', Matomo.currentUrl]);
        }
        Matomo.currentUrl = window.location.pathname + window.location.search;
        Matomo.push(['setCustomUrl', Matomo.currentUrl]);
        Matomo.push(['setDocumentTitle', title]);

        // remove all previously assigned custom variables, requires Matomo (formerly Piwik) 3.0.2
        Matomo.push(['deleteCustomVariables', 'page']);
        Matomo.push(['setGenerationTimeMs', 0]);
        Matomo.push(['trackPageView']);

        // make Matomo aware of newly added content
        var content = document.getElementById('site-content');
        Matomo.push(['MediaAnalytics::scanForMedia', content]);
        Matomo.push(['FormAnalytics::scanForForms', content]);
        Matomo.push(['trackContentImpressionsWithinNode', content]);
        Matomo.push(['enableLinkTracking']);
    }

    static async _askIsTracking() {
        Matomo.isTrackingPromise = new Promise(resolve => {
            Matomo.push([function () {
                resolve(!this["isUserOptedOut"]());
            }]);
            Matomo.push([function () {
                resolve(!this["isUserOptedOut"]());
            }]);
        });
        return Matomo.isTrackingPromise;
    }

    static async query(method) {
        return fetch(Matomo.TRACK_SITE + Matomo.BASE_PATH + method, {
            "mode": "cors",
            "credentials": "include",
        }).then(res => res.text()).then(text => (new window.DOMParser()).parseFromString(text, "text/xml"));
    }

    static getTrackingPromise() {
        return Matomo.isTrackingPromise;
    }

    static async setTrack(shouldTrack) {
        Matomo.isTrackingPromise = Promise.resolve(shouldTrack);
        localStorage.setItem(Matomo.LOCAL_STORAGE_KEY, (shouldTrack === true) ? "1" : "0");

        if (shouldTrack) {
            Matomo.push(["forgetUserOptOut"], true);
        }
        else {
            Matomo.push(["optUserOut"], true);
        }
    }

    static async trackEvent(event, name, label, value){
        let ev = ["trackEvent", event, name];
        if (Helper.isNotNull(label)){
            ev.push(label);
        }
        if (Helper.isNotNull(value) && !isNaN(parseFloat(value)) && isFinite(value)){
            ev.push(value);
        }

        return this.push(ev);
    }

    static async push(arr, force) {

        if (!Array.isArray(arr)) {
            arr = [arr];
        }
        window["_paq"].push(arr);
    }
}

Matomo.currentUrl = null;

Matomo.LOCAL_STORAGE_KEY = "matomoShouldTrack";
Matomo.TRACK_SITE = "//matomo.silas.link";
Matomo.BASE_PATH = "/index.php?module=API&method=AjaxOptOut.";
Matomo.SIDE_ID = "1";

InitPromise.addPromise(() => {
    window["_paq"] = window["_paq"] || [];
    Matomo.init();
});

class MatomoShareButton extends MultipleShareButton{

    constructor(baseButton, platform, shouldLoadImg) {
        super([baseButton, (url) => {
            Matomo.trackEvent("shared", url, platform);
        }], shouldLoadImg);
    }
}

AndroidBridge.addDefinition("MatomoShareButton", MatomoShareButton);

class ScaleHelper {
    async scaleTo(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay, addListener) {

        addListener = Helper.nonNull(addListener, true);
        animationDelay = Helper.nonNull(animationDelay, 0);

        let newFontSize = await this._getNewFontSize(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay === 0);

        if (animationDelay > 0) {
            await new Promise(r => {
                setTimeout(r, animationDelay);
                fontElement.style.fontSize = newFontSize + "px";
            });
        }

        let self = this;
        let listener = function () {
            return new Promise(resolve => {
                let timeout = (typeof addListener === 'number') ? addListener : 255;
                setTimeout(() => {
                    resolve(self.scaleTo(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay, false));
                }, timeout);
            });
        };
        if (addListener !== false) {
            window.addEventListener("resize", listener);
        }
        return listener;
    }

    async scaleToFull(fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animDelay, addListener) {
        return this.scaleTo(1, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animDelay, addListener);
    }

    async _getNewFontSize(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, setFontSize) {
        margin = Helper.nonNull(margin, 10);
        ignoreHeight = Helper.nonNull(ignoreHeight, false);
        ignoreWidth = Helper.nonNull(ignoreWidth, false);
        fontWeight = Helper.nonNull(fontWeight, fontElement.innerHTML.length);
        setFontSize = Helper.nonNull(setFontSize, true);

        let hasNoTransitionClass = container.classList.contains("no-transition");

        if (!hasNoTransitionClass) {
            container.classList.add("no-transition");
        }

        const numChanged = 5;
        let oldDiffIndex = 0;
        let oldDiff = [];

        for (let i = 0; i < numChanged; i++) {
            oldDiff.push(0);
        }

        let beforeFontSize = fontElement.style.fontSize;
        let currentFontSize = 1;
        let widthDiff = 0;
        let heightDiff = 0;
        let containerWidth = 0;
        let containerHeight = 0;
        do {
            currentFontSize += oldDiff[oldDiffIndex] / (fontWeight + 1);
            fontElement.style.fontSize = currentFontSize + 'px';

            let containerStyle = window.getComputedStyle(container);

            containerWidth = containerStyle.getPropertyValue("width").replace('px', '');
            containerHeight = containerStyle.getPropertyValue("height").replace('px', '');

            widthDiff = containerWidth - fontElement.offsetWidth;
            heightDiff = containerHeight - fontElement.offsetHeight;

            oldDiffIndex = (oldDiffIndex+1)%numChanged;
            let newDiff = (ignoreWidth ? heightDiff : (ignoreHeight ? widthDiff : Math.min(widthDiff, heightDiff)));
            if (newDiff === oldDiff[(oldDiffIndex+1)%numChanged]) {
                break;
            }
            oldDiff[oldDiffIndex] = newDiff;
        } while ((widthDiff > (1 - scale) * containerWidth || ignoreWidth) && (heightDiff > (1 - scale) * containerHeight || ignoreHeight));

        currentFontSize -= margin;
        fontElement.style.fontSize = ((setFontSize) ? currentFontSize + "px" : beforeFontSize);

        if (!hasNoTransitionClass) {
            await new Promise((r) => {
                setTimeout(r, 50);
            });
            container.classList.remove("no-transition");
        }

        return currentFontSize;
    }
}

class AudioChain {

    constructor(context, sourceBuffer, chainFunction) {
        this.buffer = sourceBuffer;
        this.shouldLoop = false;
        this.loopStart = null;
        this.loopEnd = null;
        this.chainFunction = chainFunction;
        this.context = context;

        this.startTime = null;
        this.pauseTime = null;
        this.source = null;

        this.running = false;
    }

    setBuffer(buffer) {
        this.buffer = buffer;
    }

    setLooping(shouldLoop, loopStart, loopEnd) {
        this.shouldLoop = shouldLoop;

        if (Helper.isNotNull(loopStart)) {
            this.loopStart = loopStart;
        }
        if (Helper.isNotNull(loopEnd)) {
            this.loopEnd = loopEnd;
        }
    }

    async start(delay, offset, duration) {
        //sind sonst null, schmeißt in Android 5 einen fehler
        delay = Helper.nonNull(delay, 0);
        offset = Helper.nonNull(offset, 0);
        //Duration darf nicht gesetzt werden
        // duration = Helper.nonNull(duration, -1);

        let source = this.context.createBufferSource();

        source.loop = this.shouldLoop;
        if (Helper.isNotNull(this.loopStart)) {
            source.loopStart = this.loopStart;
        }
        if (Helper.isNotNull(this.loopEnd)) {
            source.loopEnd = this.loopEnd;
        }
        source.buffer = this.buffer;
        await this.chainFunction(source);

        if (Helper.isNull(duration)){
            source.start(delay, offset);
        }
        else{
            source.start(delay, offset, duration);
        }
        this.startTime = (new Date()).getTime() - (Helper.nonNull(offset, 0) * 1000);
        this.source = source;
        this.running = true;
    }

    async stop(delay) {
        if (Helper.isNotNull(this.source)) {
            delay = Helper.nonNull(delay, 0);

            this.pauseTime = ((new Date()).getTime()) - this.startTime;
            this.running = false;
            return this.source.stop(delay);
        }
        return null;
    }

    async resume() {

        if (!this.running) {
            return this.start(null, Helper.nonNull(this.pauseTime, 0) / 1000.0);
        }
    }
}

class SoundManager {
    static getInstance() {
        if (Helper.isNull(SoundManager.instance)) {
            SoundManager.instance = new SoundManager();
        }
        return SoundManager.instance;
    }

    constructor() {
        this.channels = {};
        this.context = new AudioContext();
        this.context.onstatechange = function () {
            console.log("stateChange from context", arguments);
        };
        this.context.oncomplete = function () {
            console.log("onComplete from context", arguments);
        };

        window.addEventListener("visibilitychange", () => {
            this.handleVisibilityChange();
        });
    }

    isNotSuspended(){
        // return false;
        return this.context.state !== "suspended";
    }

    set(options, channel) {
        channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
        let audioObject = Helper.nonNull(this.channels[channel], {});

        if (typeof options === "string") {
            options = {audio: options};
        }

        let audio = options.audio;
        if (Helper.isNotNull(audio)) {
            audioObject.loadedPromise = fetch(audio).then(res => res.arrayBuffer()).then(arrayBuffer => {
                return new Promise((r) => this.context.decodeAudioData(arrayBuffer, r));
            }).catch(e => console.error(e));
            this.stop(channel);
        }
        audioObject.muted = Helper.nonNull(options.muted, audioObject.muted, false);
        audioObject.volume = Helper.nonNull(options.volume, audioObject.volume, 1);
        audioObject.loop = Helper.nonNull(options.loop, audioObject.loop, false);
        audioObject.timeOffset = Helper.nonNull(options.timeOffset, audioObject.timeOffset, 0);
        this.channels[channel] = audioObject;

        if (audioObject.muted) {
            this.stop(channel);
        }

        return this.channels[channel];
    }

    async resumeContext(){
        if (typeof this.context.resume === "function") {
            return this.context.resume();
        }
    }

    async play(channel, audioOrOptions) {
        this.resumeContext();
        channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
        if (Helper.isNull(audioOrOptions)) {
            audioOrOptions = {};
        } else if (!(typeof audioOrOptions === "object")) {
            audioOrOptions = {
                audio: audioOrOptions
            };
        }
        audioOrOptions.timeOffset = Helper.nonNull(audioOrOptions.timeOffset, 0);

        this.stop(channel);
        this.set(audioOrOptions, channel);

        if (!this.channels[channel].muted) {
            let buffer = await this.channels[channel].loadedPromise;
            let source = new AudioChain(this.context, buffer, (sourceNode) => {
                let gain = this.context.createGain();
                gain.gain.value = this.channels[channel].volume;

                sourceNode.connect(gain);
                gain.connect(this.context.destination);
            });

            source.setBuffer(buffer);

            //to prevent gap in mp3-files
            source.setLooping(this.channels[channel].loop, 0.3, buffer.duration - 0.3);

            this.channels[channel].source = source;
            await source.start();
        }
        return this.channels[channel];
    }

    stop(channel) {
        channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);


        let oldAudio = this.channels[channel];
        if (Helper.isNotNull(oldAudio) && Helper.isNotNull(oldAudio.source)) {
            oldAudio.source.stop();
        }
    }

    get(channel) {
        channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
        return this.channels[channel];
    }

    async resume(channel) {
        channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
        if (Helper.isNotNull(this.channels[channel]) && !this.channels[channel].muted && Helper.isNotNull(this.channels[channel].source)) {
            return this.channels[channel].source.resume();
        }
    }

    stopAll(){
        for (let k in this.channels) {
            if (Helper.isNotNull(this.channels[k].source)) {
                this.channels[k].source.stop();
            }
        }
    }

    resumeAllIfNotMuted(){
        for (let k in this.channels) {
            if (Helper.isNotNull(this.channels[k]) && !this.channels[k].muted && Helper.isNotNull(this.channels[k].source)) {
                this.channels[k].source.resume();
            }
        }
    }

    handleVisibilityChange() {
        if (document.hidden) {
            this.stopAll();
        }
        else {
            this.resumeAllIfNotMuted();
        }
    }
}

SoundManager.CHANNELS = {
    MUSIC: "music",
    SOUND: "sound",
    DEFAULT: "default"
};

InitPromise.addPromise(() => {
    PauseSite.onPauseListeners.push(() => {
        SoundManager.getInstance().stopAll();
    });
    PauseSite.onStartListeners.push(() => {
        SoundManager.getInstance().resumeAllIfNotMuted();
    });
});

class Code {
    constructor(args) {
        if (typeof args === "string") {
            args = {
                "code": args
            };
        }
        this.args = args;
        this.isCacheable = false;
    }

    setIsCacheable(isCacheable) {
        this.isCacheable = isCacheable;
    }

    getIsCacheable() {
        return this.isCacheable;
    }

    activate() {
        return DataManager.send("c/code", this.args);
    }
}

class CodeSite extends AbstractSite {
    constructor(siteManager) {
        super(siteManager, "core/html/load.html", "code");
    }

    onConstruct(args) {
        super.onConstruct(args);
        console.log(args);

        let resPromise = Promise.resolve();
        if (Helper.isNotNull(args["code"])) {
            let code = args["code"];
            let isCachable = Helper.nonNull(args["cachable"], false);

            let codeObject = new Code(code);
            codeObject.setIsCacheable(isCachable);

            let self = this;
            resPromise = codeObject.activate().then(function (res) {
                if (!res["success"]) {
                    FlashMessenger.addMessage(FlashMessenger.MESSAGE_TYPE_SUCCESS, Translator.translate(res["errors"][0]));
                }
                else {
                    FlashMessenger.addMessage(FlashMessenger.MESSAGE_TYPE_SUCCESS, Translator.translate(Helper.nonNull(res["result"]["successMessage"], "code-activated")));
                }
                self.finish();
            });
        }
    }
}

InitPromise.addPromise(function (app) {
    app.addDeepLink("code", CodeSite);
});

class ContactSite extends AbstractSite{
    constructor(siteManager) {
        super(siteManager, 'contact/html/contact.html', ContactSite.DEEP_LINK);
    }

    onFirstStart() {
        new Form(this.findBy("#contact-form"), "contact", "post").onSubmit((d) => {
            FlashMessenger.addMessage("contact-message-sent");
            this.finish();
        });

        super.onFirstStart();
    }
}
ContactSite.DEEP_LINK = "contactMe";

InitPromise.addPromise((app) => {
    if (ContactSite.DEEP_LINK){
        app.addDeepLink(ContactSite.DEEP_LINK, ContactSite);
    }
});

class WordRotatorBaseSite extends AbstractSite {
    // createActionBarMenu(menu) {
    //     menu = super.createActionBarMenu(menu);
    //     // menu.addAction(SettingsSite.settingsAction.copy());
    //     // let actions = menu.actions;
    //     // for (let i = 0; i < actions.length; i++) {
    //     //     if (actions[i].title === "login" || actions[i].title === "registration"){
    //     //         actions[i].setVisible(false);
    //     //     }
    //     // }
    //     return menu;
    // }
}

class TemplateContainer{
    constructor(leafTemplate, parentTemplate, rowTemplate, triangleTemplate){
        this.leafTemplate = leafTemplate;
        this.parentTemplate = parentTemplate;
        this.rowTemplate = rowTemplate;
        this.triangleTemplate = triangleTemplate;
    }

    copyLeafTemplate()
    {
        return Helper.cloneNode(this.leafTemplate);
    }

    copyParentTemplate()
    {
        return Helper.cloneNode(this.parentTemplate);
    }

    copyRowTemplate()
    {
        return Helper.cloneNode(this.rowTemplate);
    }

    copyTriangleTemplate()
    {
        return Helper.cloneNode(this.triangleTemplate);
    }
}

class Segment{
    constructor(element){
        this.rotation = 0;
        this.element = element;
        this.parent = null;
    }

    getCurrentRotations(rotations){
        return rotations;
    }

    sameAs(otherSegment){
        return false;
    }

    setParent(parent)
    {
        this.parent = parent;
    }
    
    getLevel()
    {
        if (this.parent!==null)
        {
            return this.parent.getLevel();
        }
    }

    canRotate(){
        return false;
    }

    isSolved(){
        return (this.rotation === 0);
    }
    
    async rotate(){
        return Promise.resolve();
    };

    _updateElement(){};

    applyRotations(rotations){
        return rotations;
    }

    applyLocks(locks)
    {
        return locks;
    }

    getCurrentLocked(lockedArray){
        return lockedArray;
    }

    getElement()
    {
        return this.element;
    }
}

class LeafSegment extends Segment {

    constructor(element, leaf) {
        super(element);
        this.leaf = 'A';
        if (Helper.isNotNull(leaf)) {
            this.setLeaf(leaf);
        }
    }

    sameAs(otherSegment) {
        // debugger;
        return (otherSegment instanceof LeafSegment && otherSegment.leaf === this.leaf);
    }

    setLeaf(leaf) {
        this.leaf = leaf;
    }

    _updateElement() {
        this.element.querySelector(".leaf-element").removeAllChildren().appendChild(document.createTextNode(this.leaf));
    }
}

class ParentSegment extends Segment {
    static initListener() {
        window.addEventListener("mousedown", (e) => {
            ParentSegment.mouseDownTarget = e.target;
            ParentSegment.clickPosition = {x: e.pageX, y: e.pageY};
        });
        window.addEventListener("mouseup", (e) => {
            ParentSegment.mouseDownTarget = null;
            ParentSegment.clickPosition = {};
        });

        window.addEventListener("touchstart", (e) => {
            if (e.targetTouches.length === 1) {
                ParentSegment.mouseDownTarget = e.targetTouches[0].target;
                ParentSegment.clickPosition = {x: e.targetTouches[0].pageX, y: e.targetTouches[0].pageY};
            }
            // else if (Array.isArray(e.path) && e.path.length >= 1) {
            //     ParentSegment.mouseDownTarget = e.path[0];
            //     ParentSegment.clickPosition = null;
            // }
        });
        window.addEventListener("touchend", (e) => {
            ParentSegment.mouseDownTarget = null;
            ParentSegment.clickPosition = {};
        });
    }

    setIsRotatable(rotatable) {
        this.rotatable = rotatable;
        this._updateElement();
    }

    constructor(element) {
        super(element);
        this.children = [];
        this.class = "rotate-0";
        this.rotatable = true;

        this.userRotationDelta = 100;
        this.lastUserRotation = 0;

        let self = this;
        this.touchendListener = function (e) {
            let now = new Date().getTime();

            // console.log("touchendListener", e,now, self.lastUserRotation);
            let target = null;
            let position = null;

            if (e.changedTouches.length >= 1) {
                target = document.elementFromPoint(e.changedTouches[0].pageX, e.changedTouches[0].pageY);
                position = {x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY};
            }
            if (target != null && e.targetTouches.length === 0 && self.element.contains(ParentSegment.mouseDownTarget) && self.element.contains(target)) {
                e.stopPropagation();
                e.preventDefault();
                if (self.lastUserRotation+self.userRotationDelta > now){
                    return;
                }
                self.getLevel().segmentClickedListener(self);
                self.rotate(ParentSegment.mouseDownTarget, target, ParentSegment.clickPosition, position);
                // console.log("touchendListener stopped event", e);

                self.lastUserRotation = new Date().getTime();
            }
        };
        this.mouseupListener = function (e) {
            let now = new Date().getTime();
            // console.log("mouseupListener", e,now, self.lastUserRotation);

            if (ParentSegment.mouseDownTarget !== null && self.element.contains(ParentSegment.mouseDownTarget) && self.element.contains(e.target)) {
                let position = {x: e.pageX, y: e.pageY};
                e.stopPropagation();
                e.preventDefault();
                if (self.lastUserRotation+self.userRotationDelta > now){
                    return;
                }
                self.getLevel().segmentClickedListener(self);
                self.rotate(ParentSegment.mouseDownTarget, e.target, ParentSegment.clickPosition, position);
                // console.log("mouseupListener stopped event", e);
                self.lastUserRotation = new Date().getTime();
            }
        };
    }

    canRotate() {
        return (this.rotatable && !this.getLevel().getHasWon());
    }

    async rotate(firstElem, secondElem, firstPosition, secondPosition) {
        const timeout = 250;
        const clickTolerance = 5;

        let rotationDirection = 1;
        if (Helper.isNotNull(secondElem) && Helper.isNotNull(firstElem) &&
            (Helper.isNull(firstPosition) || Helper.isNull(secondPosition) ||
                Math.abs(firstPosition.x - secondPosition.x) > clickTolerance ||
                Math.abs(firstPosition.y - secondPosition.y) > clickTolerance)) {

            let firstIndex = -1;
            let secondIndex = -1;
            let rotationIndexes = [0, 1, 3, 2];
            for (let i = 0; i < this.children.length; i++) {
                if (this.children[rotationIndexes[i]].element === firstElem || this.children[rotationIndexes[i]].element.contains(firstElem)) {
                    firstIndex = (i + this.rotation / 90) % 4;
                }
                if (this.children[rotationIndexes[i]].element === secondElem || this.children[rotationIndexes[i]].element.contains(secondElem)) {
                    secondIndex = (i + this.rotation / 90) % 4;
                }
            }

            if (firstIndex >= 0 && secondIndex >= 0) {
                if (firstIndex === 2 && (secondIndex === 0 || secondIndex === 1)
                    || firstIndex === 1 && (secondIndex === 0 || secondIndex === 3)
                    || (firstIndex === 0 && secondIndex === 3)
                    || (firstIndex === 3 && secondIndex === 2)) {
                    rotationDirection = -1;
                }
            }
        }

        if (this.canRotate()) {
            this.rotation += 360 + 90 * rotationDirection;
            this.rotation %= 360;

            let currentRotation = this.rotation;

            this._updateRotationClass();
            this.element.classList.add("rotating");
            if (rotationDirection === -1) {
                this.element.classList.add("reverse");
            }

            let delayPromise = new Promise(function (resolve) {
                setTimeout(resolve, timeout);
            }).then(() => {
                if (this.rotation === currentRotation) {
                    this.element.classList.remove("rotating");
                    this.element.classList.remove("reverse");
                }
            });
            this.getLevel().checkHasWon(delayPromise);
            return delayPromise;
        }
    }

    sameAs(otherSegment) {
        if (!(otherSegment instanceof ParentSegment) || otherSegment.children.length !== this.children.length) {
            return false;
        }
        for (let i = 0; i < this.children.length; i++) {
            if (!this.children[i].sameAs(otherSegment.children[i])) {
                return false;
            }
        }
        return true;
    }

    applyRotations(rotations) {
        this.rotation = rotations[0];

        if (isNaN(this.rotation)) {
            this.rotation = 0;
        }

        rotations.splice(0, 1);
        for (let i = 0, n = this.children.length; i < n; i++) {
            rotations = this.children[i].applyRotations(rotations);
        }
        return rotations;
    }

    applyLocks(locks) {
        this.rotatable = locks[0];
        locks.splice(0, 1);
        for (let i = 0, n = this.children.length; i < n; i++) {
            locks = this.children[i].applyLocks(locks);
        }
        return locks;
    }

    getCurrentRotations(rotations) {
        rotations.push(this.rotation);
        for (let i = 0, n = this.children.length; i < n; i++) {
            rotations = this.children[i].getCurrentRotations(rotations);
        }
        return rotations;
    }

    getCurrentLocked(locked) {
        locked.push(this.rotatable);
        for (let i = 0, n = this.children.length; i < n; i++) {
            locked = this.children[i].getCurrentLocked(locked);
        }
        return locked;
    }

    isSolved(checkChildren) {
        checkChildren = Helper.nonNull(checkChildren, true);
        if (checkChildren) {
            for (let i = 0, n = this.children.length; i < n; i++) {
                if (!this.children[i].isSolved()) {
                    return false;
                }
            }
        }
        return (this.rotation === 0 || (
            this.children[0].sameAs(this.children[3]) && this.children[1].sameAs(this.children[2]) && (
            this.rotation === 180 || this.children[0].sameAs(this.children[1]))))
    }

    setChildren(children) {
        this.children = [];
        for (let i = 0, n = children.length; i < n; i++) {
            this.addChild(children[i]);
        }
    }

    addChild(child) {
        this.children.push(child);
        child.setParent(this);
        this._updateElement();
    }

    _updateRotationClass() {
        // this.style.transform = "rotate("+this.rotation+"deg)";
        this.element.classList.remove(this.class);
        this.class = "rotate-" + this.rotation;
        if (this.class === "rotate-0") {
            this.class = "rotate-360";
        }
        this.element.classList.add(this.class);
    }

    _updateElement() {
        let layer = this._getLayer();
        if (layer >= 2) {
            this.element.classList.add("layer-" + layer);
        }

        if (!this.rotatable) {
            this.element.classList.add("locked");
        }

        const childContainer = this.element.querySelector(".child-container");
        childContainer.removeAllChildren();

        this._updateRotationClass();

        this.element.removeEventListener("mouseup", this.mouseupListener);
        this.element.removeEventListener("touchend", this.touchendListener);

        this.element.addEventListener("mouseup", this.mouseupListener);
        this.element.addEventListener("touchend", this.touchendListener);

        for (let i = 0, n = this.children.length; i < n; i++) {
            this.children[i]._updateElement();
            childContainer.appendChild(this.children[i].getElement());
            if (i % 2 === 1 && this.children.length - 1 !== i) {
                childContainer.appendChild(document.createElement("br"));
            }
        }
    }

    _getLayer() {
        if (this.children.length >= 1 && this.children[0] && this.children[0] instanceof ParentSegment) {
            return this.children[0]._getLayer() + 1;
        }
        return 1;
    }
}

ParentSegment.initListener();

class Level {
    constructor(templateContainer) {
        this.rootSegment = null;
        this.words = [];
        this.startRotations = [];
        this.templateContainer = templateContainer;

        this.hasWon = false;
        this.id = null;

        this.wonResolver = null;
        this.giveUpResolver = null;

        const self = this;
        this.wonPromise = new Promise((resolve, reject) => {
            self.wonResolver = resolve;
            self.giveUpResolver = reject;
        });

        this.segmentClickedListener = () => {};
    }

    saveAsCurrentLevel(){
        let rotations = this.getCurrentRotations();
        let locked = this.getCurrentLocked();
        localStorage.setItem("currentLevel", JSON.stringify({"id": this.id, "rotations": rotations, "locks":locked}));
    }

    getCurrentLocked(){
        if (this.rootSegment !== null)
        {
            return this.rootSegment.getCurrentLocked([]);
        }
        return [];
    }

    getCurrentRotations(){
        if (this.rootSegment !== null)
        {
            return this.rootSegment.getCurrentRotations([]);
        }
        return [];
    }

    setLocks(locks)
    {
        if (this.rootSegment !== null){
            this.rootSegment.applyLocks(locks);
        }
    }

    setId(id)
    {
        this.id = id;
    }

    getId()
    {
        return this.id;
    }

    getLevel()
    {
        return this;
    }

    setRootSegment(rootSegment)
    {
        this.rootSegment = rootSegment;
        this.rootSegment.setParent(this);
        if (this.startRotations)
        {
            this.applyRotations();
        }
    }

    setWords(words)
    {
        this.words = [];
        for (let i = 0, n = words.length; i < n; i++) {
            this.words.push(words[i].toUpperCase());
        }
    }

    setStartRotations(rotations)
    {
        this.startRotations = rotations;
    }

    applyRotations(rotations)
    {
        if (this.rootSegment)
        {
            rotations = Helper.nonNull(rotations, this.startRotations);
            this.rootSegment.applyRotations(rotations);
        }
    }

    getHasWon()
    {
        return this.hasWon;
    }

    checkHasWon(delayPromise) {
        if (this.rootSegment.isSolved()){
            this.hasWon = true;
            const self = this;
            Promise.resolve(delayPromise).then(()=>{
                self.wonResolver(true);
            });
            return true;
        }
        this.saveAsCurrentLevel();
        return false;
    }

    getWonPromise(){
        return this.wonPromise;
    }

    getRootSegment(){
        return this.rootSegment;
    }

    createSegments() {};

    getRotatableSegments(){
        return Level._getRotatableSegmentsFrom(this.rootSegment);
    }

    setSegmentClickedListener(listener){
        this.segmentClickedListener = listener;
    }

    static _getRotatableSegmentsFrom(segment){
        let rotatable = [];
        if (segment.canRotate())
        {
            rotatable.push(segment);
        }
        if (segment instanceof ParentSegment){
            for (let i = 0; i < segment.children.length; i++) {
                rotatable.push.apply(rotatable, Level._getRotatableSegmentsFrom(segment.children[i]));
            }
        }
        return rotatable;
    }

    static _createLeafsForWord(word, leafSegmentTemplate)
    {
        let leafSegments = [];
        for (let i = 0, n = word.length; i < n; i++) {
            leafSegments.push(new LeafSegment(Helper.cloneNode(leafSegmentTemplate), word.charAt(i)));
        }
        return leafSegments;
    }
}

class RowSegment extends ParentSegment{
    constructor(element) {
        super(element);
        this.rotatable = false;
    }

    applyRotations(rotations)
    {
        for (let i = 0, n = this.children.length; i < n; i++) {
            rotations = this.children[i].applyRotations(rotations);
        }
        return rotations;
    }

    getCurrentRotations(rotations){
        for (let i = 0, n = this.children.length; i < n; i++) {
            rotations = this.children[i].getCurrentRotations(rotations);
        }
        return rotations;
    }

    getCurrentLocked(locked) {
        for (let i = 0, n = this.children.length; i < n; i++) {
            locked = this.children[i].getCurrentLocked(locked);
        }
        return locked;
    }

    applyLocks(locks) {
        for (let i = 0, n = this.children.length; i < n; i++) {
            locks = this.children[i].applyLocks(locks);
        }
        return locks;
    }

    _updateElement() {
        const childContainer = this.element.querySelector(".child-container");
        childContainer.removeAllChildren();

        this._updateRotationClass();

        const self = this;
        this.element.onclick = function (e) {
            self.rotate();
            e.stopPropagation();
        };

        for (let i = 0, n = this.children.length; i < n; i++) {
            this.children[i]._updateElement();
            childContainer.appendChild(this.children[i].getElement());
        }
    }
}

class RowLevel extends Level {
    constructor(container, wordLength) {
        super(container);
        this.wordLength = wordLength;
    }

    createSegments() {
        if (this.words.length >= 2 && this.words[0].length >= this.wordLength && this.words[1].length >= this.wordLength) {
            let leafsWordOne = Level._createLeafsForWord(this.words[0], this.templateContainer.copyLeafTemplate());
            let leafsWordTwo = Level._createLeafsForWord(this.words[1], this.templateContainer.copyLeafTemplate());

            let rootSegment = new RowSegment(this.templateContainer.copyRowTemplate());
            for (let i = 0, n = this.wordLength / 2; i < n; i++) {
                let parent = new ParentSegment(this.templateContainer.copyParentTemplate());
                parent.addChild(leafsWordOne[2 * i]);
                parent.addChild(leafsWordOne[2 * i + 1]);
                parent.addChild(leafsWordTwo[2 * i]);
                parent.addChild(leafsWordTwo[2 * i + 1]);
                rootSegment.addChild(parent);
            }
            // rootSegment.applyRotations(this.startRotations);
            this.setRootSegment(rootSegment);
        }
    }
}

class SimpleLevel extends RowLevel{
    constructor(container) {
        super(container, 6);
    }
}

class RowLevel8 extends RowLevel{
    constructor(container) {
        super(container, 8);
    }
}

class RowLevel10 extends RowLevel{
    constructor(container) {
        super(container, 10);
    }
}

class TriangleSegment extends RowSegment{

}

class SixWordsRowLevel extends Level {

    constructor(templateContainer, wordLength) {
        super(templateContainer);
        this.wordLength = wordLength;
    }

    createSegments() {
        if (this.words.length >= 6 &&
            this.words[0].length >= this.wordLength &&
            this.words[1].length >= this.wordLength &&
            this.words[2].length >= this.wordLength &&
            this.words[3].length >= this.wordLength &&
            this.words[4].length >= this.wordLength &&
            this.words[5].length >= this.wordLength
        ) {
            let leafsWords = [];
            leafsWords[0] = Level._createLeafsForWord(this.words[0], this.templateContainer.copyLeafTemplate());
            leafsWords[1] = Level._createLeafsForWord(this.words[1], this.templateContainer.copyLeafTemplate());
            leafsWords[2] = Level._createLeafsForWord(this.words[2], this.templateContainer.copyLeafTemplate());
            leafsWords[3] = Level._createLeafsForWord(this.words[3], this.templateContainer.copyLeafTemplate());
            leafsWords[4] = Level._createLeafsForWord(this.words[4], this.templateContainer.copyLeafTemplate());
            leafsWords[5] = Level._createLeafsForWord(this.words[5], this.templateContainer.copyLeafTemplate());

            let rootSegment = new RowSegment(this.templateContainer.copyRowTemplate());
            for (let i = 0; i < this.wordLength / 4; i++) {

                let parents = [];
                parents[0] = new ParentSegment(this.templateContainer.copyParentTemplate());
                parents[1] = new ParentSegment(this.templateContainer.copyParentTemplate());
                parents[2] = new ParentSegment(this.templateContainer.copyParentTemplate());
                parents[3] = new ParentSegment(this.templateContainer.copyParentTemplate());
                parents[4] = new ParentSegment(this.templateContainer.copyParentTemplate());
                parents[5] = new ParentSegment(this.templateContainer.copyParentTemplate());

                parents[0].addChild(leafsWords[0][4*i]);
                parents[0].addChild(leafsWords[0][4*i+1]);
                parents[0].addChild(leafsWords[1][4*i]);
                parents[0].addChild(leafsWords[1][4*i+1]);

                parents[1].addChild(leafsWords[0][4*i+2]);
                parents[1].addChild(leafsWords[0][4*i+3]);
                parents[1].addChild(leafsWords[1][4*i+2]);
                parents[1].addChild(leafsWords[1][4*i+3]);

                parents[2].addChild(leafsWords[2][4*i]);
                parents[2].addChild(leafsWords[2][4*i+1]);
                parents[2].addChild(leafsWords[3][4*i]);
                parents[2].addChild(leafsWords[3][4*i+1]);

                parents[3].addChild(leafsWords[2][4*i+2]);
                parents[3].addChild(leafsWords[2][4*i+3]);
                parents[3].addChild(leafsWords[3][4*i+2]);
                parents[3].addChild(leafsWords[3][4*i+3]);

                parents[4].addChild(leafsWords[4][4*i]);
                parents[4].addChild(leafsWords[4][4*i+1]);
                parents[4].addChild(leafsWords[5][4*i]);
                parents[4].addChild(leafsWords[5][4*i+1]);

                parents[5].addChild(leafsWords[4][4*i+2]);
                parents[5].addChild(leafsWords[4][4*i+3]);
                parents[5].addChild(leafsWords[5][4*i+2]);
                parents[5].addChild(leafsWords[5][4*i+3]);


                let parent = new ParentSegment(this.templateContainer.copyParentTemplate());
                let triangle = new TriangleSegment(this.templateContainer.copyTriangleTemplate());
                if (i % 2 === 0) {
                    parent.addChild(parents[0]);
                    parent.addChild(parents[1]);
                    parent.addChild(parents[2]);
                    parent.addChild(parents[3]);

                    let rowSegment = new RowSegment(this.templateContainer.copyRowTemplate());

                    rowSegment.addChild(parents[4]);
                    rowSegment.addChild(parents[5]);

                    triangle.addChild(parent);
                    triangle.addChild(rowSegment);

                    triangle.getElement().classList.add("type-1");
                }
                else {

                    let rowSegment = new RowSegment(this.templateContainer.copyRowTemplate());

                    rowSegment.addChild(parents[0]);
                    rowSegment.addChild(parents[1]);

                    triangle.addChild(rowSegment);
                    triangle.addChild(parent);

                    parent.addChild(parents[2]);
                    parent.addChild(parents[3]);
                    parent.addChild(parents[4]);
                    parent.addChild(parents[5]);

                    triangle.getElement().classList.add("type-2");
                }
                rootSegment.addChild(triangle);
            }
            this.setRootSegment(rootSegment);
        }
    }
}

class SixWordsRowLevel8 extends SixWordsRowLevel {
    constructor(templateContainer) {
        super(templateContainer, 8);
    }
}

class SixWordsRowLevel12 extends SixWordsRowLevel {
    constructor(templateContainer) {
        super(templateContainer, 12);
    }
}

class FourWordsLevel extends Level {

    constructor(templateContainer, wordLength) {
        super(templateContainer);
        this.wordLength = wordLength;
    }

    createSegments() {
        if (this.words.length >= 4 &&
            this.words[0].length >= this.wordLength &&
            this.words[1].length >= this.wordLength &&
            this.words[2].length >= this.wordLength &&
            this.words[3].length >= this.wordLength
        ) {
            let leafsWords = [];
            leafsWords[0] = Level._createLeafsForWord(this.words[0], this.templateContainer.copyLeafTemplate());
            leafsWords[1] = Level._createLeafsForWord(this.words[1], this.templateContainer.copyLeafTemplate());
            leafsWords[2] = Level._createLeafsForWord(this.words[2], this.templateContainer.copyLeafTemplate());
            leafsWords[3] = Level._createLeafsForWord(this.words[3], this.templateContainer.copyLeafTemplate());

            let rootSegment = new RowSegment(this.templateContainer.copyRowTemplate());
            for (let i = 0; i < this.wordLength / 4; i++) {

                let parents = [];
                parents[0] = new ParentSegment(this.templateContainer.copyParentTemplate());
                parents[1] = new ParentSegment(this.templateContainer.copyParentTemplate());
                parents[2] = new ParentSegment(this.templateContainer.copyParentTemplate());
                parents[3] = new ParentSegment(this.templateContainer.copyParentTemplate());

                parents[0].addChild(leafsWords[0][4 * i]);
                parents[0].addChild(leafsWords[0][4 * i + 1]);
                parents[0].addChild(leafsWords[1][4 * i]);
                parents[0].addChild(leafsWords[1][4 * i + 1]);

                parents[1].addChild(leafsWords[0][4 * i + 2]);
                parents[1].addChild(leafsWords[0][4 * i + 3]);
                parents[1].addChild(leafsWords[1][4 * i + 2]);
                parents[1].addChild(leafsWords[1][4 * i + 3]);

                parents[2].addChild(leafsWords[2][4 * i]);
                parents[2].addChild(leafsWords[2][4 * i + 1]);
                parents[2].addChild(leafsWords[3][4 * i]);
                parents[2].addChild(leafsWords[3][4 * i + 1]);

                parents[3].addChild(leafsWords[2][4 * i + 2]);
                parents[3].addChild(leafsWords[2][4 * i + 3]);
                parents[3].addChild(leafsWords[3][4 * i + 2]);
                parents[3].addChild(leafsWords[3][4 * i + 3]);

                let parent = new ParentSegment(this.templateContainer.copyParentTemplate());
                parent.addChild(parents[0]);
                parent.addChild(parents[1]);
                parent.addChild(parents[2]);
                parent.addChild(parents[3]);

                rootSegment.addChild(parent);
            }
            this.setRootSegment(rootSegment);
        }
    }
}

class FourWordsLevel8 extends FourWordsLevel{
    constructor(templateContainer) {
        super(templateContainer, 8);
    }
}

class FourWordsLevel12 extends FourWordsLevel{
    constructor(templateContainer) {
        super(templateContainer, 12);
    }
}

class LevelHelper {
    static setLevelType(typeId, level) {
        LevelHelper.types[typeId] = level;
    }

    static getLevelClass(type) {
        return LevelHelper.types[type];
    }

    static inflateLevel(jsonLevel, templateContainer) {
        let level = new (LevelHelper.types[jsonLevel["rendererType"]])(templateContainer);
        level.setWords(jsonLevel["words"]);
        level.setId(jsonLevel["id"]);

        for (let i = 0, n = jsonLevel["rotations"].length; i < n; i++) {
            if (jsonLevel["rotations"][i] <= 4) {
                jsonLevel["rotations"][i] = 90 * jsonLevel["rotations"][i];
            }
        }

        level.setStartRotations(jsonLevel["rotations"]);
        return level;
    }
}

LevelHelper.types = {
    20: SimpleLevel,
    40: RowLevel8,
    60: RowLevel10,
    100: SixWordsRowLevel8,
    120: FourWordsLevel8,
    140: SixWordsRowLevel12,
    160: FourWordsLevel12,
};

class WordRotatorDb extends MyDb {

    static getInstance() {
        if (Helper.isNull(WordRotatorDb.instance)) {
            WordRotatorDb.instance = new WordRotatorDb();
        }
        return WordRotatorDb.instance;
    }

    constructor() {
        super("wordRotator", 3);
    }

    upgrade(db, oldVersion, newVersion, e) {
        console.log("upgrading!");
        if (Helper.isNull(oldVersion) || oldVersion < 1 && newVersion >= 1) {
            let levelObjectStore = db.createObjectStore(WordRotatorDb.OBJECT_STORE.LEVEL, {"keyPath": "id"});
        }
        if (Helper.isNull(oldVersion) || oldVersion < 2 && newVersion >= 2) {
            let levelObjectStore = e.target.transaction.objectStore(WordRotatorDb.OBJECT_STORE.LEVEL);
            levelObjectStore.createIndex("played", ["deleted", "played", "difficulty", "id"], {"unique": false});
        }
        if (Helper.isNull(oldVersion) || oldVersion < 3 && newVersion >= 3) {
            let levelObjectStore = e.target.transaction.objectStore(WordRotatorDb.OBJECT_STORE.LEVEL);
            levelObjectStore.createIndex("difficulty", "difficulty", {"unique": false});
        }
    };

    async saveManyLevels(levels) {
        return this.saveMany(levels, WordRotatorDb.OBJECT_STORE.LEVEL).catch(e => {
            console.error("insert error!", e);
        });
    }

    async loadLevel(levelId) {
        return this.load(levelId, WordRotatorDb.OBJECT_STORE.LEVEL);
    }

    async loadNextLevel(rendererTypes) {
        let levels = await this.loadAll(WordRotatorDb.OBJECT_STORE.LEVEL);
        levels = levels.sort((a,b) => {
           return (a["difficulty"] - b["difficulty"]);
        });

        let wrongLevels = [];
        let newLevels = [];
        let difficulty = -1;
        for (let i = 0, n = levels.length; i < n; i++) {
            if ((difficulty < 0 || difficulty === levels[i]["difficulty"]) && !levels[i]["deleted"] && !levels[i]["played"] && rendererTypes.indexOf(levels[i]["rendererType"]) !== -1) {
                newLevels.push(levels[i]);
                difficulty = levels[i]["difficulty"];
            }
            else if (levels[i]["difficulty"] !== 0 && !levels[i]["deleted"] && !levels[i]["played"] ) {
                wrongLevels.push(levels[i]);
            }
        }

        if (newLevels.length === 0) {
            return null;
        }

        return newLevels[Math.round(Math.random() * newLevels.length) % newLevels.length];
    }

    async saveLevelPlayed(levelId) {
        const level = await this.loadLevel(levelId);
        level.played = true;
        return await this.saveObj(level, WordRotatorDb.OBJECT_STORE.LEVEL);
    }

    async countLevels(){

    }
}

WordRotatorDb.OBJECT_STORE = {
    LEVEL: "level",
};
WordRotatorDb.instance = null;

class EndSite extends WordRotatorBaseSite{
    constructor(siteManager) {
        super(siteManager, "html/application/end.html");
    }

    onStart(args) {
        Matomo.update("End Site");
        return super.onStart(args);
    }
}

class LevelSite extends WordRotatorBaseSite {
    constructor(siteManager) {
        super(siteManager, "html/application/level.html");
    }

    createActionBarMenu(menu) {
        menu = super.createActionBarMenu(menu);

        let coinAction = new MenuAction(Helper.nonNull(localStorage.getItem("coins"), "0"), () => {
        }, MenuAction.SHOW_ALWAYS, 900);
        coinAction.setShouldTranslate(false);
        coinAction._liClass = "coin-counter";
        menu.addAction(coinAction);
        this.coinAction = coinAction;

        return menu;
    }

    onConstruct(args) {
        this.levelCounter = Helper.nonNull(localStorage.getItem("levelCounter"), 1);
        this.levelScaler = () => {
        };
        this.wonParams = {
            aborted: false,
            coinCounterTimer: null,
        };
        this.coinPromise = Promise.resolve();

        let settingsManager = SettingsManager.getInstance();
        let soundManager = SoundManager.getInstance();
        soundManager.set({
            audio: "sound/single_coin_fall_on_concrete_.mp3",
            muted: (settingsManager.getSetting("play-sound", "1") !== "1"),
            volume: 0.7
        }, SoundManager.CHANNELS.SOUND);

        soundManager.resume(SoundManager.CHANNELS.MUSIC);

        return super.onConstruct(args);
    }

    async onFirstStart() {
        super.onFirstStart();
        let leafSegmentTemplate = this.findBy("#segment-leaf-template");
        let parentSegmentTemplate = this.findBy("#segment-parent-template");
        let rowSegmentTemplate = this.findBy("#segment-row-template");
        let triangleTemplate = this.findBy("#segment-triangle-template");

        leafSegmentTemplate.id = null;
        parentSegmentTemplate.id = null;
        rowSegmentTemplate.id = null;
        triangleTemplate.id = null;

        leafSegmentTemplate.remove();
        parentSegmentTemplate.remove();
        rowSegmentTemplate.remove();
        triangleTemplate.remove();

        let self = this;
        let continueButton = this.findBy("#continue-button");
        continueButton.addEventListener("click", () => {
            self.nextLevel();
        });

        let wonText = this.findBy("#won-text");

        let scaleHelper = new ScaleHelper();
        this.continueButtonScaler = await scaleHelper.scaleToFull(continueButton, continueButton.parentElement, false, true, 2);
        this.wonTextScaler = await scaleHelper.scaleToFull(wonText, wonText.parentElement, false, false, 2, null, 5);
        this.wonText = wonText;
        this.wonText.style.fontSize = "0";

        //Benutze Document, da Element außerhalb von Seite (eigentlich unschön!)
        this.levelCounterActionContainer = document.getElementById("level-number-container");
        this.levelCounterAction = document.getElementById("level-number");
        this.levelCounterAction.innerText = this.levelCounter;
        this.levelNumberScaler = await scaleHelper.scaleToFull(this.levelCounterAction, this.levelCounterActionContainer, false, false, 4);
        this.levelCounterActionContainer.classList.add("visible");
        this.templateContainer = new TemplateContainer(leafSegmentTemplate, parentSegmentTemplate, rowSegmentTemplate, triangleTemplate);

        this.coinTemplate = this.findBy("#coin-template");
        this.coinContainer = this.findBy("#coin-container");

        this.coinTemplate.id = null;
        this.coinContainer.removeAllChildren();

        this.findBy("#help-button").addEventListener("click", () => {
            this.help();
        });
        await this.loadLastLevel();
    }

    async loadLastLevel() {
        try {
            let currentLevelInfo = localStorage.getItem("currentLevel");
            if (Helper.isNotNull(currentLevelInfo)) {
                currentLevelInfo = JSON.parse(currentLevelInfo);

                const db = WordRotatorDb.getInstance();
                const levelJson = await db.loadLevel(currentLevelInfo["id"]);

                if (levelJson === null) {
                    return this.nextLevel();
                }

                const level = LevelHelper.inflateLevel(levelJson, this.templateContainer);
                level.setStartRotations(currentLevelInfo["rotations"]);

                const self = this;
                level.getWonPromise().then(() => {
                    self.levelWon(level);
                });

                level.createSegments();
                level.setLocks(currentLevelInfo["locks"]);
                level.getRootSegment()._updateElement();

                level.saveAsCurrentLevel();

                let levelSegment = this.findBy("#level");
                levelSegment.removeAllChildren().appendChild(level.getRootSegment().getElement());
                let scaleHelper = new ScaleHelper();
                this.levelScaler = await scaleHelper.scaleToFull(levelSegment, levelSegment.parentElement, false, false, 1, level.words[0].length * 1.5, null, 0);

                this.level = level;
                let res = this.tutorial();
                Matomo.push(["trackEvent", "LevelSite", "LoadLastLevel"]);
                this.level.checkHasWon();
                return res;
            }
        }
        catch (e) {
            console.error(e);
        }
        return this.nextLevel();
    }

    async nextLevel() {
        try {
            this._siteContent.classList.remove('won');
            this.wonText.style.fontSize = "0";

            const db = WordRotatorDb.getInstance();
            const nextLevelJson = await db.loadNextLevel(LevelSite.RENDERER_TYPES);

                console.log("nextLevelJson", nextLevelJson);
            if (nextLevelJson === null) {
                this.startSite(EndSite);
                this.finish();
                return;
            }
            const level = LevelHelper.inflateLevel(nextLevelJson, this.templateContainer);
            const self = this;
            level.getWonPromise().then(() => {
                self.levelWon(level);
            });

            level.createSegments();
            level.getRootSegment()._updateElement();

            level.saveAsCurrentLevel();

            let levelSegment = this.findBy("#level");
            levelSegment.removeAllChildren().appendChild(level.getRootSegment().getElement());
            let scaleHelper = new ScaleHelper();
            this.levelScaler = await scaleHelper.scaleToFull(levelSegment, levelSegment.parentElement, false, false, 1, level.words[0].length * 1.5, null, 0);

            this.level = level;
            this.levelCounterAction.innerText = this.levelCounter;
            this.levelNumberScaler();

            this.coinAction.setTitle(Helper.nonNull(localStorage.getItem("coins"), "0"));
            this.coinAction.redraw();

            this.wonParams.aborted = true;
            clearTimeout(this.wonParams.coinCounterTimer);

            Matomo.push(["trackEvent", "LevelSite", "NextLevel", "Level Number Normal", this.levelCounter]);

            this.level.checkHasWon();

            return this.tutorial();
        }
        catch (e) {
            console.log("Fehler!");
            console.error(e);
        }
    }

    onStart(args) {
        Matomo.update("Level Site");
        let res = super.onStart(args);

        if (this.levelCounterAction) {

            this.levelCounterAction.innerText = this.levelCounter;
            this.levelCounterActionContainer.classList.add("visible");
        }
        this.levelScaler();

        let settingsManager = SettingsManager.getInstance();
        let soundManager = SoundManager.getInstance();
        soundManager.set({
            audio: "sound/single_coin_fall_on_concrete_.mp3",
            muted: (settingsManager.getSetting("play-sound", "1") !== "1"),
            volume: 0.7
        }, SoundManager.CHANNELS.SOUND);

        return res;
    }

    onPause(args) {
        super.onPause(args);
        this.levelCounterActionContainer.classList.remove("visible");
    }

    async levelWon(level) {
        try {
            const db = WordRotatorDb.getInstance();
            const savePromise = db.saveLevelPlayed(level.getId());

            this.levelCounter++;
            localStorage.setItem("levelCounter", this.levelCounter);

            this._siteContent.classList.add('won');
            localStorage.removeItem("currentLevel");

            let continueButton = this.findBy("#continue-button");
            continueButton.style.transition = "none";
            continueButton.style.opacity = 0;

            this.coinContainer.removeAllChildren();
            let coinsPerLevel = SystemSettings.get("coinsPerLevel", 5);

            let coinsBefore = 0;

            let soundManager = SoundManager.getInstance();
            let audioOptions = soundManager.get(SoundManager.CHANNELS.SOUND);

            this.coinPromise = this.coinPromise.then(() => {
                coinsBefore = parseInt(Helper.nonNull(localStorage.getItem("coins"), "0"));
                localStorage.setItem("coins", coinsBefore + parseInt(coinsPerLevel));
            }).then(() => {
                return Promise.all([new Promise((r) => {
                    setTimeout(() => {
                        r(continueButton.fadeIn());
                    }, 500);
                }), audioOptions.loadedPromise.catch(e => {
                    console.error(e);
                })]);
            });

            this.wonParams.aborted = false;

            for (let i = 0; i < coinsPerLevel; i++) {
                let coinElem = Helper.cloneNode(this.coinTemplate);
                this.coinContainer.appendChild(coinElem);
                this.coinPromise = this.coinPromise.then(() => {
                    return new Promise(r => {
                        let timeout = 350;
                        if (!this.wonParams.aborted) {
                            coinElem.fadeIn(timeout / 1000);
                            soundManager.play(SoundManager.CHANNELS.SOUND);

                            this.wonParams.coinCounterTimer = setTimeout(() => {
                                if (!this.wonParams.aborted) {
                                    this.coinAction.setTitle(++coinsBefore);
                                    this.coinAction.redraw();
                                }
                            }, timeout / 2);
                        }
                        else {
                            r();
                        }
                        //Always do the next promise for garbage collection
                        setTimeout(r, timeout);
                    })
                });
            }

            this.coinPromise = this.coinPromise.catch((e) => {
                console.error(e);
            });

            this.wonTextScaler();
            this.continueButtonScaler();
            this.levelScaler();

            Matomo.push(["trackEvent", "LevelSite", "LevelWon", "Coins", parseInt(Helper.nonNull(localStorage.getItem("coins"), "0"))]);

            await savePromise;
        }
        catch (e) {
            console.error(e);
        }
    }

    help() {
        let cost = SystemSettings.get("costForHelp", 25);
        let currentCoins = parseInt(Helper.nonNull(localStorage.getItem("coins"), 0));

        if (currentCoins >= cost) {
            currentCoins -= cost;
            localStorage.setItem("coins", currentCoins);
            this.coinAction.title = currentCoins;
            this.coinAction.redraw();

            let rotatables = this.level.getRotatableSegments();
            rotatables = rotatables.filter((segment) => {
                return (!segment.isSolved(false));
            });

            let index = Math.floor(Math.random() * rotatables.length);

            let segmentToHelp = rotatables[index];
            while (segmentToHelp.rotation !== 0) {
                segmentToHelp.rotate();
            }
            segmentToHelp.setIsRotatable(false);
            this.level.saveAsCurrentLevel();

            Matomo.push(["trackEvent", "LevelSite", "Help", "Coins", parseInt(Helper.nonNull(localStorage.getItem("coins"), "0"))]);
        }
        else {
            FlashMessenger.addMessage("not-enough-coins");
            Matomo.push(["trackEvent", "LevelSite", "Help", "Not enough Coins", parseInt(Helper.nonNull(localStorage.getItem("coins"), "0"))]);
        }
    }

    async tutorial() {
        if (this.level.id === LevelSite.TUTORIAL.FIRST_LEVEL) {
            let currentStep = Helper.nonNull(localStorage.getItem("tutorial-step"), "1");

            let scaleHelper = new ScaleHelper();
            this._siteContent.classList.add("tutorial");
            this._siteContent.classList.add("step-" + currentStep);

            switch (currentStep) {
                case "1": {
                    this.level.setSegmentClickedListener(() => {
                        this._siteContent.classList.remove("step-1");
                        localStorage.setItem("tutorial-step", "2");
                        this.tutorial();
                    });

                    let textElem = this.findBy(".tutorial-text .step-1");

                    await this.levelScaler();
                    scaleHelper.scaleToFull(textElem, textElem.parentElement, null, true, 1, 2);

                    break;
                }
                case "2": {
                    this.level.setSegmentClickedListener(() => {
                    });
                    this.level.getWonPromise().then(() => {
                        this._siteContent.classList.remove("tutorial");
                        this._siteContent.classList.remove("step-2");
                        localStorage.removeItem("tutorial-step");
                        this.coinPromise = this.coinPromise.then(() => {
                            FlashMessenger.addMessage("extra-coins-after-first-level");
                            localStorage.setItem("coins", parseInt(Helper.nonNull(localStorage.getItem("coins"), "0")) + 50);
                            this.coinAction.setTitle(Helper.nonNull(localStorage.getItem("coins"), "0"));
                            this.coinAction.redraw();
                        });
                        // this.levelScaler();
                    });

                    let textElem = this.findBy(".tutorial-text .step-2");

                    await this.levelScaler();
                    scaleHelper.scaleToFull(textElem, textElem.parentElement, null, true, 1, 2);

                    break;
                }
                default:{
                    this._siteContent.classList.remove("tutorial");
                }
            }
        }
        else if (this.level.id === LevelSite.TUTORIAL.SECOND_LEVEL) {
            let currentStep = Helper.nonNull(localStorage.getItem("tutorial-step"), "3");

            switch (currentStep) {
                case "3": {
                    let scaleHelper = new ScaleHelper();

                    this._siteContent.classList.add("tutorial");
                    this._siteContent.classList.add("step-" + currentStep);

                    let eventListener = () => {
                        this._siteContent.classList.remove("tutorial");
                        this._siteContent.classList.remove("step-3");
                        localStorage.setItem("tutorial-step", "4");
                        this.findBy("#help-button").removeEventListener("click", eventListener);
                        this.levelScaler();
                    };
                    this.findBy("#help-button").addEventListener("click", eventListener);

                    let textElem = this.findBy(".tutorial-text .step-3");

                    await this.levelScaler();
                    scaleHelper.scaleToFull(textElem, textElem.parentElement, null, true, 1, 2);
                    break;
                }
                default:{
                    this._siteContent.classList.remove("tutorial");
                }
            }
        }
        else if (this.level.id === LevelSite.TUTORIAL.BIG_SEGMENT_LEVEL) {
            let currentStep = Helper.nonNull(localStorage.getItem("tutorial-step"), "4");

            switch (currentStep) {
                case "4": {

                    let scaleHelper = new ScaleHelper();
                    this._siteContent.classList.add("tutorial");
                    this._siteContent.classList.add("step-" + currentStep);

                    let rotatableSegments = this.level.getRotatableSegments();
                    let firstSegment = rotatableSegments[0];

                    let pointer = this.findBy("#tutorial-pointer");
                    pointer.remove();
                    firstSegment.element.appendChild(pointer);

                    this.level.setSegmentClickedListener((segment) => {
                        if (firstSegment === segment) {
                            this._siteContent.classList.remove("tutorial");
                            this._siteContent.classList.remove("step-4");
                            localStorage.setItem("tutorial-step", "5");
                            this.levelScaler();
                        }
                    });

                    let textElem = this.findBy(".tutorial-text .step-4");

                    await this.levelScaler();
                    scaleHelper.scaleToFull(textElem, textElem.parentElement, null, true, 1, 2);

                    break;
                }
                default:{
                    this._siteContent.classList.remove("tutorial");
                }
            }
        }
    }
}
LevelSite.RENDERER_TYPES = [20, 40, 60, 100, 120, 140, 160];
LevelSite.TUTORIAL = {
    FIRST_LEVEL: 67,
    SECOND_LEVEL: 15,
    BIG_SEGMENT_LEVEL: 341
};

class MainMenuLevel extends FourWordsLevel{

    constructor(templateContainer) {
        super(templateContainer, 4);
    }

    saveAsCurrentLevel() {
    }

    // checkHasWon(delayPromise) {
    // }
}

class ShareDialog extends Dialog{
    constructor() {
        let viewPromise = ViewInflater.inflate("html/application/dialog/share.html").then(view => {
            view.appendChild(ShareManager.generateDefaultShareElement(window.location.hostname + Helper.basePath("")));
            let closeListener = () => {
                this.close();
            };

            view.querySelectorAll("a").forEach((element) => {
               element.addEventListener("click", closeListener);
            });
            return view;
        });

        super(viewPromise, "share-dialog");
    }
}

class MenuSite extends WordRotatorBaseSite {
    constructor(siteManager) {
        super(siteManager, "html/application/menu.html");
        this.loadLevelPromise = this.loadLevels();
        this.listener = null;
    }

    onStart(args) {
        Matomo.update("Menu Site");
        let res = super.onStart(args);

        let level = new MainMenuLevel(this.templateContainer);
        level.setWords(["WORD", "ROTA", "TORW", "ORDR"]);
        level.createSegments();

        level.getWonPromise().then(() => {
            Matomo.push(["trackEvent", "MainMenu", "levelSolved"]);
            this.startLevelSite();
        });

        let segment = level.getRootSegment();
        segment._updateElement();

        let levelSegment = this.findBy("#level");
        levelSegment.removeAllChildren().appendChild(segment.getElement());

        let rotationsSegments = level.getRotatableSegments();

        let randomRotationFunction = () => {
            let timeout = Math.random() * 4500 + 1500;
            this.randomRotateTimeout = setTimeout(() => {
                let indexBlocked = -1;
                let indexesNotRight = [];
                for (let i = 0; i < rotationsSegments.length; i++) {
                    if (rotationsSegments[i].rotation !== 0) {
                        indexesNotRight.push(i);
                        if (indexesNotRight.length >= 2) {
                            break;
                        }
                    }
                }
                if (indexesNotRight.length === 1) {
                    indexBlocked = indexesNotRight[0];
                }

                let index = Math.floor(Math.random() * rotationsSegments.length);
                if (index === indexBlocked) {
                    index = (index + 1) % rotationsSegments.length;
                }

                rotationsSegments[index].rotate();
                randomRotationFunction();
            }, timeout);
        };
        randomRotationFunction();

        this.listener = async () => {
            let playButton = this.findBy("#play-button");
            let levelNumber = this.findBy("#level-number");
            levelNumber.innerText = Helper.nonNull(localStorage.getItem("levelCounter"), 1);

            let levelSegment = this.findBy("#level");

            let scaleHelper = new ScaleHelper();
            await scaleHelper.scaleToFull(levelSegment, levelSegment.parentElement, false, false, 2, 8, null, false);

            // debugger;
            let levelStyle = getComputedStyle(levelSegment);
            playButton.style.width = levelStyle.getPropertyValue("width");
            scaleHelper.scaleToFull(playButton.children[0], playButton, null, null, null, 4, null, false);

            await scaleHelper.scaleTo(0.2, levelNumber.parentElement, levelNumber.parentElement.parentElement, null, null, null, 10, null, false);
            scaleHelper.scaleToFull(levelNumber, levelNumber.parentElement, false, false, 8, null, null, false);
        };

        this.listener();
        window.addEventListener("resize", this.listener);

        //Musikbuttons update, falls in den Einstellungen umgestellt
        let settingsManager = SettingsManager.getInstance();
        let playSoundButton = this.findBy("#play-sound");
        playSoundButton.checked = (settingsManager.getSetting("play-sound", "1") === "1");
        let playMusicButton = this.findBy("#play-music");
        playMusicButton.checked = (settingsManager.getSetting("play-music", "1") === "1");

        return res;
    }

    async startLevelSite() {
        SoundManager.getInstance().resumeContext();
        this.startSite(LevelSite, Promise.race([this.loadLevelPromise, new Promise(async resolve => {
            const db = WordRotatorDb.getInstance();
            let level = await db.loadNextLevel(LevelSite.RENDERER_TYPES);
            if (level !== null) {
                resolve();
            }
        })]));
    }

    async onFirstStart() {
        super.onFirstStart();

        let playButton = this.findBy("#play-button");
        playButton.addEventListener("click", () => {
            Matomo.push(["trackEvent", "MainMenu", "startButton"]);
            this.startLevelSite();
        });

        let leafSegmentTemplate = this.findBy("#segment-leaf-template");
        let parentSegmentTemplate = this.findBy("#segment-parent-template");
        let rowSegmentTemplate = this.findBy("#segment-row-template");
        let triangleTemplate = this.findBy("#segment-triangle-template");

        leafSegmentTemplate.id = null;
        parentSegmentTemplate.id = null;
        rowSegmentTemplate.id = null;
        triangleTemplate.id = null;

        leafSegmentTemplate.remove();
        parentSegmentTemplate.remove();
        rowSegmentTemplate.remove();
        triangleTemplate.remove();

        this.templateContainer = new TemplateContainer(leafSegmentTemplate, parentSegmentTemplate, rowSegmentTemplate, triangleTemplate);

        if (Helper.nonNull(MenuSite.app._cookieClosePromise)) {
            MenuSite.app._cookieClosePromise.then(() => {
                if (this.listener) {
                    this.listener();
                }
            });
        }

        let settingsManager = SettingsManager.getInstance();
        let soundManager = SoundManager.getInstance();

        let playMusicButton = this.findBy("#play-music");
        playMusicButton.checked = (settingsManager.getSetting("play-music", "1") === "1");
        playMusicButton.addEventListener("change", () => {
            settingsManager.setSetting("play-music", (playMusicButton.checked)?"1":"0");
            soundManager.set({muted: !playMusicButton.checked}, SoundManager.CHANNELS.MUSIC);
            if (playMusicButton.checked) {
                soundManager.play(SoundManager.CHANNELS.MUSIC);
            }
            Matomo.push(["trackEvent", "MainMenu", "PlayMusic", "Play Music", (playMusicButton.checked) ? 1 : 0]);
        });

        let playSoundButton = this.findBy("#play-sound");
        playSoundButton.checked = (settingsManager.getSetting("play-sound", "1") === "1");
        playSoundButton.addEventListener("change", () => {
            settingsManager.setSetting("play-sound", (playSoundButton.checked)?"1":"0");
            soundManager.set({muted: !playSoundButton.checked}, SoundManager.CHANNELS.SOUND);
            Matomo.push(["trackEvent", "MainMenu", "PlaySound", "Play Sound", (playSoundButton.checked) ? 1 : 0]);
        });

        this.findBy("#share-button").addEventListener("click", () => {
            new ShareDialog().show();
        });
        // this.findBy("#share-buttons").appendChild(ShareManager.generateDefaultShareElement("https://wordrotator.silas.link"));
    }

    onPause(args) {
        clearTimeout(this.randomRotateTimeout);
        window.removeEventListener("resize", this.listener);
        super.onPause(args);
    }

    async loadLevels() {
        const dateLastSync = Helper.nonNull(localStorage.getItem("date-last-sync"), 0);
        const db = WordRotatorDb.getInstance();

        // let numberLevels = db.countLevels();
        let newLastSync = null;
        let maxRuns = 1;
        let levelPromises = [];
        for (let run = 0; run < maxRuns; run++) {
            let res = await DataManager.load("wordRotator/levels" + DataManager.buildQuery({
                "currentRun": run,
                "dateLastSync": dateLastSync
            }));
            if (!res["success"]) {
                if (await db.loadNextLevel(LevelSite.RENDERER_TYPES) === null) {
                    FlashMessenger.addMessage("sync-error", null, 6000);
                }
                newLastSync = null;
                break;
            }
            res = res["result"];
            newLastSync = Helper.nonNull(newLastSync, res["currentSyncDate"]);
            maxRuns = res["maxRuns"];

            let levels = res["levels"];
            for (let i = 0; i < levels.length; i++) {
                let currentLevel = levels[i];
                levelPromises.push(db.loadLevel(levels[i]["id"]).then(level => {
                    currentLevel["played"] = (Helper.nonNull(Helper.nonNull(level, {}).played, false));
                    return currentLevel;
                }));
            }
        }
        let levels = await Promise.all(levelPromises);
        console.log("levels to save", levels);
        await db.saveManyLevels(levels);

        if (newLastSync != null && newLastSync !== "null") {
            localStorage.setItem("date-last-sync", newLastSync);
        }
    }
}

MenuSite.app = null;
InitPromise.addPromise(app => {
    MenuSite.app = app;
});

class PrivacyPolicySite extends WordRotatorBaseSite {
    constructor(siteManager) {
        super(siteManager, "html/application/privacyPolicy.html", "privacyPolicy");
    }

    onFirstStart() {
        let trackSwitch =this.findBy("#track-switch");

        trackSwitch.addEventListener("change", function (e) {
            Matomo.setTrack(this.checked === true);
            e.stopPropagation();
            e.preventDefault();
        });
        super.onFirstStart();
    }

    onStart(args) {
        let trackSwitch =this.findBy("#track-switch");
        let shouldTrack = (Helper.nonNull(localStorage.getItem("matomoShouldTrack"), "1") === "1");
        trackSwitch.checked = shouldTrack;

        Matomo.update("Privacy Policy Site");
        return super.onStart(args);
    }
}

InitPromise.addPromise(app => {
    app.addDeepLink("privacyPolicy", PrivacyPolicySite);
});

class CreditsSite extends WordRotatorBaseSite{
    constructor(siteManager) {
        super(siteManager, "html/application/credits.html", "credits");
    }

    onStart(args) {
        Matomo.update("Credits Site");
        return super.onStart(args);
    }
}

InitPromise.addPromise(app => {
    app.addDeepLink("credits", CreditsSite);
});

class ChooseThemeDialog extends Dialog {

    constructor() {
        let viewPromise = ViewInflater.inflate("html/application/dialog/chooseTheme.html").then(view => {

            let template = view.querySelector("#choose-theme-template");
            template.remove();
            template.id = null;

            let themeTemplateContainer = view.querySelector("#theme-choose-container");

            for (let i = 0; i < ThemeManager.themes.length; i++) {
                let themeElem = Helper.cloneNode(template);
                let theme = ThemeManager.themes[i];
                themeElem.querySelector(".name").appendChild(Translator.makePersistentTranslation(theme._name));
                themeElem["dataset"]["theme"] = theme._name;

                themeElem.addEventListener("click", () => {
                    this.result = themeElem["dataset"]["theme"];
                    this.close();
                });
                themeTemplateContainer.appendChild(themeElem);
            }

            return view;
        });

        super(viewPromise, "choose-theme-dialog-title");
    }
}

class ImpressumSite extends WordRotatorBaseSite{
    constructor(siteManager) {
        super(siteManager, "html/application/impressum.html", "impressum");
    }
}

InitPromise.addPromise(app => {
    app.addDeepLink("impressum", ImpressumSite);
});

class PersistDialog extends ConfirmDialog{
    constructor() {
        super("persist-storage-dialog-message", "persist-storage-dialog-title");
    }
}

class WordRotatorSettingFragment extends LocalStorageSettingsFragment {
    constructor(site) {
        super(site, "html/application/fragment/settings.html");
    }

    onFirstStart() {
        let currentThemeName = ThemeManager.currentTheme._name;
        SettingsManager.getInstance().setSetting("theme", currentThemeName);

        let themeNameElem = this.findBy("#theme-name");
        themeNameElem.removeAllChildren().appendChild(Translator.makePersistentTranslation(currentThemeName));
        this.findBy("#theme-chooser").addEventListener("click", async () => {
            let newTheme = await (new ChooseThemeDialog()).show();
            if (Helper.isNotNull(newTheme)) {
                SettingsManager.getInstance().setSetting("theme", newTheme);
                ThemeManager.changeCurrentTheme(newTheme);
                themeNameElem.removeAllChildren().appendChild(Translator.makePersistentTranslation(newTheme));
            }
        });

        this.findBy("#reset-levels").addEventListener("click", () => {
            localStorage.removeItem("currentLevel");
            localStorage.removeItem("date-last-sync");
            localStorage.removeItem("levelCounter");
            localStorage.removeItem("tutorial-step");
            WordRotatorDb.getInstance().removeAll(WordRotatorDb.OBJECT_STORE.LEVEL);
        });

        if (location.hostname.includes("beta") || location.hostname.includes("127.0.0.1")) {
            this.findBy("#reset-levels").classList.remove("hidden");
        }

        let playMusicButton = this.findBy("#play-music");
        playMusicButton.addEventListener("change", () => {
            let soundManager = SoundManager.getInstance();
            soundManager.set({muted: !playMusicButton.checked}, SoundManager.CHANNELS.MUSIC);
            if (playMusicButton.checked) {
                soundManager.play(SoundManager.CHANNELS.MUSIC);
            }
        });

        this.findBy("#track-switch").addEventListener("change", function (e) {
            Matomo.setTrack(this.checked === true);
            e.stopPropagation();
            e.preventDefault();
        });

        this.findBy("#credits-button").addEventListener("click", () => {
            this.getSite().startSite(CreditsSite);
        });
        this.findBy("#privacy-policy-button").addEventListener("click", () => {
            this.getSite().startSite(PrivacyPolicySite);
        });
        this.findBy("#contact-button").addEventListener("click", () => {
            this.getSite().startSite(ContactSite);
        });
        this.findBy("#impressum-button").addEventListener("click", () => {
            this.getSite().startSite(ImpressumSite);
        });

        InstallManager.setCanInstallListener(() => {
            let installButton = this.findBy("#install-button");
            installButton.addEventListener("click", () => {
                installButton.classList.add("hidden");
                InstallManager.prompt().then((e) => {
                    console.log("clicked", e);
                    if (e["outcome"] === "accepted") {
                        Matomo.trackEvent("installed", "installed");
                    }
                });
            });
            installButton.classList.remove("hidden");
        });

        let storageManager = MyStorageManager.getInstance();
        if (storageManager.canPersist()) {
            Promise.all([storageManager.isPersistent(), navigator.serviceWorker["ready"]]).then(res => {
                let isPersisted = res[0];
                let storageObject = this.findBy("#storage-info");
                storageObject.appendChild(Translator.makePersistentTranslation("storage-info", [
                    ((isPersisted) ? "" : "nicht")]));
                storageObject.parentElement.classList.remove("hidden");
                storageObject.parentElement.addEventListener("click", async () => {
                    if (!isPersisted) {
                        try {
                            let shouldAskForNotifications = await (new PersistDialog().show());
                            if (shouldAskForNotifications) {
                                let sub = await res[1]["pushManager"]["subscribe"]({
                                    "userVisibleOnly": true,
                                    "applicationServerKey": new Uint8Array([0x4, 0x53, 0xb7, 0x8d, 0xc6, 0xd1, 0x1a, 0xd4, 0x8b, 0xb2, 0xeb, 0x82, 0xf0, 0x9e, 0x12, 0xf6, 0xd, 0x32, 0x18, 0xa, 0x35, 0xf, 0x2d, 0x4c, 0x5, 0x29, 0x15, 0x95, 0x23, 0xb1, 0xd3, 0xab, 0x87, 0x88, 0x85, 0x1d, 0xc0, 0x98, 0x6e, 0x65, 0xe3, 0xcb, 0xa2, 0x28, 0x63, 0x7, 0x34, 0x9b, 0xfa, 0x46, 0x9a, 0x49, 0xcc, 0x70, 0x7a, 0xdd, 0xbe, 0x1e, 0xfc, 0xde, 0xcc, 0xb3, 0x5b, 0xcb, 0xf4])
                                });
                            }
                        } catch (e) {
                            if ('Notification' in window && Notification.permission === 'denied') {
                                FlashMessenger.addMessage('notification-permission-denied');
                            }
                        }

                        isPersisted = await storageManager.isPersistent();
                        storageObject.removeAllChildren().appendChild(Translator.makePersistentTranslation("storage-info", [
                            ((isPersisted) ? "" : "nicht")]));

                    }
                    // storageManager.persist().then(isPersisted => {
                    //     storageObject.removeAllChildren().appendChild(Translator.makePersistentTranslation("storage-info", [
                    //         Math.round(storage.usage / (1024 * 1024) * 100) / 100,
                    //         Math.round(storage.quota / (1024 * 1024) * 100) / 100,
                    //         Math.round(storage.usage / storage.quota * 10000) / 100,
                    //         ((isPersisted) ? "" : "nicht")]));
                    //
                    //     console.log("p", isPersisted);
                    //     if (!isPersisted){
                    //         FlashMessenger.addMessage("storage-permission-not-get");
                    //     }
                    //     else {
                    //         FlashMessenger.addMessage("storage-permission-get");
                    //     }
                    // });
                });
            });
        }
        return super.onFirstStart();
    }

    onStart() {
        Matomo.update("Settings Site");
        super.onStart();
    }
}

InitPromise.addPromise(function () {
    SettingsSite.addSettingsFragment("settings", WordRotatorSettingFragment);
});

class SelectWordsSite extends UserSite{

    constructor(siteManager) {
        super(siteManager, "version/2/html/selectWords.html", null, "select-words");
    }

    async onConstruct(args) {
        let res = await super.onConstruct(args);
        this.stats = (await DataManager.load("words"))["result"];
        this.words = this.stats["wordsToCheck"];
        console.log(this.stats);
        return res;
    }

    onFirstStart() {
        super.onFirstStart();
        this.findBy("#not-checked").appendChild(document.createTextNode(this.stats["wordsNotChecked"]));
        this.findBy("#checked").appendChild(document.createTextNode(this.stats["wordsChecked"]));
        this.findBy("#not-sure").appendChild(document.createTextNode(this.stats["wordsUnsure"]));
        this.findBy("#deleted").appendChild(document.createTextNode(this.stats["wordsDeleted"]));
        this.findBy("#unused").appendChild(document.createTextNode(this.stats["wordsNotUsed"]));

        let template = this.findBy("#word-template");
        template.id = null;
        template.remove();

        let container = this.findBy("#word-container");

        let numWords = this.words.length;
        for (let i = 0; i < numWords; i++) {
            let wordElement = Helper.cloneNode(template);
            wordElement.dataset["id"] = -1;
            this.setWord(wordElement, this.words[i]);
            container.appendChild(wordElement);

            wordElement.querySelector(".button-ok").addEventListener("click", async () => {
                let newWord = (await DataManager.send("checkWord", {
                    "wordId":wordElement.dataset["id"],
                    "action":"1"
                }))["result"];
                this.setWord(wordElement, newWord[0]);
            });

            wordElement.querySelector(".button-unsure").addEventListener("click", async () => {
                let newWord = (await DataManager.send("checkWord", {
                    "wordId":wordElement.dataset["id"],
                    "action":"2"
                }))["result"];
                this.setWord(wordElement, newWord[0]);
            });

            wordElement.querySelector(".button-delete").addEventListener("click", async () => {
                let newWord = (await DataManager.send("checkWord", {
                    "wordId":wordElement.dataset["id"],
                    "action":"3"
                }))["result"];
                this.setWord(wordElement, newWord[0]);
            });
        }
    }

    setWord(wordElement, word){
        wordElement.querySelector(".word").removeAllChildren().appendChild(document.createTextNode(word["word"]));
        wordElement.dataset["id"] = word["id"];
    }
}

InitPromise.addPromise(app => {
    app.addDefaultAction(new UserAction("select-words", () => {
        app.startSite(SelectWordsSite);
    }, null, null, "select-words"));
});

class DeleteWordsSite extends UserSite {
    constructor(siteManager) {
        super(siteManager, "version/2/html/deleteLevels.html", null, "admin");
    }

    async onConstruct(args) {
        let res = super.onConstruct(args);
        this.words = (await DataManager.load("getDoubleUsedWordsAction"))["result"];
        return res;
    }


    onFirstStart() {
        super.onFirstStart();

        let levelTemplate = this.findBy("#level-template");
        let wordTemplate = this.findBy("#word-template");
        let wordContainer = this.findBy("#word-container");

        levelTemplate.id = null;
        levelTemplate.remove();

        wordTemplate.id = null;
        wordTemplate.remove();

        for (let k in this.words) {
            let wordElem = Helper.cloneNode(wordTemplate);
            wordElem.querySelector(".name").appendChild(document.createTextNode(k));
            let levelContainer = wordElem.querySelector(".level-container");
            for (let j = 0; j < this.words[k].length; j++) {
                let level = this.words[k][j];
                let levelElem = Helper.cloneNode(levelTemplate);
                levelElem.querySelector(".id").appendChild(document.createTextNode(level["id"]));
                levelElem.querySelector(".words").appendChild(document.createTextNode(level["words"]));
                levelElem.querySelector(".positions").appendChild(document.createTextNode(level["rotations"]));

                levelElem.querySelector(".delete-button").addEventListener("click", async () => {
                    let res = await DataManager.send("deleteLevel", {"levelId": level["id"]});
                    if (res["success"]){
                        levelElem.remove();
                    }
                });

                levelContainer.appendChild(levelElem);
            }
            wordContainer.appendChild(wordElem);
        }
    }
}

InitPromise.addPromise(app => {
    app.addDefaultAction(new UserAction("delete-levels", () => {
        app.startSite(DeleteWordsSite);
    },null, null, "admin"));
});

let basePath = "/pwa/wordRotator/public/";
if (window.location.pathname.indexOf("publicTest/") >= 0)
{
    basePath = "/pwa/wordRotator/publicTest/";
}

SystemSettings.setBasePath(basePath);
Translator.supportedLanguages = ["de"];
Translator.markTranslations = false;

Matomo.SIDE_ID = "2";

window.onerror = (e, u, l) => {
    console.error(e, u, l);
};

applyPolyfills();

ThemeManager.addTheme(new Theme('red', 'red'));
ThemeManager.addTheme(new Theme("blue", "blue"));
ThemeManager.addTheme(new Theme("black", "black"));
ThemeManager.addTheme(new Theme("green", "green"));
ThemeManager.addTheme(new Theme("pink", "pink"));
ThemeManager.addTheme(new Theme("dark", "dark"));

ShareManager.addShareButton(new MatomoShareButton(new WhatsappShareButton('img/whatsapp.svg'), "whatsapp", true));
ShareManager.addShareButton(new MatomoShareButton(new SmsShareButton('img/sms.svg'), "sms", true));
ShareManager.addShareButton(new MatomoShareButton(new TelegramShareButton('img/telegram.svg'), "telegram", true));
// ShareManager.addShareButton(new CopyShareButton('img/copy.svg'));

let app$1 = new App();

AndroidBridge.addDefinition(() => {
    window["app"] = app$1;
    window["app"]["pause"] = app$1.pause;
    window["app"]["resume"] = app$1.resume;
    window["app"]["setAppEndListener"] = app$1.setAppEndListener;
});

SettingsSite.setTemplate("html/application/setting-template.html");
// SettingsSite.shouldAddSettingsAction = false;

RegistrationSite.addAction = false;
LoginSite.addLoginAction = false;

InitPromise.resolve(app$1).then(async function () {
    SettingsSite.settingsAction.showFor = MenuAction.SHOW_ALWAYS;

    let settingsManager = SettingsManager.getInstance();

    let soundManager = SoundManager.getInstance();
    soundManager.play(SoundManager.CHANNELS.MUSIC, {
        audio: "sound/brightAndBeautifull__.mp3",
        loop: true,
        volume: 0.6,
        muted: (settingsManager.getSetting("play-music", "1") !== "1")
    }).catch(e => console.error(e));

    app$1.start(MenuSite);
    Translator.setLanguage("de");

    InstallManager.setCanInstallListener(e => {});
    if (InstallManager.isInstalled()){
        MyStorageManager.getInstance().persist();
    }

    window["applyAndroidBridge"] = AndroidBridge.applyDefinitions;
});

// console.log("trying push... 1");
// navigator.serviceWorker["ready"].then(function (registration) {
//     console.log("trying push... 2");
//     registration["pushManager"]["subscribe"]({
//         "userVisibleOnly": true,
//         "applicationServerKey": new Uint8Array([0x4, 0x53, 0xb7, 0x8d, 0xc6, 0xd1, 0x1a, 0xd4, 0x8b, 0xb2, 0xeb, 0x82, 0xf0, 0x9e, 0x12, 0xf6, 0xd, 0x32, 0x18, 0xa, 0x35, 0xf, 0x2d, 0x4c, 0x5, 0x29, 0x15, 0x95, 0x23, 0xb1, 0xd3, 0xab, 0x87, 0x88, 0x85, 0x1d, 0xc0, 0x98, 0x6e, 0x65, 0xe3, 0xcb, 0xa2, 0x28, 0x63, 0x7, 0x34, 0x9b, 0xfa, 0x46, 0x9a, 0x49, 0xcc, 0x70, 0x7a, 0xdd, 0xbe, 0x1e, 0xfc, 0xde, 0xcc, 0xb3, 0x5b, 0xcb, 0xf4])
//     }).then(function (sub) {
//         console.log("trying push... 3");
//         console.log("Subscription", sub);
//     }).catch(function (e) {
//         // if ('Notification' in window && Notification.permission === 'denied') {
//         //     console.warn('Permission for notifications was denied');
//         // } else {
//         //     console.error('Unable to subscribe to push', e);
//         // }
//     });
// });
