// Import the Lasal Runtime base element into your component
// Import SigPolymer to use Polymer methods: html, afterNextRender, beforeNextRender, debouncer, timeOut
// or if your component's getter template() returns a HTML literal
import { LasalRuntimeSigElement, SigPolymer } from '../../sigmatek/sig-element/sig-element.js';

import '../sig-control-scrollbar/sig-control-scrollbar-css.js'

/****************************************************************************************************
 * @class LasalRuntimeSigRecipeManagerElement
 * @extends {LasalRuntimeSigElement}
 ****************************************************************************************************/
class LasalRuntimeSigRecipeManagerElement extends LasalRuntimeSigElement {

    /****************************************************************************************************
     * When the element is loaded, importMeta() assigns reference to the path from 
     * which an element was imported to its importPath property. 
     * Relative URLs in styles are automatically re-written to be relative to the importPath property. 
     * You can use [[importPath]] in your component to load url resources
     ****************************************************************************************************/
    static get importMeta() {
        return import.meta;
    }

    /****************************************************************************************************
     * Return the component style and template as a template literal
     ****************************************************************************************************/
    static get template() {
        return SigPolymer.html`
        <style include="sig-element-css sig-control-scrollbar-css">
            :host {
                /* Basic styling of the component */
                background-color: var(--theme-sig-control-recipe-manager-background-color, inherit);

                border-width: var(--theme-sig-control-recipe-manager-border-width, 0px);
                border-color: var(--theme-sig-control-recipe-manager-border-color, inherit);
                border-style: var(--theme-sig-control-recipe-manager-border-style, none);
                border-radius: var(--theme-sig-control-recipe-manager-border-radius, 0px);

                /* Basic box model settings */
                display: block;
                position: absolute;
                overflow: hidden;
            }

            :host([isdesignmode]:hover) {
                background-color: var(--theme-sig-control-recipe-manager-background-color, rgba(255, 255, 255, .3));
            }

            :host * {
                /* Reset margin and paddings for child elements and turn of rectangle selection */
                margin: 0;
                padding: 0;
            }

            .clearfix:after {
                @apply --clearfix
            }

            .sig-recipe-manager {
                width: 100%;
                height: 100%;
            }

            #originalCCContainer {
                visibility: hidden;
                position: absolute;
            }

            #errorMsg {
                position: absolute;
                height: 100%;
                width: 100%;
                background-color: black;
                display: none;
            }

            #progress {
                position: absolute;
                height: 100%;
                width: 100%;
                background-color: black;
                top: 0;
                color: white;
            }

            #lineList {
                height: 100%;
                width: 100%;
                overflow: auto;
            }

            sig-composite-control {
                position: relative !important;
            }

            .hide {
                display: none;
            }

            .hideLine {
                display: none;
            }

            .slotContainer {
                display: block;
                position: relative;
            }

            /* If the composites are positions absolutely, it makes working with them difficoult */
            :host ::slotted(sig-composite-control) {
                position: relative !important;

            }
        </style>
        <div class="sig-recipe-manager clearfix">
            <div id=originalCCContainer>
                <slot name="recipeline"></slot>
            </div>
            <div id="lineList" on-scroll="_scrollHandler"></div>
            <div id="errorMsg"></div>
            <div id="progress" class="hide"></div>
        </div>
        `;
    }

    /****************************************************************************************************
     * define the custom tag name 
     ****************************************************************************************************/
    static get is() {
        return "sig-control-recipe-manager";
    }

    /****************************************************************************************************
     * define the main porperties of the component
     * reflected property names should always be lower case so it can be used directly on the element as an attribute
     ****************************************************************************************************/
    static get properties() {
        return {
            loadwindow: {
                type: Object,
                value: () => { return {} },
                observer: '_loadChanged'
            },
            loadbuttonname: {
                type: String,
                value: "",
                observer: '_loadChanged'
            },
            savewindow: {
                type: Object,
                value: () => { return {} },
                observer: "_saveChanged"
            },
            savebuttonname: {
                type: String,
                value: "",
                observer: "_saveChanged"
            },
            saverecvalsbuttonname: {
                type: String,
                value: "",
                observer: "_saveChanged"
            },            
            applybuttonname: {
                type: String,
                value: "",
                observer: "_applyChanged"
            },
            applyfilterbuttonname: {
                type: String,
                value: "",
                observer: "_applyFilterChanged"
            },
            refreshbuttonname: {
                type: String,
                value: "",
                observer: "_refreshChanged"
            },
            filename: {
                type: String,
                value: undefined
            },
            userdefinedbitsbuttonname: {
                type: String,
                value: "",
                observer: "_userDefinedBitsChanged"
            },
            userdefinedbitswindow: {
                type: Object,
                value: () => { return {} },
                observer: "_userDefinedBitsChanged"
            },
            filter1: {
                type: Number,
                value: 0
            },
            filter11: {
                type: Number,
                value: 0,
                observer: '_updateFilter',
                notify: true
            },
            filter12: {
                type: Number,
                value: 0,
                observer: '_updateFilter',
                notify: true
            },
            filter2: {
                type: Number,
                value: 0
            },
            filter21: {
                type: Number,
                value: 0,
                observer: '_updateFilter',
                notify: true
            },
            filter22: {
                type: Number,
                value: 0,
                observer: '_updateFilter',
                notify: true
            },
            progressbarname: {
                type: String,
                value: "",
                observer: "_progressBarChanged"
            },
            processresult : {
                type: Number,
                value: undefined,
                notify:true
            },
            filterinputname: {
                type: String,
                value: "",
                observer: "_filterInputChanged"
            },
            sortenumname: {
                type: String,
                value: "",
                observer: "_sortEnumChanged"
            },
            sortenumtextscheme: {
                type: String,
                value: ""
            },
            modifiedstate: {
                type: Number,
                value: 0,
                notify: true
            }
        }
    }

    /****************************************************************************************************
     * called when the element has been created, but before property values are set and local DOM is initialized
     ****************************************************************************************************/
    constructor() {
        super();
        this.parent = undefined;
        this.parentVM = undefined;

        //the composite control that was rendered into the recipeline slot
        this.compositeControl = undefined;

        //The checksum used for the temporary recipe file
        this.tmpFileCRC = 0xFFFFFFFF;

        //The VM and bounds of the composite control
        this.compositeControlVM = undefined;
        this.compositeControlBounds = undefined;

        //The Bounds of the line list
        this.lineListHeight = undefined;
        //The recipe that is currently loaded, in the order it was loaded
        this.currentRecipe = [];
        this.originalRecipe = [];
        this.showRecipe = []; // what is shown (hiding datapoints locked via checkbit)
        // this.lastLoadedFilePath = "c:/userdata/recipe";
        this.lastLoadedFilePath = "";
        this.lastLoadedRecipeHeaderText = "";

        this.userdefinedcrc = -1;

        this.updateDebouncer = undefined;


        this.totalHeight = undefined;
        this.maxNoLines = undefined;

        //The line divs
        this.lines = [];

        //The HTML elements that are put above and below the lines to 
        //ensure the scroll height
        this.fakeScrollTop = undefined;
        this.fakeScrollBot = undefined;

        //The buttons used to control the recipe manager
        this.loadButton = undefined;
        this.refreshButton = undefined;
        this.applyButton = undefined;
        this.applyFilterButton = undefined;
        this.filterInput = undefined;
        this.saveButton = undefined;
        this.udbButton = undefined;
        this.sortEnum = undefined;

        //The components of the load window
        this.sigLoadFileManager = undefined;
        this.sigLoadOK = undefined;
        this.sigLoadCancel = undefined;

        this.loadWindowRef = undefined;
        this.loadWindow = undefined;

        //the components of the save window
        this.sigSaveFileManager = undefined;
        this.sigSaveOK = undefined;
        this.sigSaveCancel = undefined;
        this.sigSaveInput = undefined;

        this.saveWindowRef = undefined;
        this.saveWindow = undefined;

        //the components of the user defined bits window
        //UDB == userDefinedBits
        this.sigUDBfilter1 = undefined;
        this.sigUDBfilter2 = undefined;
        this.sigUDBOK = undefined;
        this.sigUDBCancel = undefined;

        this.udbWindowRef = undefined;
        this.udbWindow = undefined;

        this.progressBar = undefined;

        this.eventHandler = {
            _udbButtonClickHandler: undefined,
            _loadButtonClickHandler: undefined,
            _applyButtonClickHandler: undefined,
            _applyFilterButtonClickHandler: undefined,
            _refreshButtonClickHandler: undefined,
            _saveButtonClickHandler: undefined,
            _sigLoadCancelTapHandler: undefined,
            _sigLoadOKTapHandler: undefined,
            _sigSaveCancelTapHandler: undefined,
            _sigSaveOKTapHandler: undefined,
            _sigUDBCancelTapHandler: undefined,
            _sigUDBOKTapHandler: undefined
        }

        // error offsets for process result
        this.errorOffsetRecipeApi = -1000;
        this.errorOffsetFilemanApi = -2000;  

        // create shortcuts to the common APIs - this makes the code more readable
        if (window.sigApi) {
            this.vmApi = window.sigApi.vm;
            this.fmApi = window.sigApi.fileManager;
            this.rmApi = window.sigApi.recipeManager;
            this.wmApi = window.sigApi.windowManager;
            this.checkbitApi = window.sigApi.checkbitManager;
        } else {
            this._log('constructor(): API not ready!', 'ERROR');
        }
    }

    /****************************************************************************************************
     *  called after property default values are set and local DOM is initialized
     ****************************************************************************************************/
    ready() {
        super.ready();
        // We changed the init, so that we don't need an actual height of the component
        // As long as the CSS variables are set.
        // So we can call it here.
        this._init();
    }

    /****************************************************************************************************
     * Called when the component was rendered, and has a useable height and width
     ****************************************************************************************************/
    _init() {
        if (!this.isdesignmode) {
            this._initVars();
        }

        this._initEventHandler();

        if (this.filename) {
            this._loadRecipe(this.filename);
        }

        this.updateDebouncer = SigPolymer.debouncer.debounce(this.updateDebouncer, SigPolymer.timeOut.after(100), () => {
            this._updateElements();
        })

        //Gets the Load Button, adds the eventlisteners
        //makes sure the button does what it should
        if (this.loadbuttonname && this.loadwindow) {   
            this._setupLoad();
        }
        if (this.refreshbuttonname) {  
            this._setupRefresh();
        }
        if (this.applybuttonname) { 
            this._setupApply();
        }
        if (this.applyfilterbuttonname) { 
            this._setupApplyFilter();
        }
        if (this.savebuttonname && this.savewindow) { 
            this._setupSave();
        }
        if (this.userdefinedbitsbuttonname && this.userdefinedbitswindow) {            
            this._setupUserDefinedBits();
        }
        if (this.progressbarname) {            
            this._setupprogress();
        }
        if (this.filterinputname) {            
            this._setupFilterInput();
        }
        if (this.sortenumname) {            
            this._setupSortEnum();
        }

        this._addEventListeners();
    }

    /****************************************************************************************************
     * Static event listeners are added here
     *
     ****************************************************************************************************/
    _addEventListeners() {
        this.sigAddEventListener('LANGUAGE_CHG', () => {
            this._languageChanged();
        }, 'runtimeEvent');
    }

    /****************************************************************************************************
     * handles language changes
     *
     * @returns
     ****************************************************************************************************/
    async _languageChanged() {
        if(!this.sortEnum) return;
        const valueoptions =  await this._getSortEnumValueOptions();
        this.sortEnum.valueoptions = valueoptions;
        
    }

    /****************************************************************************************************
     * Handles changes to the load setup (button, window)
     * 
     ****************************************************************************************************/
    _loadChanged() {
        if (this.loadbuttonname && this.loadwindow) {
            this._setupLoad();
        }
    }
    /****************************************************************************************************
     * handles changes to the refresh setup (button)
     *
     ****************************************************************************************************/
    _refreshChanged() {
        if (this.refreshbuttonname) {
            this._setupRefresh();
        }
    }
    /****************************************************************************************************
     * handles changes to the apply setup (button)
     *
     ****************************************************************************************************/
    _applyChanged() {
        if (this.applybuttonname) {
            this._setupApply();
        }
    }
    /****************************************************************************************************
     * Handles changes to the filter setup
     *
     ****************************************************************************************************/
    _applyFilterChanged() {
        if (this.applyfilterbuttonname) {
            this._setupApplyFilter();
        }
    }
    /****************************************************************************************************
     * handles changes to the save setup
     *
     ****************************************************************************************************/
    _saveChanged() {
        if ((this.savebuttonname || this.saverecvalsbuttonname) && this.savewindow) {
            this._setupSave();
        }
    }
    /****************************************************************************************************
     * handles changes to the userDefinedBits setup
     *
     ****************************************************************************************************/
    _userDefinedBitsChanged() {
        if (this.userdefinedbitsbuttonname && this.userdefinedbitswindow) {
            this._setupUserDefinedBits();
        }
    }
    /****************************************************************************************************
     * Handles changes to the progress bar setup
     * 
     ****************************************************************************************************/
    _progressBarChanged() {
        if (this.progressbarname) {
            this._setupprogress();
        }
    }
    /****************************************************************************************************
     * handles changes to the filter setup
     * 
     ****************************************************************************************************/
    _filterInputChanged() {
        if (this.filterinputname) {
            this._setupFilterInput();
        }
    }
    /****************************************************************************************************
     * handles changes to the sortEnum setup
     *
     ****************************************************************************************************/
    _sortEnumChanged() {
        if (this.sortenumname) {
            this._setupSortEnum();
        }
    }

    /****************************************************************************************************
     * Gets the sort enum
     * add the eventListener
     ****************************************************************************************************/
    async _setupSortEnum() {
        if (this.sortenumname && this.parentVM) {
            const sortEnumVM = this.parentVM.getChildByName(this.sortenumname);
            if (sortEnumVM) {
                const sortEnum = sortEnumVM.getDomRef();
                this.sortEnum = sortEnum;
                 
                //Get the texts for the enum list from the text scheme
                //and parse it in a way to be used in the enum
                // [{name, id}, {name, id} ...]
                const sortEnumValueOptions = await this._getSortEnumValueOptions();
                //set the enum list texts
                this.sortEnum.valueoptions = sortEnumValueOptions;
                //set the enum value to 0 -> select the first element in the list, which is 'no sort'
                this.sortEnum.value = 0;

                this.sigAddEventListener('value-changed', () => {
                    this._sortListFromEnumSettings();
                }, 'jsEvent', this.sortEnum);
            }
        }
    }

    /****************************************************************************************************
     * Sorts the currentRecipe according to the enum setting
     ****************************************************************************************************/
    _sortListFromEnumSettings() {
        //Don't try to sort if there isn't a sort enum
        if(!this.sortEnum) {
            return;
        }
        switch (Number(this.sortEnum.value)) {
            case 1:
                this._sort('datapointText1', true);
                break;
            case 2:
                this._sort('datapointText1');
                break;
            case 0:
            default: //Default: no sort
                this._sort('idx', true);
        }
    }

    /****************************************************************************************************
     * gets the value options of the sortEnum, from a text scheme
     * 
     * [{
     *  id,
     *  name
     * }, ...]
     * @return {Array} - the value options array for the enum
     ****************************************************************************************************/
    async _getSortEnumValueOptions() {
        const ret = [];
        let schemeIsSet = false;

        try{
            const branchResult = await window.sigApi.properties.getSchemeBranchDataWithProperty(this.id, 'sortenumtextscheme');
            schemeIsSet = (branchResult !== undefined && branchResult !== null) ? true : false;

        }catch(e){
            // do not directly trigger error if no scheme configured
        }

        if (schemeIsSet) {
            let schemeData = await window.sigApi.properties.getSchemeDataWithProperty(this.id, 'sortenumtextscheme');
            if (schemeData && schemeData.length === 3) {
                // Sorting the scheme data
                schemeData = schemeData.sort((a, b) => {
                    return a.compare.value - b.compare.value;
                })

                for (let schemeEntry of schemeData) {
                    ret.push({
                        name: schemeEntry.value,
                        id: schemeEntry.compare.value
                    })
                }
            }
        }
        return ret;
    }

    /****************************************************************************************************
     * Gets the filterInput
     * adds the eventListener
     ****************************************************************************************************/
    _setupFilterInput() {
        if (this.filterinputname && this.parentVM) {
            const filterInputVM = this.parentVM.getChildByName(this.filterinputname);
            if (filterInputVM) {
                const filterInput = filterInputVM.getDomRef();
                this.filterInput = filterInput;

                this.sigAddEventListener('value-changed', () => {
                    this._filterInputValueChangedHandler();
                }, 'jsEvent', this.filterInput);
            }
        }
    }

    /****************************************************************************************************
     * eventHandler for the 'value-changed' event of the filterInput
     * filters the recipe by datapointText1
     ****************************************************************************************************/
    _filterInputValueChangedHandler() {
        const filterVal = this.filterInput.value;
        const filterRegex = this._wildcardToRegExp(filterVal);

        this._filter('datapointText1', filterVal, (val) => {
            return filterRegex.test(val['datapointText1']);
        });
    }
    
    /****************************************************************************************************
     * Creates a RegExp from the given string, converting asterisks to .* expressions,
     * and escaping all other characters.
     * @param {*} s - string with wildcard *
     * @returns {RegExp} a useable regular expression with special characters escaped
     ****************************************************************************************************/
    _wildcardToRegExp(s) {
        return new RegExp(s.split(/\*+/).map(this._regExpEscape).join('.*'), 'i');
    }

    /**
     
     */
    /****************************************************************************************************
     * RegExp-escapes all characters in the given string.
     * @param {String} s
     * @returns {String} - a string with special characters escaped
     ****************************************************************************************************/
    _regExpEscape(s) {
        return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
    }

    /****************************************************************************************************
     * Gets the applyFilter button,
     *  adds the eventListener
     ****************************************************************************************************/
    _setupApplyFilter() {
        if (this.applyfilterbuttonname && this.parentVM) {
            const applyFilterButtonVM = this.parentVM.getChildByName(this.applyfilterbuttonname);
            const applyFilterButton = applyFilterButtonVM.getDomRef();
            this.applyFilterButton = applyFilterButton;

            this.sigAddEventListener('tap', () => {
                this.eventHandler._applyFilterButtonClickHandler();
            }, 'polymerEvent', this.applyFilterButton);
        }
    }

    /****************************************************************************************************
     * clickHandler for the applyFilterButton
     * applies only the currently filtered recipe
     ****************************************************************************************************/
    _applyFilterButtonClickHandler() {
        const fileText = this._createFileText(true);
        this._applyTemporaryRecipeFile(fileText);
    }

    /****************************************************************************************************
     * Sets up the progress overlay
     ****************************************************************************************************/
    _setupprogress() {
        //check if there is a progress bar set
        if (this.progressbarname && this.parentVM) {
            const progressBarVM = this.parentVM.getChildByName(this.progressbarname);
            if (progressBarVM) {
                const progressBar = progressBarVM.getDomRef();
                this.progressBar = progressBar;
                this.progressBar.style.display = 'none';
            }
        }
    }

    /****************************************************************************************************
     * Gets the user defined bits button,
     * and adds the click handler
     ****************************************************************************************************/
    _setupUserDefinedBits() {
        if (this.userdefinedbitsbuttonname && this.userdefinedbitswindow && this.parentVM) {
            //------------------
            //Setup of the UDB button
            //------------------

            const udbButton = this.parentVM.getChildByName(this.userdefinedbitsbuttonname)
            if (udbButton) {
                this.udbButton = udbButton.getDomRef();
                this.sigAddEventListener('tap', (e) => {
                    this._udbButtonClickHandler(e);
                }, 'polymerEvent', this.udbButton);
            }

            //------------------
            //Setup of the UDB window
            //------------------
            this.udbWindowRef = this.wmApi.getWindow(this.userdefinedbitswindow);
        }

    }

    /****************************************************************************************************
     * Clickhandler for the user defined bits button
     * opens the window where the user defined bits are set, and adds the eventlisteners to the buttons in that window
     ****************************************************************************************************/
    async _udbButtonClickHandler() {
        await this.udbWindowRef.open();
        this.udbWindowRef.getDomRef().front();
        //After opening the window, we can now get a reference to it
        this.udbWindow = this.wmApi.getWindow(this.userdefinedbitswindow);

        //From the window reference we can get the fileManager and the buttons
        this.sigUDBOK = this.udbWindow.getChildByName('sigOK');
        this.sigUDBCancel = this.udbWindow.getChildByName('sigCancel');
        this.sigUDBfilter1 = this.udbWindow.getChildByName('sigRecipeFilter1');
        this.sigUDBfilter2 = this.udbWindow.getChildByName('sigRecipeFilter2');

        //Set the filterinputs values to the filter values of the recipe manager
        if (this.sigUDBfilter1) {
            this.sigUDBfilter1.getDomRef().value = this.filter1;
        }
        if (this.sigUDBfilter2) {
            this.sigUDBfilter2.getDomRef().value = this.filter2;
        }


        //Add the eventListeners to the buttons
        if (this.sigUDBOK) {
            this.sigAddEventListener('tap', this.eventHandler._sigUDBOKTapHandler, 'polymerEvent', this.sigUDBOK.getDomRef());
        }
        if (this.sigUDBCancel) {
            this.sigAddEventListener('tap', this.eventHandler._sigUDBCancelTapHandler, 'polymerEvent', this.sigUDBCancel.getDomRef());
        }

    }

    /****************************************************************************************************
     * Handles a click on the OK button in the User Defined Bits window
     * gets the values of the filter inputs, and writes them to the current filter
     ****************************************************************************************************/
    _sigUDBOKTapHandler() {
        const _32BIT = Math.pow(2, 32);
        //Check if there is an input for filter1
        const filter1Input = this.udbWindow.getChildByName('sigRecipeFilter1');
        if (filter1Input) {
            //If there is an input, make sure it is converted to a number
            // because '0' is an applied filter whereas 0 isn't
            let filter1 = Number(filter1Input.getDomRef().value);

            //If the filter isn't a number, that means it is invalid, so we just use 0
            filter1 = isNaN(filter1) ? 0 : filter1;
            const filter12 = filter1 % _32BIT;
            const filter11 = (filter1 - filter12) / _32BIT

            this.filter11 = filter11;
            this.filter12 = filter12;
        }

        //Check if there is an input for filter2
        const filter2Input = this.udbWindow.getChildByName('sigRecipeFilter2');
        if (filter2Input) {
            //If there is an input, make sure it is converted to a number
            // because '0' is an applied filter whereas 0 isn't
            let filter2 = Number(filter2Input.getDomRef().value);

            //If the filter isn't a number, that means it is invalid, so we just use 0
            filter2 = isNaN(filter2) ? 0 : filter2;
            const filter22 = filter2 % _32BIT;
            const filter21 = (filter2 - filter22) / _32BIT

            this.filter21 = filter21;
            this.filter22 = filter22;
        }

        //Finally close the window
        this.udbWindow.close();
    }

    /****************************************************************************************************
     * Closes the window without taking any action
     ****************************************************************************************************/
    _sigUDBCancelTapHandler() {
        this.udbWindow.close();
    }

    /****************************************************************************************************
     * gets the save button, and makes sure everything works when saving
     ****************************************************************************************************/
    _setupSave() {
        if ((this.savebuttonname || this.saverecvalsbuttonname) && this.savewindow && this.parentVM) {

            //------------------
            //Setup of the save button
            //------------------
            if (this.savebuttonname) {
                const saveButtonVm = this.parentVM.getChildByName(this.savebuttonname);
                if (saveButtonVm) {
                    this.saveButton = saveButtonVm.getDomRef();
                    this.sigAddEventListener('tap', (e) => {
                        this._saveButtonClickHandler(e, false);
                    }, 'polymerEvent', this.saveButton);                
                }                
            }

            //------------------
            // Create reference to the save modified recipe button
            //------------------
            if (this.saverecvalsbuttonname) {
                const saveModButtonVm = this.parentVM.getChildByName(this.saverecvalsbuttonname);
                if (saveModButtonVm) {
                    this.saveModButton = saveModButtonVm.getDomRef();
                    this.sigAddEventListener('tap', (e) => {
                        this._saveButtonClickHandler(e, true);
                    }, 'polymerEvent', this.saveModButton);
                }             
            }

            //------------------
            //Setup of the save window
            //------------------
            this.saveWindowRef = this.wmApi.getWindow(this.savewindow);
        }
    }

    /****************************************************************************************************
     * Handles a click on the save button
     * opens the save window
     * @param {*} e - event data
     * @param {*} saveRecipeValues - true .. recipe values are saved; false .. current values are saved
     ****************************************************************************************************/
    _saveButtonClickHandler(e, saveRecipeValues) {
        this.saveWindowRef.open().then(() => {
            // Store references; must be updated each call
            this.saveWindow = this.wmApi.getWindow(this.savewindow);

            //From the window reference we can get the fileManager and the buttons
            this.sigSaveFileManager = this.saveWindow.getChildByName('sigFilemanager'); 
            this.sigSaveOK = this.saveWindow.getChildByName('sigOK');
            this.sigSaveCancel = this.saveWindow.getChildByName('sigCancel');
            this.sigSaveInput = this.saveWindow.getChildByName('sigInput');
    
            //Add the eventListeners to the buttons
            this.sigAddEventListener('tap', () => {
                this.eventHandler._sigSaveOKTapHandler(saveRecipeValues);
            }, 'polymerEvent', this.sigSaveOK.getDomRef());
            this.sigAddEventListener('tap', this.eventHandler._sigSaveCancelTapHandler, 'polymerEvent', this.sigSaveCancel.getDomRef());
        });
    }

    /****************************************************************************************************
     *Handles a click on the ok button in the save window
     *
     * @param {*} saveRecipeValues - true .. recipe values are saved; false .. current values are saved
     ****************************************************************************************************/
    _sigSaveOKTapHandler(saveRecipeValues) {
        const inputValue = this.sigSaveInput.getDomRef().value;
        const fileManagerPath = this.sigSaveFileManager.getDomRef().activepathleft;
        let path = fileManagerPath + inputValue;
        if (path.startsWith('/')) {
            path = this.sigSaveFileManager.getDomRef()._convertPath(path);
        }

        //Only makes sense to save if a filename was entered
        if (inputValue) {

            //check if only the manipulated recipe should be saved
            if (saveRecipeValues) {
                //check if there is a recipe to save (has to be loaded first) and a user CRC
                if ((this.currentRecipe.length > 0) && (this.userdefinedcrc !== undefined)) {
                    if (this.userdefinedcrc !== 0) {
                        // Store modified recipe values
                        this._saveRecipeValues(path);
                        this.saveWindow.close();
                    } else {
                        this._log('_sigSaveOKTapHandler(): User CRC has value 0 which has no effect', 'WARN');
                    }
                } else if (!this.currentRecipe.length) {
                    this._log('_sigSaveOKTapHandler(): No recipe loaded that could be manipulated', 'WARN');
                } else {
                    this._log('_sigSaveOKTapHandler(): User CRC has to be specified to save a manipulated recipe', 'WARN');
                }
            } else {

                //save the current values
                this.rmApi.save(path, '', this.filter1, this.filter2, (percent) => {
                    this._progress(percent.progress / 1000, `Save:`);
                }, () => {
                    //reset save state
                    this.saveState = 0;
                    this._progressResultUpdate('begin','save',0);
                }, () => {
                    //end callback is also called if error occurred -> so for state to do not overwrite
                    if (this.saveState === 0) {
                        this.saveState = 1;
                    }
                    this._progressResultUpdate('end','save', this.saveState);
                }).catch(e => {
                    // Save rejected
                    this.saveState = this.errorOffsetRecipeApi - e.status;
                });
                this.saveWindow.close();                
            }
        }
    }

    /****************************************************************************************************
     *handles a click on the cancel button in the save window
     *
     ****************************************************************************************************/
    _sigSaveCancelTapHandler() {
        this.saveWindow.close();
    }

    /****************************************************************************************************
     * Updates the component status for save/load routines
     *
     * @param {string} sequence begin/end
     * @param {string} type save/load
     * @param {number} result result or error code
     * @return {*} 
     * @memberof LasalRuntimeSigRecipeManagerElement
    ****************************************************************************************************/
    _progressResultUpdate(sequence, type, result) {
        // Update error code
        if (result !== undefined) this.processresult = result;

        // Fire user event
        switch (sequence) {
            case 'begin':
                this._progressStart();
                break;

            case 'end':
                this._progressEnd();
                switch(type) {
                    case 'save':
                        this._dispatchEvent('save-end');
                        break;
                    case 'load':
                        this._dispatchEvent('load-end');
                        break;
                    case 'apply':
                        this._dispatchEvent('apply-end');
                        break;    
                }
                break;

            default:
                return;
        }
    }

    /****************************************************************************************************
     * gets the apply button, makes sure it does what it should do
     ****************************************************************************************************/
    _setupApply() {
        if (this.applybuttonname && this.parentVM) {
            const applyButton = this.parentVM.getChildByName(this.applybuttonname).getDomRef();
            this.applyButton = applyButton;

            this.sigAddEventListener('tap', () => {
                this._applyButtonClickHandler();
            }, 'polymerEvent', this.applyButton);
        }
    }

    /****************************************************************************************************
     * Handles the apply button clicks
     * calculates the new settings (from the recipe, and the current values)
     * creates a temporary recipe file
     * loads that file
     * and deletes it
     ****************************************************************************************************/
    async _applyButtonClickHandler() {
        //create the text of the temp recipe file
        const fileText = this._createFileText();
        this._applyTemporaryRecipeFile(fileText);
    }

    /****************************************************************************************************
     * Creates a temporary recipe file
     * Loads the file
     * deletes the file
     * 
     * @param {String} fileText - the text that should be in the file
     ****************************************************************************************************/
    async _applyTemporaryRecipeFile(fileText) {
        //make a blob out of the text, so that the fileManager api can work with it
        const blob = new Blob([fileText], { type: 'text/plain' });
        //Timestamp used in the recipe filename, to keep it somewhat unique
        const time = new Date().getTime()
        const fileName = `_tmpRecipe${time}.txt`;
      //  const fileName = `_tmpRecipe${time}`;

        // create the file path for the temporary file based on the file path of the last loaded recipe
        let filePath;
        if (this.lastLoadedFilePath !== "") {
            let dotIndex = this.lastLoadedFilePath.lastIndexOf('.');
            if (dotIndex > 0) {
                filePath = this.lastLoadedFilePath.substring(0, dotIndex) + fileName;
            } else {
                filePath = this.lastLoadedFilePath + fileName;
            }
        } else {
            filePath = `c:\\${fileName}`
           //   filePath = `c:/userdata/recipe\\${fileName}`

        }        
        
        try {
            //Create the file
            await this.fmApi.writeFile(filePath, blob);
            try {
                //load the file
                await this.rmApi.load(filePath, this.filter1, this.filter2, this.tmpFileCRC, (percent) => {
                    this._progress(percent.progress / 1000, `apply:`);
                }, () => {
                    this.applyState = 0;
                    this._progressResultUpdate('begin','apply',0);
                }, () => {
                    //end callback is also called if error occured
                    if (this.applyState === 0) {
                        this.applyState = 1;
                    }
                    this._progressResultUpdate('end','apply', this.applyState);
                }).catch(e => {
                    //set apply state
                    this.applyState = e.status ? this.errorOffsetRecipeApi - e.status : -1
                });                
            }
            catch (error) {
                this._log('_applyTemporaryRecipeFile(): failed to load temp file -> ' + error.errortext, 'ERROR');
                this._progressResultUpdate('end','apply', -1);
            }            
        }
        catch (error) {
            this._log('_applyTemporaryRecipeFile(): failed to write temp file -> ' + error.message, 'ERROR');
            this._progressResultUpdate('end','apply', -1);
        } 

        //delete the file after it has been applied
        await this.fmApi.remove(filePath);
    }

    /****************************************************************************************************
     * creates the file text from this.currentrecipe
     * 
     * @param {Boolean} filtered - determines if the created file uses all loaded datapoints or just the filtered ones
     * @param {Boolean} useUserDefinedCRC - determines if the header should contain the user CRC or the default override CRC
     * @returns {String} The text that will be in the recipe file
     ****************************************************************************************************/
    _createFileText(filtered = false, useUserDefinedCRC = false) {
        // Add the header to the fileText
        let CRCString;
        if (useUserDefinedCRC) {
            // take the users CRC

            // the >>> 0 will actually do nothing but it is used to get an unsigned integer
            // bit operations in js are always 32 bit therefore it makes sure the result is between 0 and 0xFFFFFFFF
            this.userdefinedcrc = this.userdefinedcrc >>> 0;

            CRCString = this.userdefinedcrc.toString(16);
        } else {
            // take our own fake CRC
            CRCString = this.tmpFileCRC.toString(16);
        }

        // get the current date and time (separated by space) and split it into 2 strings
        const actDateTime = new Date;
        let fileText = this.lastLoadedRecipeHeaderText + "\ncrc = 16#" + CRCString + 
                                                         ";\nversion = 1" + 
                                                         ";\ndate = " + this._formatDate(actDateTime) + 
                                                         ";\ntime = " + this._formatTime(actDateTime) + ";\n\n";

        let usedRecipe;
        if (filtered) {
            // If we use the filtered values, we still have to get them in the correct order
            usedRecipe = this.currentRecipe.sort((a, b) => {
                return a - b;
            });

        } else {
            usedRecipe = this.originalRecipe;
            //Mirror the values from the modified recipe to the original recipe
            //Since the modified recipe can have changed order, we cannot use it here
            for (let line of this.currentRecipe) {
                Object.assign(this.originalRecipe[line.idx], line);
            }
        }

        //create the lines for the datapoints
        for (let line of usedRecipe) {
            fileText += line.datapointString;
            fileText += ' = ';
            if (line.datapointUseCurrentValue === 1) {
                fileText += line.datapointValue;
            } else {
                fileText += line.datapointRecipeValue;
            }
            fileText += ';\n';
        }
        return fileText;
    }


    /****************************************************************************************************
     * method is used to format the date needed for various operations
     *
     * @param {date} date - date object which should be converted
     * @param {string} [delimiter='.'] - separation sign of day - month - year
     * @return {string} - formatted date string
     * @memberof LasalRuntimeSigRecipeManagerElement
     ****************************************************************************************************/
    _formatDate(date, delimiter='.') {
        const day = ('0' + date.getDate()).slice(-2);
        const month = ('0' + (date.getMonth()+1)).slice(-2);
        const year = date.getFullYear();
        return day + delimiter + month + delimiter + year;
    }


    /****************************************************************************************************
     * method is used to format the time needed for various operations
     *
     * @param {date} date - date object which should be converted
     * @param {string} [delimiter=':'] - separation sign of hours - minutes - seconds
     * @return {string} - formatted time string
     * @memberof LasalRuntimeSigRecipeManagerElement
     ****************************************************************************************************/
     _formatTime(date, delimiter=':') {
        const hours = ('0' + date.getHours()).slice(-2);
        const minutes = ('0' + date.getMinutes()).slice(-2);
        const seconds = ('0' + date.getSeconds()).slice(-2);
        return hours + delimiter + minutes + delimiter + seconds;
    }


    /****************************************************************************************************
     * gets the refresh button, makes sure it does what it should do
     ****************************************************************************************************/
    _setupRefresh() {
        //check if the we even have a name for the refresh button
        if (this.refreshbuttonname && this.parentVM) {
            const refreshButton = this.parentVM.getChildByName(this.refreshbuttonname);
            if (refreshButton) {
                this.refreshButton = refreshButton.getDomRef();
                this.sigAddEventListener('tap', () => {
                    this._refreshButtonClickHandler();
                }, 'polymerEvent', this.refreshButton);
            }
        }
    }

    /****************************************************************************************************
     * Reloads the current datapoint values,
     * updates the lines
     ****************************************************************************************************/
    async _refreshButtonClickHandler() {
        //Show the progress overlay
        this._progressStart();
        //Update the current recipe with the current datapoint values
        await this._updateDatapointCurrentValues();
        //Hide the progress overlay
        this._progressEnd();
        //Update the shown lines
        const firstElement = this._getFirstElement();
        this._updateLines(firstElement);
    }

    /****************************************************************************************************
     * gets the load button, makes sure it does what it should do
     * the button needs to be a sibling of the recipe manager for this to work
     * meaning if the recipe manager is in a composite control, the button has to be in the same
     * composite control
     * If the recipe manager lies on a dashboard, the button has to lie on the same
     * dashboard
     ****************************************************************************************************/
    _setupLoad() {
        if (this.loadbuttonname && this.loadwindow && this.parentVM) {
            //------------------
            //Setup of the load button
            //------------------
            const loadButton = this.parentVM.getChildByName(this.loadbuttonname).getDomRef();

            this.loadButton = loadButton;
            this.sigAddEventListener('tap', (e) => {
                this._loadButtonClickHandler(e);
            }, 'polymerEvent', this.loadButton);

            //------------------
            //Setup of the load window
            //------------------
            this.loadWindowRef = this.wmApi.getWindow(this.loadwindow);
        }
    }

    /****************************************************************************************************
     * Click handler for the load button
     * opens the window, and adds the event listeners to the buttons
     ****************************************************************************************************/
    async _loadButtonClickHandler() {
        await this.loadWindowRef.open();
        this.loadWindowRef.getDomRef().front();
        //After opening the window, we can now get a reference to it
        this.loadWindow = this.wmApi.getWindow(this.loadwindow);

        //From the window reference we can get the fileManager and the buttons
        this.sigLoadFileManager = this.loadWindow.getChildByName('sigFilemanager');
        this.sigLoadOK = this.loadWindow.getChildByName('sigOK');
        this.sigLoadCancel = this.loadWindow.getChildByName('sigCancel');

        //For adding the eventListeners, the filemanager must be present
        if (this.sigLoadFileManager) {
            if (this.sigLoadOK) {
                this.sigAddEventListener('tap', this.eventHandler._sigLoadOKTapHandler, 'polymerEvent', this.sigLoadOK.getDomRef());
            }
            if (this.sigLoadCancel) {
                this.sigAddEventListener('tap', this.eventHandler._sigLoadCancelTapHandler, 'polymerEvent', this.sigLoadCancel.getDomRef());
            }
        }
    }

    /****************************************************************************************************
     * Handles a tap on the OK button in the load window
     * if a file was selected, load that file
     * if a folder was selected, open the folder
     ****************************************************************************************************/
    async _sigLoadOKTapHandler() {
        //check if there is a filemanager
        if (this.sigLoadFileManager) {
            //get the active selection of that filemanager
            const activeSelection = this.sigLoadFileManager.getDomRef().activeselection.left[0];
			///////////////////////////////////////////////////////////////////////////////
			// ESYS Patch start												   
			sigApi.datapointManager.writeDataPoint('10:ToolsName1.Tools_Name', activeSelection.name);																						 
			// ESYS Patch end
			///////////////////////////////////////////////////////////////////////////////
            //check if a file was selected
            if (activeSelection.type === 'file') {
                //Get the path to the selected file
                let path = this.sigLoadFileManager.getDomRef().activeselection.left[0].path;
                //Make sure it is a windows path that works with salamander
                if (path.startsWith('/')) {
                    path = this.sigLoadFileManager.getDomRef()._convertPath(path);
                }

                this.loadWindow.close();

                //Clear the current recipe before loading a new one
                await this._loadRecipe(path);

                // if there are entries => remember the filename. it's used for the temporary file
                if (this.currentRecipe.length > 0) {
                    this.lastLoadedFilePath = path;
                }

                //reset scroll position
                this.$.lineList.scrollTop = '0px';

                //Update the shown elements, with the newly loaded recipe
                this.updateDebouncer = SigPolymer.debouncer.debounce(this.updateDebouncer, SigPolymer.timeOut.after(100), () => {
                    this._updateElements(0);
                })
            } else {
                //if a folder or volume was selected, open the folder/volume
                this.sigLoadFileManager.getDomRef()._actionSwitchPath({ path: activeSelection.path });
            }
        } else {
            this.log('_sigLoadOKTapHandler(): No Filemanager found', 'ERROR');
        }
    }

    /****************************************************************************************************
     * Handles a tap on the cancel button in the load window
     * closes the load window
     ****************************************************************************************************/
    _sigLoadCancelTapHandler() {
        this.loadWindow.close();
    }

    /****************************************************************************************************
     * Initializes the variables that can be initalized at this point (renderCallback)
     ****************************************************************************************************/
    _initVars() {
        //Gets the composite control that was rendered into the recipeline slot
        this.compositeControl = this.querySelector('sig-composite-control[slot="recipeline"]');
        //Log an error if there is no composite control
        // WE NEED THAT COMPOSITE CONTROL
        if (!this.compositeControl) {
            this._log(`_initVars(): No composite control ${this.id}`, 'ERROR');
            return;
        }
        //Get the Composite control name
        //The name is needed to render the CC via the this.vmApi.appendNewCC function
        this.compositeControlName = this.vmApi.getVmById(this.compositeControl.id).getName();

        //The bounds of the composite control, and the lineList, needed for various calculations
        this.compositeControlBounds = this.compositeControl.getControlBounds();//this.getControlBounds(this.compositeControl);
        //this.lineListHeight = this.$.lineList.offsetHeight;
        this.lineListHeight = parseFloat(this.getControlBounds().height) - 2 * parseFloat(this.getControlBounds().border);

        this.compositeControlVM = this.vmApi.getVmById(this.compositeControl.id);

        if (!this.parent && !this.parentVM) {
            const thisVM = this.vmApi.getVmById(this.id);
            this.parentVM = thisVM.getParent();
        }

    }

    /****************************************************************************************************
     * The event handlers are stored in this.eventHandler
     * so that we don't use anonymous functions, and can remove the event handler if needed
     * this.eventHandler is an object
     * a prop of that object is for example _clickHandler
     * so this function would set this.eventHandler._clickHandler to
     * (e) => {
     *  this._clickHandler(e)
     * }
     ****************************************************************************************************/
    _initEventHandler() {
        for (let evtHandler in this.eventHandler) {
            this.eventHandler[evtHandler] = (e) => {
                this[evtHandler](e);
            }
        }
    }

    /**
     * 
     * 
     * @param {Number} firstLine -
     * */
    /****************************************************************************************************
     * Updats the element list from the current scroll position.
     *
     * @param {number} [firstLine=0] -  the first line to be shown
     * @returns
     ****************************************************************************************************/
    _updateElements(firstLine = 0) {
        if (!this.compositeControl || !this.showRecipe) {
            return;
        }
        //If there aren't any lines loaded yet
        //meaning if they aren't created, we create them here
        if (this.$.lineList.children.length === 0) {
            this._createLines();
        }

        this._updateLines(firstLine);

    }

    /****************************************************************************************************
     * Updates the lines
     * @param {Number} firstLine - the first line that should be shown
     ****************************************************************************************************/
    _updateLines(firstLine) {
        for (let i = 0; i < this.maxNoLines; i++) {
            //check if line can be shown
            if ((firstLine + i) < this.showRecipe.length) {
                this._setLineValues(this.lines[i], this.showRecipe[firstLine + i], firstLine + i);
                //turn line visible
                this.lines[i].classList.remove('hideLine');
            }
            else {
                //make line invisible cause it will have no senseful content
                this.lines[i].classList.add('hideLine');
            }       
        }

        //save act scroll position
        const actScrollPos = this.$.lineList.scrollTop;

        //calc fake scroll heights
        this.fakeScrollTop.style.height = `${this.compositeControlBounds.height * (firstLine)}px`;
        //get number of lines which are really shown
        const nrLinesShown = Math.min(this.maxNoLines, this.showRecipe.length);
        let bottomHeight = this.totalHeight - parseFloat(this.fakeScrollTop.style.height) - (nrLinesShown * this.compositeControlBounds.height);
        bottomHeight = Math.max(0, bottomHeight);
        this.fakeScrollBot.style.height = `${bottomHeight}px`;

        //restore scroll position cause in case of changing the heights of the fake scroll elements the scroll position gets adapted
        this.$.lineList.scrollTop = actScrollPos;
    }

    /****************************************************************************************************
     * Sets the values of the line to the values of the recipe entry
     * 
     * @param {HTMLElement} line -the line that needs to be set
     * @param {Object} recipeEntry -the Object containing the recipe data for that line
     * @param {Number} idx - the index of the line that is updated
     ****************************************************************************************************/
    async _setLineValues(line, recipeEntry, idx) {
        //Set the index first
        //otherwise the value changed listeners for the checkbox and the recipe value
        //will still have the old index which causes chaos
        line.setAttribute('data-idx', idx);
        //get slot property
        const slotProp = sigApi.properties.getProperty(this.id, line.name);
        if (slotProp !== undefined) {
            //Get the inputs/texts/checkbox etc
            const compositeControlVM = slotProp.getCompositeControlVM();
            if (compositeControlVM !== null) {
                //If we have a datapointName element, set its value
                const sigDatapointName = compositeControlVM.getChildByName('sigDatapointName');
                if (sigDatapointName) {
                    sigDatapointName.getDomRef().sigtext = recipeEntry.datapointText1;
                }
                //if we have an element showing the current datapoint value, set its value
                const sigDatapointCurrentValue = compositeControlVM.getChildByName('sigDatapointCurrentValue');
                if (sigDatapointCurrentValue) {                
                    sigDatapointCurrentValue.getDomRef().value = await recipeEntry.convertTo(recipeEntry.datapointValue);
                }
                //if we have an element showing the recipe value, set its value
                const sigDatapointRecipeValue = compositeControlVM.getChildByName('sigDatapointRecipeValue');
                if (sigDatapointRecipeValue) {
                    //reset value initialized to avoid modified state to be set
                    sigDatapointRecipeValue.getDomRef().valueInitialized = false;
                    //set limits of datapoint
                    sigDatapointRecipeValue.getDomRef().limitHigh = await recipeEntry.convertTo(recipeEntry.limitHigh);
                    sigDatapointRecipeValue.getDomRef().limitLow = await recipeEntry.convertTo(recipeEntry.limitLow);
                    sigDatapointRecipeValue.getDomRef().value = await recipeEntry.convertTo(recipeEntry.datapointRecipeValue);
                    //set value initialized, has to be done this way cause if setting the same value the observer is not called
                    sigDatapointRecipeValue.getDomRef().valueInitialized = true;                    
                }
                //If we have an element determining if we should use the current value or the recipe value, set its value
                const sigDatapointUseCurrentValue = compositeControlVM.getChildByName('sigDatapointUseCurrentValue');
                if (sigDatapointUseCurrentValue) {
                    sigDatapointUseCurrentValue.getDomRef().value = recipeEntry.datapointUseCurrentValue;
                }
                //If there are elements showing the unit text, set their values
                const sigDatapointUnitLabelsValues = compositeControlVM.getChildren().filter((child) => {
                    if (child.getName().match(/sigUnitText.*/)) {
                        return true;
                    }
                    return false;
                })
                if (sigDatapointUnitLabelsValues.length) {
                    for (let i = 0; i < sigDatapointUnitLabelsValues.length; i++) {
                        sigDatapointUnitLabelsValues[i].getDomRef().sigtext = recipeEntry.unitText;
                    }
                }
            }
            else {
                this._log('_setLineValues(): failed to get composite control vm', 'ERROR');
            }               
        }
        else {
            this._log('_setLineValues(): failed to get slot', 'ERROR');
        }                
    }

    /****************************************************************************************************
     * First creates the lines,
     * adds the fakeScrollElements
     * sets the total height of the scroll area
     ****************************************************************************************************/
    _createLines() {
        const parent = this.vmApi.getVmById(this.id);

        this.$.lineList.innerHTML = '';
        //calc the maximum number of shown lines
        this.maxNoLines = Math.ceil(this.lineListHeight / this.compositeControlBounds.height);
        this.updateStyles({
            '--theme-sig-control-recipe-manager-line-height': this.compositeControlBounds.height + 'px',
            '--theme-sig-control-recipe-manager-line-width': this.compositeControlBounds.width + 'px'
        })

        //The fakes scroll elements that are above and below the lines
        const fakeScrollTop = document.createElement('div');
        const fakeScrollBot = document.createElement('div');

        this.fakeScrollTop = fakeScrollTop;
        this.fakeScrollBot = fakeScrollBot;

        this.$.lineList.appendChild(fakeScrollTop);
        //Create a container for each line shown
        for (let i = 0; i < this.maxNoLines; i++) {
            const slotContainer = document.createElement('div');
            slotContainer.classList.add('slotContainer');
            const lineContainer = document.createElement('slot');
            const lineName = `lineContainer${i}`;
            lineContainer.name = lineName;
            lineContainer.classList.add('lineContainer');
            lineContainer.classList.add('hideLine');
            this.vmApi.appendNewCC(parent, this.compositeControlName, lineName);
            slotContainer.appendChild(lineContainer);
            //Add the composite control to the lines array
            this.lines.push(lineContainer);
            this.$.lineList.appendChild(slotContainer);
        }
        this.$.lineList.appendChild(fakeScrollBot);

        const recipeValueAvailable = this.compositeControlVM.getChildByName('sigDatapointRecipeValue') ? true : false;
        const useCurrentValueAvailable = this.compositeControlVM.getChildByName('sigDatapointUseCurrentValue') ? true : false;

        //go through lines and add eventlisteners
        for (let line of this.lines) {
            //Check if there is an input for the recipe value in the original CC
            if (recipeValueAvailable) {
                //get slot property
                const slotProp = sigApi.properties.getProperty(this.id, line.name);
                if (slotProp !== undefined) {
                    //Get the inputs/texts/checkbox etc
                    const compositeControlVM = slotProp.getCompositeControlVM();
                    if (compositeControlVM !== null) {
                        const sigDatapointRecipeValue = compositeControlVM.getChildByName('sigDatapointRecipeValue').getDomRef();
                        if ((sigDatapointRecipeValue !== null) && (sigDatapointRecipeValue !== undefined)) {
                            //reset value initialized info
                            sigDatapointRecipeValue.valueInitialized = false;
                            //Value changed listener for the input
                            sigDatapointRecipeValue.sigAddEventListener('value-changed', async (e) => {
                                //Cant do anthing if we dont have a recipe loaded
                                if (!this.showRecipe.length) {
                                    return;
                                }

                                //check if value has been initialized
                                if (e.srcElement.valueInitialized) {
                                    //display that values have been changed
                                    this.modifiedstate = 1;                                    
                                } 

                                const value = e.target.value;
                                const compositeControl = e.target.closest('sig-composite-control');
                                const line = this.$.lineList.querySelector(`[name="${compositeControl.slot}"]`);
                                const idx = parseInt(line.getAttribute('data-idx'));


                                //If the first value changed occurs before the initializing is done
                                //So there is no data-idx attribute set; 
                                //In that case no action is needed, since the changed value is the value 
                                //from the recipe
                                if (isNaN(idx)) {
                                    return;
                                }

                                this.showRecipe[idx].datapointRecipeValue = await this.showRecipe[idx].convertFrom(value);
                                e.target.value = await this.showRecipe[idx].convertTo(this.showRecipe[idx].datapointRecipeValue);
                            }, 'jsEvent', sigDatapointRecipeValue); 
                        }        
                    }
                    else {
                        this._log('_createLines(): failed to get composite control vm', 'ERROR');
                    }               
                }
                else {
                    this._log('_createLines(): failed to get slot', 'ERROR');
                }
                
            } 

            //handle use current datapoint checkbox
            if (useCurrentValueAvailable) {
                //get slot property
                const slotProp = sigApi.properties.getProperty(this.id, line.name);
                if (slotProp !== undefined) {
                    //Get the inputs/texts/checkbox etc
                    const compositeControlVM = slotProp.getCompositeControlVM();
                    if (compositeControlVM !== null) {
                        const sigDatapointUseCurrentValue = compositeControlVM.getChildByName('sigDatapointUseCurrentValue').getDomRef();
                        //Value Changed listener for the checkbox
                        sigDatapointUseCurrentValue.sigAddEventListener('value-changed', (e) => {
                            if (!this.showRecipe.length) {
                                return;
                            }
                            const value = e.target.value;
                            const compositeControl = e.target.closest('sig-composite-control');
                            const line = this.$.lineList.querySelector(`[name="${compositeControl.slot}"]`);
                            const idx = parseInt(line.getAttribute('data-idx'));

                            //If the first value changed occurs before the initializing is done
                            //So there is no data-idx attribute set; 
                            //In that case no action is needed, since the changed value is the value 
                            //from the recipe
                            if (isNaN(idx)) {
                                return;
                            }

                            this.showRecipe[idx].datapointUseCurrentValue = value;
                        }, 'jsEvent', sigDatapointUseCurrentValue);  
                    }
                    else {
                        this._log('_createLines(): failed to get composite control vm', 'ERROR');
                    }               
                }
                else {
                    this._log('_createLines(): failed to get slot', 'ERROR');
                }                        
            }            
        }    
    }

    /****************************************************************************************************
     * Loads a recipe, parsesit, and saves it to this.currentRecipe
     * 
     * @param {String} path - the filepath to the recipe
     ****************************************************************************************************/
    async _loadRecipe(path) {
        try {
            this._progressResultUpdate('begin', 'load', 0);
            
            //Reload the bounds; In case the first bounds were wrong
            this.compositeControlBounds = this.compositeControl.getControlBounds();
            //Read the file
            const fileContent = await this.fmApi.fileContent(path);

            //Get the file content as string
            const fileContentText = await this._readFileAsText(fileContent.data);

            //Try to parse the file
            let parsedRecipe = this._parseRecipe(fileContentText);

            //We can set it directly here
            //If the parsing didn't work _parseRecipe should Throw an error that will be caught by the catch
            this.currentRecipe = await this._filterRecipe(parsedRecipe);

            this.originalRecipe = [];
            Object.assign(this.originalRecipe, this.currentRecipe);

            //get checkbit filtered recipe for displaying
            this.showRecipe = this._getChkBitFilteredRecipe();

            //the total height of the lineListContent
            //The number of lines times their height
            const totalHeight = (this.showRecipe.length) * this.compositeControlBounds.height;
            this.totalHeight = totalHeight;            

            //After we got the recipe
            //We need to load the current datapoint values as well
            await this._updateDatapointCurrentValues();

            //Sort the list
            //If the enum isn't there yet, it should be sorted by index by default
            this._sortListFromEnumSettings();

            //reset modified state
            this.modifiedstate = 0;

            this._progressResultUpdate('end', 'load', 1);
            this.filename = path;

        } catch (e) {
            this._log(`_loadRecipe(): Error loading Recipe: ${e.message}`, 'ERROR');
            this._progressEnd();

            if (e && e.status)
                this._progressResultUpdate('end', 'load', this.errorOffsetFilemanApi - e.status);
            else
                this._progressResultUpdate('end', 'load', -1);
        }
    }


    /****************************************************************************************************
     * get checkbit filtered recipe for displaying
     *
     * @memberof LasalRuntimeSigRecipeManagerElement
     ****************************************************************************************************/
    _getChkBitFilteredRecipe() {
        // for displaying get a recipe only with elements that are not locked via checkbit
        let ret = [];
        for (let [idx, line] of this.currentRecipe.entries()) {
            //Get the datapoint
            //const datapoint = await window.sigApi.datapointManager.getDatapoint(line.datapointString);
            const datapoint = sigApi.datapointManager.getDatapoint(line.datapointString);

            let lockedViaCheckbit = false;
            let actCheckbit = datapoint.getProperties().checkbit;
            if (actCheckbit !== undefined) {
                let actCheckbitIdElements = actCheckbit.value.match(/([\S]*):([\S]*)/); // make 2 groups: (any number of non-white space characters) <colon> (any number of non-white space characters)
                let actCheckbitInvert = actCheckbitIdElements[1];
                let actCheckbitId = actCheckbitIdElements[2];
                lockedViaCheckbit = (this.checkbitApi.getCheckbitValueById(actCheckbitId) ^ actCheckbitInvert) === 0;
            }
    
            if (lockedViaCheckbit === false) {
                ret.push(line);
            }
        } 
        return ret;       
    }


    /****************************************************************************************************
     * Filters a recipes datapoints according to the filters
     * filter1 = include filter
     * filter2 = exclude filter
     * 
     * adds an index to the filtered recipe
     * this is done to make it easier to get the original ordering from a sorted recipe later
     * 
     * @param {Array} recipe - the recipe that needs to be filtered
     * @return {Array} - the filtered recipe
     ****************************************************************************************************/
    async _filterRecipe(recipe) {
        //Check if we have a filter
        //or if any filter is different than 0
        if (this.filter1 || this.filter2) {
            let ret = [];

            //Loop through each recipe entry
            for (let i = 0; i < recipe.length; i++) {
                //Update the progress overlay
                this._progress((i / recipe.length), `Filter:`);
                let userDefinedBits = 0;
                //Get the dpInfo for each datapoint, to get the user defined bits
                let dp = await window.sigApi.datapointManager.getDatapoint(recipe[i].datapointString);
                if(!dp) continue;
                let dpId = dp.getId();
                let dpInfo = await window.sigApi.datapointManager.getDpInfo(dpId);
                dpInfo = dpInfo[0];
                userDefinedBits = dpInfo.getUserdefinedBits();

                //filter1 == include filter
                //filter2 == exclude filter
                // So we check if there are no overlapping bits between the exlude filter and the user defined bits of the datapoint
                if ((!this.filter2) || !(userDefinedBits & this.filter2)) {
                    //Then we check if there are overlapping bits with the include filter
                    if ((!this.filter1) || (userDefinedBits & this.filter1)) {
                        // if both conditions are met, we add the index, and add the recipe entry to the filtered recipe
                        recipe[i].idx = ret.length;
                        ret.push(recipe[i]);
                    }
                }
            }
            return ret;
        } else {
            //If there are no filters, all we need to do is set the idx variable
            //it is needed when assembling the recipe file, to assign a value from the sorted, and ordered recipe to the original recipe
            for (let i = 0; i < recipe.length; i++) {
                recipe[i].idx = i;
            }
            return recipe;
        }
    }

    /****************************************************************************************************
     * Reads a blob and returns a string
     * 
     * @param {Blob} data - the data of a read recipe file
     * @return {String} - The parsed blob in string form
     ****************************************************************************************************/
    _readFileAsText(data) {
        const fileReader = new FileReader();

        return new Promise((resolve, reject) => {
            fileReader.onerror = () => {
                fileReader.abort();
                reject(new Error('Error with the FileReader'));
            }

            fileReader.onload = () => {
                resolve(fileReader.result);
            }
            fileReader.readAsText(data);
        })
    }

    /****************************************************************************************************
     * Parses a recipe file into an object that can be used
     * 
     * @param {String} fileContent - the text content of the recipe file
     * @return {Array} - the file content, parsed into an Array
     ****************************************************************************************************/
    _parseRecipe(fileContent) {
        if ((fileContent) && (fileContent.length > 0) && (typeof fileContent === 'string')) {
            //the array with the parsed file that will be returned
            const ret = [];

            //Spliting the file content up into 3 elements: Headertext, Headerinfo (crc, version, date, time) and Recipedata
            //First group (Headertext): everything before the second group
            //Second group (Headerinfo): everything from the line that starts with "crc" (whitespaces allowed before it) until the line that contains "time" and ends with ";"
            //Third group (Recipedata): everything after the second group
            const fileElements = fileContent.match(/([\s\S]*)(\n[\s]*crc[\s\S]*time[\s\S]*?;)([\s\S]*)/m);

            if ((fileElements) && (fileElements.length >= 2)) {
                //remember the header text in case the loaded recipe should be saved again before applying the values to the system (save manipulated recipe)
                this.lastLoadedRecipeHeaderText = fileElements[1];
                
                //extract the recipe data and throw them in an array of strings for each line
                const datapointString = fileElements[3];
                const datapointLines = datapointString.split('\n');

                for (let line of datapointLines) {
                    //Split the line
                    //Format
                    //Server = value;
                    const splitLine = line.split('=');

                    //We need exactly two entries here
                    //If we have less there isn't an assignment to a server
                    //If we have more something else must be corrupted with the line
                    if (splitLine.length === 2) {
                        //Trim the first entry to remove trailing space
                        const datapointString = splitLine[0].trim();
                        //Trim the second entry to remove preceding space
                        //slice to remove the trailing ;
                        const datapointRecipeValue = splitLine[1].trim().slice(0, -1);

                        if (datapointString && datapointRecipeValue.length > 0) {
                            //get limit of datapoint
                            const datapoint = sigApi.datapointManager.getDatapoint(datapointString);
                            if (datapoint) {
                                let limitHigh, limitLow;
                                const limitHighProp = datapoint.getLimitHigh();
                                if ((limitHighProp) && (limitHighProp.type === 1)) {
                                    limitHigh = limitHighProp.value;
                                }
                                const limitLowProp = datapoint.getLimitLow();
                                if ((limitLowProp) && (limitLowProp.type === 1)) {
                                    limitLow = limitLowProp.value;
                                }                        
                                ret.push({
                                    datapointString,
                                    datapointRecipeValue,
                                    datapointUseCurrentValue: 0,
                                    limitHigh: limitHigh,
                                    limitLow: limitLow
                                })                              
                            }
                            else {
                                this._log('_parseRecipe(): get datapoint failed','ERROR');
                            }
                        }
                    }
                }

                if (ret.length) {
                    return ret;
                } else {
                    this._log("_parseRecipe(): no valid recipe entries", 'ERROR');
                    throw new Error('_parseRecipe: Return value empty')
                }                 
            }
            else {
                this._log("_parseRecipe(): couldn't parse file content", 'ERROR');
                throw new Error('File Content Empty');                
            }
        }
        else {
            this._log('_parseRecipe(): invalid file content', 'ERROR');
            throw new Error('File Content Empty');
        }
    }

    /****************************************************************************************************
     * Updates the current datapoint values
     * if we dont have a text for the datapoint, meaning if this is the first time this function is run,
     * we also set the text, and some properties concerning units (unittext, conversion functions);
     ****************************************************************************************************/
    async _updateDatapointCurrentValues() {
        for (let [idx, line] of this.showRecipe.entries()) {
            //Get the datapoint
            const datapoint = await window.sigApi.datapointManager.getDatapoint(line.datapointString)
            //Get the value from the Datapoint
            const datapointValue = await datapoint.getValue();
            //Save it in the current recipe
            //Since line is a reference to an array entry from this.showRecipe this is possible
            line.datapointValue = datapointValue;

            //Add additional variables like the unit
            if (!line.datapointText1 || true) {//TODO REMOVE, TEST FOR UNIT SCHEMES
                //Get the datapoint text
                const datapointText1 = await window.sigApi.datapointManager.getDatapointText(line.datapointString, 'text1');
                line.datapointText1 = datapointText1;

                let convertTo, convertFrom;
                const datapointProperties = datapoint.getProperties();

                //Add the funcionas to convert the value according to its unit
                if (datapointProperties.unit) {
                    let unit;

                    //check for multiple units (unit and scheme property)
                    if ((datapointProperties.unit.children) && (datapointProperties.unit.children.length > 1)) {
                        // There is a scheme and a unit so we need to get the scheme if we dont get a value of the scheme we take the unit
                        let i = 0;
                        let uScheme, uUnit;
                        for (i = 0; i < datapointProperties.unit.children.length; i++) {
                            if (typeof datapointProperties.unit.children[i].value === 'string') {
                                uScheme = datapointProperties.unit.children[i];
                            } else {
                                uUnit = datapointProperties.unit.children[i];
                            }
                        }

                        // get the Unit of the Scheme
                        const splitUnitValue = uScheme.value.split(':');
                        // get the unit out of the scheme with the input we just got
                        const awaitUnit = await window.sigApi.properties.doScheme(splitUnitValue[1], splitUnitValue[0]);

                        // Is there a unit in the scheme for the value?
                        if (awaitUnit) {
                            unit = awaitUnit;
                        } else {
                            unit = window.sigApi.unitManager.getUnit(uUnit.value);
                        }

                    } else {
                        const unitValue = datapointProperties.unit.value;
                        // There is either a scheme or a unit set so get either one of them
                        if (typeof unitValue === 'string') {
                            // get the Unit of the Scheme
                            const splitUnitValue = unitValue.split(':');
                            // get the unit out of the scheme with the input we just got
                            const awaitUnit = await window.sigApi.properties.doScheme(splitUnitValue[1], splitUnitValue[0]);
                            unit = awaitUnit;
                        } else {
                            unit = window.sigApi.unitManager.getUnit(unitValue);
                        }
                    }
                    if (unit) {
                        const unitText = unit.getUnitText();
                        line.unitText = unitText;

                        convertTo = (value) => {
                            let convVal = value;
                            return sigApi.properties.formatValueWithUnit(unit.getName(), convVal, datapoint.isFloatingPoint())
                        }

                        convertFrom = (value) => {
                            return sigApi.properties.convertValueBackWithUnit(unit.getName(), value, datapoint.isFloatingPoint());
                        }
                    } else {
                        line.unitText = '';
                        //If no unit was assigned to the datapoint, we still add the conversion functions for consistency
                        convertTo = (value) => {
                            return value;
                        }
                        convertFrom = (value) => {
                            return value;
                        }
                    }

                } else {
                    line.unitText = '';
                    //If no unit was assigned to the datapoint, we still add the conversion functions for consistency
                    convertTo = (value) => {
                        return value;
                    }
                    convertFrom = (value) => {
                        return value;
                    }
                }


                line.convertTo = convertTo;
                line.convertFrom = convertFrom;
            }


            this._progress((idx / this.showRecipe.length).toFixed(2), `Update Current Datapoint Values:`);
        }
        return true;
    }

    /****************************************************************************************************
     * Handles scroll events
     * updates the List
     *
     * @param {*} e - the scroll event
     ****************************************************************************************************/
    _scrollHandler(e) {
        const firstEl = this._getFirstElement();
        this._updateElements(firstEl);

    }

    /****************************************************************************************************
     * Calculates the first shown element
     * 
     * @return {Number} The index of the firs shown element
     ****************************************************************************************************/
    _getFirstElement() {
        const scrollTop = this.$.lineList.scrollTop;
        let firstEl = Math.floor(scrollTop / this.compositeControlBounds.height);
        return firstEl;
    }

    /****************************************************************************************************
     * Shows and initializes the progress overlay
     ****************************************************************************************************/
    _progressStart() {
        this.$.progress.innerHTML = `0%`;
        //If present, setup the progress bar
        if (this.progressBar) {
            //Min and max value so that the progress can be shown between 0 and 100%
            this.progressBar.minvalue = 0;
            this.progressBar.maxvalue = 100;
            //Set the unit, make sure it is shown
            this.progressBar.unittext = '%';
            this.progressBar.showunit = true;
            //Reset the progressbars progress
            this.progressBar.value = 0;

            //Show the progress bar
            this.progressBar.style.display = '';
        }
        //After everything has been setup, show the progress overlay                
        this.$.progress.classList.remove('hide');
    }

    /**
     
     * */
    /****************************************************************************************************
     * Updates the progress overlay
     *
     * @param {*} percent - the progress in percent
     * @param {*} text - the text that should be shown
     ****************************************************************************************************/
    async _progress(percent, text) {
        let convertedPercent = percent * 100;
        this.$.progress.innerHTML = `${text} ${convertedPercent.toFixed(1)}%`;
        if (this.progressBar) {
            this.progressBar.value = Number(convertedPercent.toFixed(1));
        }
    }

    /****************************************************************************************************
     * Hides the progress overlay
     ****************************************************************************************************/
    _progressEnd() {
        this.$.progress.classList.add('hide');
        if (this.progressBar) {
            this.progressBar.style.display = 'none';
        }
    }

    /****************************************************************************************************
     * Sorts the loaded recipe
     * 
     * @param {String} prop - the property used for the comparison
     * @param {Boolean} asc - if true, the sorting will be ascending
     * @param {Function} cmp - the comparison function
     ****************************************************************************************************/
    _sort(prop = 'datapointText1', asc = false, cmp) {
        if (!cmp) {
            this.showRecipe.sort((a, b) => {
                if (a[prop] > b[prop]) {
                    return asc ? 1 : -1;
                } else {
                    return asc ? -1 : 1;
                }
            })
        } else {
            this.showRecipe.sort(cmp);
        }
        this._updateLines(0);
    }

    /****************************************************************************************************
     * filters the loaded recipe
     * 
     * @param {String} prop - the property used for the filter
     * @param {String} val - the value used for comparison
     * @param {Function} fcn - the filter function
     ****************************************************************************************************/
    _filter(prop = 'datapointText1', val = '', fcn) {
        if (!fcn) {
            this.showRecipe = this.originalRecipe.filter((entry) => {
                const type = typeof entry[prop];
                //Default filtering only makes sense with strings I think?
                if (type === 'string') {
                    return entry[prop].indexOf(val) >= 0;
                }
            })
        } else {
            this.showRecipe = this.originalRecipe.filter(fcn);
        }
        this._sortListFromEnumSettings();
        this._updateLines(0);

    }

    /****************************************************************************************************
     * Observer for the filter properties
     * filter11,filter12
     * filter21, filter22
     * combines them and saves them in the filter1 and filter2 variables
     ****************************************************************************************************/
    _updateFilter() {
        if ((this.filter11 || this.filter11 === 0) && (this.filter12 || this.filter12 === 0)) {
            this.filter1 = Math.pow(2, 32) * this.filter11 + this.filter12;
        }
        if ((this.filter21 || this.filter21 === 0) && (this.filter22 || this.filter22 === 0)) {
            this.filter2 = Math.pow(2, 32) * this.filter21 + this.filter22;
        }
        if(this.filename && this.filename.length) {
            this._loadRecipe(this.filename);
        }
        
    }

    /****************************************************************************************************
     * Saves a recipe with the recipe values instead of the current values 
     * @param {String} filePath - path name where to save the recipe
     ****************************************************************************************************/
    _saveRecipeValues(filePath) {
        this._progressResultUpdate('begin','save',0);

        const fileText = this._createFileText(true, true);          

        //make a blob out of the text, so that the fileManager api can work with it
        const blob = new Blob([fileText], { type: 'text/plain' });
        
        //Create the file
        this.fmApi.writeFile(filePath, blob).then(() => {
            //reset modified state
            this.modifiedstate = 0;
            // Write completed
            this._progressResultUpdate('end', 'save', 1);
        }).catch(e => {
            // Write failed
            this._log(`_saveRecipeValues(): Error saving recipe values: ${e.message}`, 'ERROR');
            this._progressResultUpdate('end', 'save', this.errorOffsetFilemanApi - e.status);
        });
    }
}
customElements.define(LasalRuntimeSigRecipeManagerElement.is, LasalRuntimeSigRecipeManagerElement);