/**
 * @file valve.js
 *
 * Permission is hereby granted to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to use it for personal 
 * or non-commercial purposes, with the following restrictions:
 *
 * 1. **No Copying or Redistribution**: The Software or any of its parts may not 
 *    be copied, merged, distributed, sublicensed, or sold without explicit 
 *    prior written permission from the author.
 * 
 * 2. **Commercial Use**: Any use of the Software for commercial purposes requires 
 *    a valid license, obtainable only with the explicit consent of the author.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 
 * OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 * SOFTWARE.
 *
 * Ownership of this code remains solely with the original author. Unauthorized 
 * use of this Software is strictly prohibited.
 *
 * Author:
 * - Rene De Ren
 * Email:
 * - r.de.ren@brabantsedelta.nl
 *
 * Future Improvements:
 * - Time-based stability checks
 * - Warmup handling
 * - Dynamic outlier detection thresholds
 * - Dynamic smoothing window and methods
 * - Alarm and threshold handling
 * - Maintenance mode
 * - Historical data and trend analysis
 */
/**
 * @file valveClass.js
 *
 * Permission is hereby granted to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to use it for personal 
....
*/

//load local dependencies
const EventEmitter = require('events');
const {loadCurve,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions');

class Valve { 
    constructor(valveConfig = {}, stateConfig = {}) { 
       //basic setup
        this.emitter = new EventEmitter(); // nodig voor ontvangen en uitvoeren van events emit() --> Zien als internet berichten (niet bedraad in node-red)

        this.logger = new logger(valveConfig.general.logging.enabled,valveConfig.general.logging.logLevel, valveConfig.general.name);
        this.configManager = new configManager(); 
        this.defaultConfig = this.configManager.getConfig('valve'); // Load default config for rotating machine ( use software type name ? )
        this.configUtils = new configUtils(this.defaultConfig);

        // Load a specific curve
        this.model = valveConfig.asset.model; // Get the model from the valveConfig
        this.curve = this.model ? loadCurve(this.model) : null;

        //Init config and check if it is valid
        this.config = this.configUtils.initConfig(valveConfig);

        // Initialize measurements
        this.measurements = new MeasurementContainer();
        this.child = {}; // object to hold child information so we know on what to subscribe
        
        // Init after config is set
        this.state = new state(stateConfig, this.logger); // Init State manager and pass logger

        this.state.stateManager.currentState = "operational"; // Set default state to operational
        
        this.kv = 0; //default
        this.rho =  1,225 //dichtheid van lucht standaard
        this.T = 293; // temperatuur in K standaard
        this.downstreamP = 0.54 //hardcodes for now --> assumed to be constant watercolumn and deltaP diffuser
        this.currentMode = this.config.mode.current;

        // wanneer hij deze ontvangt is de positie van de klep verandererd en gaat hij de updateposition functie aanroepen wat dan alle metingen en standen gaat updaten 
        this.state.emitter.on("positionChange", (data) => {
          this.logger.debug(`Position change detected: ${data}`);
          this.updatePosition()}); //To update deltaP 

    
        this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
        this.vCurve = this.curve[1.204]; // specificy the desired density RECALC THIS AUTOMTICALLY BASED ON DENSITY OF AIR LATER OLIFANT!! 
        this.predictKv = new predict({curve:this.vCurve}); // load valve size (x : ctrl , y : kv relationship) 
        //this.logger.debug(`PredictKv initialized with curve: ${JSON.stringify(this.predictKv)}`);
    }

      // -------- Config -------- //
    updateConfig(newConfig) {
        this.config = this.configUtils.updateConfig(this.config, newConfig);
    }

    isValidSourceForMode(source, mode) {
        const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
        return allowedSourcesSet.has(source);
    }

    async handleInput(source, action, parameter) {
        if (!this.isValidSourceForMode(source, this.currentMode)) {
          let warningTxt = `Source '${source}' is not valid for mode '${this.currentMode}'.`;
          this.logger.warn(warningTxt);
          return {status : false , feedback: warningTxt};
        }
    
        this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`);
        try {
          switch (action) {
            case "execSequence":
              await this.executeSequence(parameter);
              break;
            case "execMovement": // past het setpoint aan - movement van klep stand
              await this.setpoint(parameter);
              break;
            case "emergencyStop":
              this.logger.warn(`Emergency stop activated by '${source}'.`);
              await this.executeSequence("emergencyStop");
              break;
            case "statusCheck":
              this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source  }'.`);
              break;
            default:
              this.logger.warn(`Action '${action}' is not implemented.`);
              break;
          }
          this.logger.debug(`Action '${action}' successfully executed`);
          return {status : true , feedback: `Action '${action}' successfully executed.`};
        } catch (error) {
          this.logger.error(`Error handling input: ${error}`);
        }
        
    }

    setMode(newMode) {
        const availableModes = defaultConfig.mode.current.rules.values.map(v => v.value);
        if (!availableModes.includes(newMode)) {
          this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`);
          return;
        }
    
        this.currentMode = newMode;
        this.logger.info(`Mode successfully changed to '${newMode}'.`);
    }

    // -------- Sequence Handlers -------- //  
  async executeSequence(sequenceName) {

    const sequence = this.config.sequences[sequenceName];

    if (!sequence || sequence.size === 0) {
      this.logger.warn(`Sequence '${sequenceName}' not defined.`);
      return;
    }

    if (this.state.getCurrentState() == "operational" && sequenceName == "shutdown") {
      this.logger.info(`Machine will ramp down to position 0 before performing ${sequenceName} sequence`);
      await this.setpoint(0);
    }

    this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`);

    for (const state of sequence) {
      try {
        await this.state.transitionToState(state);
        // Update measurements after state change

      } catch (error) {
        this.logger.error(`Error during sequence '${sequenceName}': ${error}`);
        break; // Exit sequence execution on error
      }
    }
    }

    async setpoint(setpoint) {

    try {
      // Validate setpoint
      if (typeof setpoint !== 'number' || setpoint < 0) {
        throw new Error("Invalid setpoint: Setpoint must be a non-negative number.");
      }

      // Move to the desired setpoint
      await this.state.moveTo(setpoint);

    } catch (error) {
      console.error(`Error setting setpoint: ${error}`);
    }
  }

  updatePressure(variant,value,position) {
    if( value === null || value === undefined) {
      this.logger.warn(`Received null or undefined value for flow update. Variant: ${variant}, Position: ${position}`);
      return;
    }
    this.logger.debug(`Updating pressure: variant=${variant}, value=${value}, position=${position}`);

    switch (variant) {
      case ("measured"):
        // put value in measurements container
        console.log( 'wtf ... ' + value);
        this.measurements.type("pressure").variant("measured").position(position).value(value); 
        // get latest downstream pressure measurement
        const measuredDownStreamP = this.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(); //update downstream pressure measurement
        // update predicted flow measurement
        this.updateDeltaPKlep(value,this.kv,measuredDownStreamP,this.rho,this.T); //update deltaP based on new flow
        break;

      case ("predicted"):
        // put value in measurements container
        this.measurements.type("pressure").variant("predicted").position(position).value(value);
        const predictedDownStreamP = this.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(); //update downstream pressure measurement
        this.updateDeltaPKlep(value,this.kv,predictedDownStreamP,this.rho,this.T); //update deltaP based on new flow
        break;

      default:
        this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
        break;
    }
  }

  updateMeasurement(variant, subType, value, position) {
    this.logger.debug(`---------------------- updating ${subType} ------------------ `);
    switch (subType) {
      case "pressure":
        // Update pressure measurement
        this.updatePressure(variant,value,position);
        break;
      case "flow":
        this.updateFlow(variant,value,position);
        break;
      case "power":
        // Update power measurement
        break;
      default:
        this.logger.error(`Type '${subType}' not recognized for measured update.`);
        return;
    }
  }

  // NOTE: Omdat met zeer kleine getallen wordt gewerkt en er kwadraten in de formule zitten kan het zijn dat we alles *1000 moeten doen 
  updateDeltaPKlep(q,kv,downstreamP,rho,temp){
      //q must be in Nm3/h
      //temp must be in K
      //q must be in m3/h

      //downstreamP must be in bar so transfer from mbar to bar
      downstreamP = downstreamP / 1000;
      //convert downstreamP to absolute bar 
      downstreamP += 1.01325;
  
      if( kv !== 0 && downstreamP != 0 && q != 0) { //check if kv and downstreamP are not zero to avoid division by zero
        
        //calculate deltaP
        let deltaP = ( q**2 * rho * temp ) / ( 514**2 * kv**2 * downstreamP);
        
        //convert deltaP to mbar
        deltaP = deltaP * 1000;

        // Synchroniseer deltaP met het Valve-object
        this.deltaPKlep = deltaP

        // Opslaan in measurement container
        this.measurements.type("pressure").variant("predicted").position("delta").value(deltaP);
        this.logger.info('DeltaP updated to: ' + deltaP);

        this.emitter.emit('deltaPChange', deltaP); // Emit event to notify valveGroupController of deltaP change
        this.logger.info('DeltaPChange emitted to valveGroupController');
      }

    }

     
  // Als er een nieuwe flow door de klep komt doordat de machines harder zijn gaan werken, dan update deze functie dit ook in de valve attributes en measurements
  updateFlow(variant,value,position) {
    if( value === null || value === undefined) {
      this.logger.warn(`Received null or undefined value for flow update. Variant: ${variant}, Position: ${position}`);
      return;
    }
    this.logger.debug(`Updating flow: variant=${variant}, value=${value}, position=${position}`);

    switch (variant) {
      case ("measured"):
        // put value in measurements container
        this.measurements.type("flow").variant("measured").position(position).value(value); 
        // get latest downstream pressure measurement
        const measuredDownStreamP = this.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(); //update downstream pressure measurement
        // update predicted flow measurement
        this.updateDeltaPKlep(value,this.kv,measuredDownStreamP,this.rho,this.T); //update deltaP based on new flow
        break;

      case ("predicted"):
        // put value in measurements container
        this.measurements.type("flow").variant("predicted").position(position).value(value);
        const predictedDownStreamP = this.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(); //update downstream pressure measurement
        this.updateDeltaPKlep(value,this.kv,predictedDownStreamP,this.rho,this.T); //update deltaP based on new flow
        break;

      default:
        this.logger.warn(`Unrecognized variant '${variant}' for flow update.`);
        break;
    }
  }

  updatePosition() { //update alle parameters nadat er een verandering is geweest in stand van klep
    if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") {
      
      this.logger.debug('Calculating new deltaP');
      const currentPosition = this.state.getCurrentPosition();
      const measuredFlow = this.measurements.type("flow").variant("measured").position("downstream").getCurrentValue(); // haal de flow op uit de measurement containe
      const predictedFlow = this.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue(); // haal de predicted flow op uit de measurement container
      const currentFlow = predictedFlow ;
      
      const downstreamP = this.measurements.type("pressure").variant("measured").position("downstream").getCurrentValue(); // haal de downstream pressure op uit de measurement container
      //const valveSize = 125; //NOTE: nu nog hardcoded maar moet een attribute van de valve worden
      this.predictKv.fDimension = 125; //load valve size by defining fdimension in predict class
      const x = currentPosition; // dit is de positie van de klep waarvoor we delta P willen berekenen
      const y = this.predictKv.y(x); // haal de waarde van kv op uit de spline

      this.kv = y; //update de kv waarde in de valve class
      if (this.kv < 0.1){
        this.kv = 0.1; //minimum waarde voor kv
      }
      this.logger.debug(`Kv value for position valve ${x} is ${this.kv}`); // log de waarde van kv
  
      this.updateDeltaPKlep(currentFlow,this.kv,downstreamP,this.rho,this.T); //update deltaP

    }
  }

  getOutput() { 

    // Improved output object generation
    const output = {};
    //build the output object
    this.measurements.getTypes().forEach(type => {
      this.measurements.getVariants().forEach(variant => {
        this.measurements.getPositions().forEach(position => {

        const value = this.measurements.type(type).variant(variant).position(position).getCurrentValue(); //get the current value of the measurement
        
      
        if (value != null) {
          output[`${position}_${variant}_${type}`] = value;
        }
      });
      });
    });

    //fill in the rest of the output object
    output["state"] = this.state.getCurrentState();
    output["percentageOpen"] = this.state.getCurrentPosition();
    output["moveTimeleft"] = this.state.getMoveTimeLeft();
    output["mode"] = this.currentMode;

    //this.logger.debug(`Output: ${JSON.stringify(output)}`);

    return output;
  }

}

module.exports = Valve;



