// 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 knob() returns a HTML literal
import { LasalRuntimeSigElement, SigPolymer } from '../../sigmatek/sig-element/sig-element.js';

/**********************************************************************************************************************************************************
 * @class LasalRuntimeComponentKnob
 * @extends {LasalRuntimeSigElement}
***********************************************************************************************************************************************************/
class LasalRuntimeComponentKnob extends LasalRuntimeSigElement {
    /****************************************************************************************************
     * define the custom tag name 
     *
     * @readonly
     * @static
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    static get is() {
        return "sig-control-knob";
    }


    /****************************************************************************************************
     * 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
     * 
     * @readonly
     * @static
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    static get importMeta() { return import.meta; }


    /****************************************************************************************************
     * Return the component style and knob as a knob literal
     *
     * @readonly
     * @static
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    static get template() {
        return SigPolymer.html`
        <style include="sig-element-css">
            :host {             
                /* Basic styling of the component */
                background-color: var(--theme-sig-control-knob-background-color, inherit);
                background-image: var(--theme-sig-control-knob-realbackground-image, none);
                background-size : var(--theme-sig-control-knob-backgroundimagedisplay, initial);
                background-repeat : no-repeat;

                /* Basic styling of the borders */
                border-width: var(--theme-sig-control-knob-border-width, 0px);
                border-color: var(--theme-sig-control-knob-border-color, inherit);
                border-style: var(--theme-sig-control-knob-border-style, solid);

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

            :host * {
                /* Reset margin and padding for child elements and turn off text selection */
                @apply --notextselect;
                margin: 0;
                padding: 0;
            }

            .clearfix:after {
                /* Import the global clearfix css mixin */
                @apply --clearfix;
            }

            #container{
                width : 100%;
                height : 100%;
            }

            #knob{
                height: fit-content;
                width: fit-content;
                overflow : hidden;  
                box-sizing: border-box;
                display : flex;     /* for objectfit  */
            }

            #knob img{
                pointer-events : none;
                object-fit: contain;  
            }
        
        </style>
        <div id="container">
            <div id="knob" on-track="_handleTrack" on-tap="_handleTap">
                <img id="knobImage" src="[[cimgpath]]">
            </div>     
        </div>
        `;
    }


    /****************************************************************************************************
     * 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
     *
     * @readonly
     * @static
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    static get properties() {
        let props = {
            valueuc: {
                type: Number,
                value: 0,
                observer: "_valueChanged",
                notify: true
            },
            minimalvalueuc: {
                type: Number,
                value: 0,
                observer: "_valueChanged"
            },
            maximalvalueuc: {
                type: Number,
                value: 100,
                observer: "_valueChanged"
            },     
            imgpath: {                 // path for the knob image 
                type: String,
                value: ""
            },
            cimgpath: {
                type: String,
                computed: "_cImgPath(imgpath)"
            }
        }
        return props;
    }



    /****************************************************************************************************
     * Method that checks if an knob image was selected or not and assigns it then
     *
     * @param {*} imgPath - path of the image
     * @return {*} 
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    _cImgPath(imgPath) {
        let tempImgPath = '';
        //Check if we need to use the fallback
        if (!imgPath || imgPath === 'none' || imgPath === 'default') {
            tempImgPath = this.importPath + 'knob.svg';
        } else {
            //if we dont need to use the fallback we use the background image
            tempImgPath = imgPath;
        }

        // return image path
        return tempImgPath;
    }


    /****************************************************************************************************
     * Creates an instance of LasalRuntimeComponentKnob.
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    constructor() {
        super();
        //set property defaults
        this.rightend = 360;
        this.leftend = 0;

        this.controlBounds = undefined;

        this.propApi = window.sigApi.properties;
        if ((this.propApi === undefined) || (this.propApi === null)) {
            this._log("constructor(): API properties not available", "ERROR");
        }

        this.dpApi = window.sigApi.datapointManager;
        if ((this.dpApi === undefined) || (this.dpApi === null)) {
            this._log("constructor(): API datapointManager not available", "ERROR");
        }

        this.addPredefProps([
            {
                src: 'value',
                dest: 'valueuc',
                type: window.sigApi.SIG_CONST.PROP_PREDEF_PROPS_TYPE_UNCONVERTED_VALUE
            }, 
            {
                src: 'minimalvalue',
                dest: 'minimalvalueuc',
                type: window.sigApi.SIG_CONST.PROP_PREDEF_PROPS_TYPE_UNCONVERTED_VALUE
            }, 
            {
                src: 'maximalvalue',
                dest: 'maximalvalueuc',
                type: window.sigApi.SIG_CONST.PROP_PREDEF_PROPS_TYPE_UNCONVERTED_VALUE
            }, 
            {
                src: 'value',
                dest: 'limitLow',
                type: window.sigApi.SIG_CONST.PROP_PREDEF_PROPS_TYPE_LIMIT_LOW
            }, 
            {
                src: 'value',
                dest: 'limitHigh',
                type: window.sigApi.SIG_CONST.PROP_PREDEF_PROPS_TYPE_LIMIT_HIGH
            },            
        ]);

		// add required properties
		this.sigAddRequiredProperty("valueuc");
		this.sigAddRequiredProperty("leftend");
		this.sigAddRequiredProperty("rightend");
		this.sigAddRequiredProperty("minimalvalueuc");
		this.sigAddRequiredProperty("maximalvalueuc");  
		this.sigAddRequiredProperty("limitLow");
		this.sigAddRequiredProperty("limitHigh");                
    }


    /****************************************************************************************************
     * called after property default values are set and local DOM is initialized
     *
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    ready() {
        super.ready();

		this.isReady = true;

		// wait for next render in designer, for getting control bounds
		if (this.isdesignmode) {
			SigPolymer.afterNextRender(this, function () {
				this._initialize();
			});
		} 
        // no designer, get control bounds immediately
        else {
			if (this.propsReady) {
				this._initialize();
			}
		}
    }

	/****************************************************************************************************
	 * gets called if required properties are set
	 *
	 * @overwritten
	 * @memberof LasalRuntimeComponentKnob
	 ****************************************************************************************************/
     sigOnRequiredPropertiesReady() {
		super.sigOnRequiredPropertiesReady();
		this.propsReady = true;

		if (this.isReady) {
			this._initialize();
		}
	}

	/****************************************************************************************************
	 * Method to initialize control
	 *
	 * @private
	 * @memberof LasalRuntimeComponentKnob
	 ****************************************************************************************************/
    _initialize() {
        // set width and height to minimum as it was done in former version too -> needs to be compatible
        this.controlBounds = this.getControlBounds();

        // get border width of each side (could differ if shorthand settings are used)
        this._getBorderWidth(); 

        // the shortest side defines the diameter
        this.diameter = Math.min(this.controlBounds.height - this.borderTopWidth - this.borderBottomWidth, this.controlBounds.width - this.borderLeftWidth - this.borderRightWidth);        

        this.$.knobImage.style.height =this.diameter + 'px';
        this.$.knobImage.style.width =this.diameter + 'px';

		// control is ready
		this.initialized = true;

		// first draw of know
		this._valueChanged();
	}

	/****************************************************************************************************
	 * Method that handles on-doubleClick event
	 *
	 * @param {Event} event on-doubleClick event
	 * @private
	 * @memberof LasalRuntimeComponentKnob
	 ****************************************************************************************************/
     _handleTap(event) {
        //check if tap was done again
        if (this.doubleTapTimeout) {
            //clear timeout variable
            this.doubleTapTimeout = undefined;
            this._dispatchEvent("double-tap");
        }
		else {
            this.doubleTapTimeout = this.sigSetTimeout(() => {
                //simply clear timeout variable
                this.doubleTapTimeout = undefined;
            }, 300);
        }
	}


    /****************************************************************************************************
     * Method that does the calculation of the knob position
     *
     * @param {*} event - event data of track event
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    _handleTrack(event) {
        if (event.detail.state === 'start') {
            this.oldAngle = undefined;
            this.tracking = true;
            //get the actual angle
            this.actAngle = this._convertValueToAngle(this.valueuc);
            if (this.actAngle > this.rightend) {
                this.actAngle = this.rightend;
            } else if (this.actAngle < this.leftend) {
                this.actAngle = this.leftend;
            }
            //get bounding box
            const boundingBox = this.$.knob.getBoundingClientRect();

            //get centerpoint
            this.centerPoint = {
                x: boundingBox.left + (boundingBox.width / 2),
                y: boundingBox.top + (boundingBox.height / 2),
            }
        }

        //calculate offset
        const offset = {
            x: event.detail.x - this.centerPoint.x,
            y: event.detail.y - this.centerPoint.y
        }
        //calculate start angle of movement
        let angle = (Math.atan2(offset.y, offset.x) * 180) / Math.PI;

        if (this.oldAngle) {
            let newAngle = angle - this.oldAngle;
            //handle overflow
            if (newAngle > 180) {
                newAngle -= 360;
            }
            else if (newAngle < -180) {
                newAngle += 360;
            }
            //add angle to act angle
            let tmpAngle = this.actAngle + newAngle;
            //check if new angle reaches limit
            if (tmpAngle > this.rightend) {
                tmpAngle = this.rightend;
            }
            else if (tmpAngle < this.leftend) {
                tmpAngle = this.leftend;
            }
            this.valueuc = this._convertAngleToValue(tmpAngle);
            //write datapoint
            this._writeDatapoint(this.valueuc);
            this.actAngle = tmpAngle;
            this.$.knob.style.transform = `rotate(${(this.actAngle)}deg)`;  // rotate knob             
        }
        this.oldAngle = angle;

        //update angle to last value in case of last angles are out of value range
        if (event.detail.state === 'end') {
            this.tracking = false;
            this._valueChanged();
        }        
    }

    /****************************************************************************************************
     * method is used to write the value to the datapoint -> needs to be done via datapoint manager to
     * use unconverted value
     * @param {*} value - unconverted value which should be written
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    _writeDatapoint(value) {
        //we need to fetch property everytime to get datapoint in case of datapoint-scheme configured
        if ((this.propApi !== null) && (this.dpApi !== null)) {
            //get unconverted values of limits
            let lowerLimit, upperLimit;
            let prop = this.propApi.getProperty(this.id, "limitLow");
            lowerLimit = prop ? prop.getUnconvertedValue() : value;
            prop = this.propApi.getProperty(this.id, "limitHigh");
            upperLimit = prop ? prop.getUnconvertedValue() : value;
            //limit value needed cause otherwise the last value which is written before the limit is reached
            //isn't the limit
            value = (value < lowerLimit) ? lowerLimit : value;
            value = (value > upperLimit) ? upperLimit : value;
            //check if property configured
            prop = this.propApi.getProperty(this.id, 'value');
            if (prop !== null) {
                //get datapoint
                const dp = prop.getDatapoint();
                if (dp !== null) {
                    //write value
                    this.dpApi.writeDataPoint(dp.getId(), value).then().catch((error) => {
                        this._log('_writeDatapoint(): failed to write datapoint -> status: ' + error.status, 'ERROR');
                    });
                }
                else {
                    this._log('_writeDatapoint(): failed to get datapoint', 'ERROR');
                }
            }
        } 
        else {
            this._log('_writeDatapoint(): invalid apis', 'ERROR');
        }
    }

    /****************************************************************************************************
    * rotate the knob if the observer noticed a change of a certain value
    ****************************************************************************************************/
    _valueChanged() {
        if (!this.tracking && this.initialized) {
            let destAngle = this._convertValueToAngle(this.valueuc);
            if (destAngle < this.leftend) {
                destAngle = this.leftend;
            } else if (destAngle > this.rightend) {
                destAngle = this.rightend;
            }
            this.$.knob.style.transform = `rotate(${(destAngle)}deg)`;  // rotate knob 
        }
    }


    /****************************************************************************************************
     * Method that converts a given server value to its corresponding angle
     * the angle and the value itself have lower- and upper-limits
     *
     * @param {*} inputValue - value which should be converted to an angle
     * @return {*} 
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    _convertValueToAngle(inputValue) {
        const valRange = this.maximalvalueuc - this.minimalvalueuc;
        const percent = (inputValue - this.minimalvalueuc) / valRange;

        const range = this.rightend - this.leftend;
        const val = range * percent + this.leftend;
        return val;
    }


    /****************************************************************************************************
     * Method that converts a given angle to its corresponding server value
     * the angle and the value itself have lower- and upper-limits
     *
     * @param {*} intputAngle - angle which should be converted to a value
     * @return {*} 
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    _convertAngleToValue(intputAngle) {
        const range = this.rightend - this.leftend;
        const percent = (intputAngle - this.leftend) / range;

        const valRange = this.maximalvalueuc - this.minimalvalueuc;
        const val = valRange * percent + this.minimalvalueuc;
        return val;
    }


    /****************************************************************************************************
     * called if bounds were changed in designmode
     *
     * @memberof LasalRuntimeComponentKnob
     ****************************************************************************************************/
    designerOnBoundsChanged() {
        super.designerOnBoundsChanged();
        //set width and height new to image
        if (this.initialized) {
            this._initialize();
        }
    }

    /****************************************************************************************************
    * Method to get all the border width elements independent of the used shorthand style
    *
    * @memberof LasalRuntimeComponentKnob
    ****************************************************************************************************/
    _getBorderWidth() {
        const borderStyle = this.style.getPropertyValue('--theme-' + this.tagName.toLowerCase() + '-border-style');

        if (borderStyle === "none") {
            this.borderTopWidth = this.borderRightWidth = this.borderBottomWidth = this.borderLeftWidth = 0;
            return;
        }

        let borderWidth = this.style.getPropertyValue('--theme-' + this.tagName.toLowerCase() + '-border-width');

        if (borderWidth === "") {
            this.borderTopWidth = this.borderRightWidth = this.borderBottomWidth = this.borderLeftWidth = 0;            
        } else {
            let values = borderWidth.split(' ');
        
            switch (values.length) {
                case 1: // "5px"
                    this.borderTopWidth = this.borderRightWidth = this.borderBottomWidth = this.borderLeftWidth = parseInt(values[0]);
                    break;
                case 2: // "1px 5px"
                    this.borderTopWidth = this.borderBottomWidth = parseInt(values[0]);
                    this.borderRightWidth = this.borderLeftWidth = parseInt(values[1]);
                    break;
                case 3: // "1px 5px 10px"
                    this.borderTopWidth = parseInt(values[0]);
                    this.borderRightWidth = this.borderLeftWidth = parseInt(values[1]);
                    this.borderBottomWidth = parseInt(values[2]);
                    break;
                case 4: // "1px 5px 10px 15px"
                    this.borderTopWidth = parseInt(values[0]);
                    this.borderRightWidth = parseInt(values[1]);
                    this.borderBottomWidth = parseInt(values[2]);
                    this.borderLeftWidth = parseInt(values[3]);
                    break;
                default: // invalid format => use no border
                    this.borderTopWidth = this.borderRightWidth = this.borderBottomWidth = this.borderLeftWidth = 0;
                    break;
            }
        }
    }            

    /****************************************************************************************************
    * Overriding the default sigApplyCSSValue function
    * needed because the component needs to be redrawn on some css changes
    *
    * @param {*} cssObj
    * @param {boolean} [doBoundsCheck=false]
    * @return {*} 
    * @memberof LasalRuntimeSigGauge2
    ****************************************************************************************************/
    sigApplyCSSValue(cssObj, doBoundsCheck = false) {
        super.sigApplyCSSValue(cssObj, doBoundsCheck);
        // value only changes in design mode
        if(this.isdesignmode) {
            if ((cssObj.hasOwnProperty('--theme-sig-control-knob-border-width'))||
                (cssObj.hasOwnProperty('--theme-sig-control-knob-border-style'))) {
                this._initialize();
            }
        }
    }    
}
customElements.define(LasalRuntimeComponentKnob.is, LasalRuntimeComponentKnob);