const EventEmitter = require('events');
const {logger,configUtils,configManager,MeasurementContainer} = require('generalFunctions');

class Measurement {
  constructor(config={}) {

    this.emitter = new EventEmitter();  // Own EventEmitter
    this.configManager = new configManager(); 
    this.defaultConfig = this.configManager.getConfig('measurement');
    this.configUtils = new configUtils(this.defaultConfig);
    this.config = this.configUtils.initConfig(config);

    // Init after config is set
    this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);

    // General properties
    this.measurements = new MeasurementContainer({
      autoConvert: true,
      windowSize: this.config.smoothing.smoothWindow
    });

    this.measurements.setChildId(this.config.general.id);
    this.measurements.setChildName(this.config.general.name);

    // Smoothing
    this.storedValues = [];

    // Simulation
    this.simValue = 0;

    // Internal tracking
    this.inputValue = 0;
    this.outputAbs = 0;
    this.outputPercent = 0;

    // Stability
    this.stableThreshold = null;

    //internal variables
    this.totalMinValue = Infinity;
    this.totalMaxValue = -Infinity;
    this.totalMinSmooth = 0;
    this.totalMaxSmooth = 0;

    // Scaling
    this.inputRange = Math.abs(this.config.scaling.inputMax - this.config.scaling.inputMin);
    this.processRange = Math.abs(this.config.scaling.absMax - this.config.scaling.absMin);

    this.logger.debug(`Measurement id: ${this.config.general.id}, initialized successfully.`);

  }

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

  async tick() {
    if (this.config.simulation.enabled) {
      this.simulateInput();
    }

    this.calculateInput(this.inputValue);
    return Promise.resolve();
  }

  calibrate() {

    let offset = 0;

    const { isStable } = this.isStable();

    //first check if the input is stable
    if( !isStable ){
      this.logger.warn(`Large fluctuations detected between stored values. Calibration aborted.`);
    }else{

      this.logger.info(`Stable input value detected. Proceeding with calibration.`);

      // offset should be the difference between the input and the output
      if(this.config.scaling.enabled){
        offset = this.config.scaling.inputMin - this.outputAbs;
      } else {
        offset = this.config.scaling.absMin - this.outputAbs;
      }

      this.config.scaling.offset = offset;
      this.logger.info(`Calibration completed. Offset set to ${offset}`);
    }
  }

  isStable() {
    const marginFactor  = 2;  // or 3, depending on strictness
    let stableThreshold = 0;

    if (this.storedValues.length < 2) return false;
        const stdDev = this.standardDeviation(this.storedValues);
        stableThreshold = stdDev * marginFactor;

    return { isStable: ( stdDev < stableThreshold || stdDev == 0) , stdDev} ;
  }

  evaluateRepeatability() {

    const { isStable, stdDev } = this.isStable(); 

   if(this.config.smoothing.smoothMethod == 'none'){
      this.logger.warn('Repeatability evaluation is not possible without smoothing.');
      return null;
    }
  
    if (this.storedValues.length < 2) {
      this.logger.warn('Not enough data to evaluate repeatability.');
      return null;
    }

    if( isStable == false){
      this.logger.warn('Data not stable enough to evaluate repeatability.');
      return null;
    }

    const standardDeviation = stdDev

    this.logger.info(`Repeatability evaluated. Standard Deviation: ${stdDev}`);

    return standardDeviation;
  }

  simulateInput() {

    // Simulate input value
    const absMax = this.config.scaling.absMax;
    const absMin = this.config.scaling.absMin;
    const inputMin = this.config.scaling.inputMin;
    const inputMax = this.config.scaling.inputMax;
    const sign = Math.random() < 0.5 ? -1 : 1;
    let maxStep = 0;
    
    switch ( this.config.scaling.enabled ) {
      case true:

        maxStep = this.inputRange > 0 ? this.inputRange * 0.05 : 1;

        if (this.simValue < inputMin || this.simValue > inputMax) {
          this.logger.warn(`Simulated value ${this.simValue} is outside of input range constraining between min=${inputMin} and max=${inputMax}`);
          this.simValue = this.constrain(this.simValue, inputMin, inputMax);
        }
        break;
      case false:
        
        maxStep = this.processRange > 0 ? this.processRange * 0.05 : 1;

        if (this.simValue < absMin || this.simValue > absMax) {
          this.logger.warn(`Simulated value ${this.simValue} is outside of abs range constraining between min=${absMin} and max=${absMax}`);
          this.simValue = this.constrain(this.simValue, absMin, absMax);
        }
        break;
    }

    this.simValue += sign * Math.random() * maxStep;

    this.inputValue = this.simValue;

  }

  outlierDetection(val) {
    if (this.storedValues.length < 2) return false;

    this.logger.debug(`Outlier detection method: ${this.config.outlierDetection.method}`);

    switch (this.config.outlierDetection.method) {
      case 'zScore':
        return this.zScoreOutlierDetection(val);
      case 'iqr':
        return this.iqrOutlierDetection(val);
      case 'modifiedZScore':
        return this.modifiedZScoreOutlierDetection(val);
      default:
        this.logger.warn(`Outlier detection method "${this.config.outlierDetection.method}" is not recognized.`);
        return false;
    }
  }

  zScoreOutlierDetection(val) {
    const threshold = this.config.outlierDetection.threshold || 3;
    const mean = this.mean(this.storedValues);
    const stdDev = this.standardDeviation(this.storedValues);
    const zScore = (val - mean) / stdDev;

    if (Math.abs(zScore) > threshold) {
      this.logger.warn(`Outlier detected using Z-Score method. Z-score=${zScore}`);
      return true;
    }

    return false;
  }

  iqrOutlierDetection(val) {
    const sortedValues = [...this.storedValues].sort((a, b) => a - b);
    const q1 = sortedValues[Math.floor(sortedValues.length / 4)];
    const q3 = sortedValues[Math.floor(sortedValues.length * 3 / 4)];
    const iqr = q3 - q1;
    const lowerBound = q1 - 1.5 * iqr;
    const upperBound = q3 + 1.5 * iqr;

    if (val < lowerBound || val > upperBound) {
      this.logger.warn(`Outlier detected using IQR method. Value=${val}`);
      return true;
    }

    return false;
  }

  modifiedZScoreOutlierDetection(val) {
    const median = this.medianFilter(this.storedValues);
    const mad = this.medianFilter(this.storedValues.map(v => Math.abs(v - median)));
    const modifiedZScore = 0.6745 * (val - median) / mad;
    const threshold = this.config.outlierDetection.threshold || 3.5;

    if (Math.abs(modifiedZScore) > threshold) {
      this.logger.warn(`Outlier detected using Modified Z-Score method. Modified Z-Score=${modifiedZScore}`);
      return true;
    }

    return false;
  }

  calculateInput(value) {

    // Check if the value is an outlier and check if outlier detection is enabled
    if (this.config.outlierDetection.enabled) {
      if ( this.outlierDetection(value) ){
        this.logger.warn(`Outlier detected. Ignoring value=${value}`);
        return;
      }
    }

    // Apply offset
    let val = this.applyOffset(value);
  
    // Track raw min/max
    this.updateMinMaxValues(val);
  
    // Handle scaling if enabled
    if (this.config.scaling.enabled) {
      val = this.handleScaling(val);
    }
  
    // Apply smoothing
    const smoothed = this.applySmoothing(val);
  
    // Update smoothed min/max and output
    this.updateSmoothMinMaxValues(smoothed);
    this.updateOutputAbs(smoothed);
  }
  
  applyOffset(value) {
    return value + this.config.scaling.offset;
  }
  
  handleScaling(value) {
    // Check if input range is valid
    if (this.inputRange <= 0) {
      this.logger.warn(`Input range is invalid. Falling back to default range [0, 1].`);
      this.config.scaling.inputMin = 0;
      this.config.scaling.inputMax = 1;
      this.inputRange = this.config.scaling.inputMax - this.config.scaling.inputMin;
    }
  
    // Constrain value within input range
    if (value < this.config.scaling.inputMin || value > this.config.scaling.inputMax) {
      this.logger.warn(`Value=${value} is outside of INPUT range. Constraining.`);
      value = this.constrain(value, this.config.scaling.inputMin, this.config.scaling.inputMax);
    }
  
    // Interpolate value
    this.logger.debug(`Interpolating value=${value} between min=${this.config.scaling.inputMin} and max=${this.config.scaling.inputMax} to absMin=${this.config.scaling.absMin} and absMax=${this.config.scaling.absMax}`);
    return this.interpolateLinear(value, this.config.scaling.inputMin, this.config.scaling.inputMax, this.config.scaling.absMin, this.config.scaling.absMax);
  }

  constrain(input, inputMin , inputMax) {
    this.logger.warn(`New value=${input} is constrained to fit between min=${inputMin} and max=${inputMax}`);
    return Math.min(Math.max(input, inputMin), inputMax);
  }

  interpolateLinear(iNumber, iMin, iMax, oMin, oMax) {
    if (iMin >= iMax || oMin >= oMax) {
      this.logger.warn(`Invalid input for linear interpolation iMin=${JSON.stringify(iMin)} iMax=${iMax} oMin=${JSON.stringify(oMin)} oMax=${oMax}`);
      return iNumber;
    }
  
    const range = iMax - iMin;
    return oMin + ((iNumber - iMin) * (oMax - oMin)) / range;
  }
  
  applySmoothing(value) {
    
    this.storedValues.push(value);
  
    // Maintain only the latest 'smoothWindow' number of values
    if (this.storedValues.length > this.config.smoothing.smoothWindow) {
      this.storedValues.shift();
    }
  
    // Smoothing strategies
    const smoothingMethods = {
      none: (arr) => arr[arr.length - 1],
      mean: (arr) => this.mean(arr),
      min: (arr) => this.min(arr),
      max: (arr) => this.max(arr),
      sd: (arr) => this.standardDeviation(arr),
      lowPass: (arr) => this.lowPassFilter(arr),
      highPass: (arr) => this.highPassFilter(arr),
      weightedMovingAverage: (arr) => this.weightedMovingAverage(arr),
      bandPass: (arr) => this.bandPassFilter(arr),
      median: (arr) => this.medianFilter(arr),
      kalman: (arr) => this.kalmanFilter(arr),
      savitzkyGolay: (arr) => this.savitzkyGolayFilter(arr),
    };
  
    // Ensure the smoothing method is valid
    const method = this.config.smoothing.smoothMethod;
    this.logger.debug(`Applying smoothing method "${method}"`);

    if (!smoothingMethods[method]) {
      this.logger.error(`Smoothing method "${method}" is not implemented.`);
      return value;
    }
  
    // Apply the smoothing method
    return smoothingMethods[method](this.storedValues);
  }
  
  standardDeviation(values) {
    if (values.length <= 1) return 0;
    const mean = values.reduce((a, b) => a + b, 0) / values.length;
    const sqDiffs = values.map(v => (v - mean) ** 2);
    const variance = sqDiffs.reduce((a, b) => a + b, 0) / (values.length - 1);
    return Math.sqrt(variance);
  }

  savitzkyGolayFilter(arr) {
    const coefficients = [-3, 12, 17, 12, -3]; // Example coefficients for 5-point smoothing
    const normFactor = coefficients.reduce((a, b) => a + b, 0);
  
    if (arr.length < coefficients.length) {
      return arr[arr.length - 1]; // Return last value if array is too small
    }
  
    let smoothed = 0;
    for (let i = 0; i < coefficients.length; i++) {
      smoothed += arr[arr.length - coefficients.length + i] * coefficients[i];
    }
  
    return smoothed / normFactor;
  }
  
  kalmanFilter(arr) {
    let estimate = arr[0];
    const measurementNoise = 1; // Adjust based on your sensor's characteristics
    const processNoise = 0.1; // Adjust based on signal variability
    const kalmanGain = processNoise / (processNoise + measurementNoise);
  
    for (let i = 1; i < arr.length; i++) {
      estimate = estimate + kalmanGain * (arr[i] - estimate);
    }
  
    return estimate;
  }
  
  medianFilter(arr) {
    const sorted = [...arr].sort((a, b) => a - b);
    const middle = Math.floor(sorted.length / 2);
  
    return sorted.length % 2 !== 0
      ? sorted[middle]
      : (sorted[middle - 1] + sorted[middle]) / 2;
  }

  bandPassFilter(arr) {
    const lowPass = this.lowPassFilter(arr); // Apply low-pass filter
    const highPass = this.highPassFilter(arr); // Apply high-pass filter
  
    return arr.map((val, idx) => lowPass + highPass - val).pop(); // Combine the filters
  }
  
  weightedMovingAverage(arr) {
    const weights = arr.map((_, i) => i + 1); // Weights increase linearly
    const weightedSum = arr.reduce((sum, val, idx) => sum + val * weights[idx], 0);
    const weightTotal = weights.reduce((sum, weight) => sum + weight, 0);
  
    return weightedSum / weightTotal;
  }
  
  highPassFilter(arr) {
    const alpha = 0.8; // Smoothing factor (0 < alpha <= 1)
    let filteredValues = [];
    filteredValues[0] = arr[0];
  
    for (let i = 1; i < arr.length; i++) {
      filteredValues[i] = alpha * (filteredValues[i - 1] + arr[i] - arr[i - 1]);
    }
  
    return filteredValues[filteredValues.length - 1];
  }
  
  lowPassFilter(arr) {
    const alpha = 0.2; // Smoothing factor (0 < alpha <= 1)
    let smoothedValue = arr[0];
  
    for (let i = 1; i < arr.length; i++) {
      smoothedValue = alpha * arr[i] + (1 - alpha) * smoothedValue;
    }
  
    return smoothedValue;
  }

  // Or also EMA called exponential moving average
  recursiveLowpassFilter() {
    
  }
  
  mean(arr) {
    return arr.reduce((a, b) => a + b, 0) / arr.length;
  }

  min(arr) {
    return Math.min(...arr);
  }

  max(arr) {
    return Math.max(...arr);
  }

  updateMinMaxValues(value) {
    if (value < this.totalMinValue) {
      this.totalMinValue = value;
    }
    if (value > this.totalMaxValue) {
      this.totalMaxValue = value;
    }
  }

  updateSmoothMinMaxValues(value) {
    // If this is the first run, initialize them
    if (this.totalMinSmooth === 0 && this.totalMaxSmooth === 0) {
      this.totalMinSmooth = value;
      this.totalMaxSmooth = value;
    }
    if (value < this.totalMinSmooth) {
      this.totalMinSmooth = value;
    }
    if (value > this.totalMaxSmooth) {
      this.totalMaxSmooth = value;
    }
  }

  updateOutputAbs(val) {
    
        // Constrain first, then check for changes
    let constrainedVal = val;
    
    if (val < this.config.scaling.absMin || val > this.config.scaling.absMax) {
        this.logger.warn(`Output value=${val} is outside of ABS range. Constraining.`);
        constrainedVal = this.constrain(val, this.config.scaling.absMin, this.config.scaling.absMax);
    }
    
    const roundedVal = Math.round(constrainedVal * 100) / 100;

    //only update on change
    if (roundedVal != this.outputAbs) {

      // Constrain value within process range
      if (val < this.config.scaling.absMin || val > this.config.scaling.absMax) {
        this.logger.warn(`Output value=${val} is outside of ABS range. Constraining.`);
        val = this.constrain(val, this.config.scaling.absMin, this.config.scaling.absMax);
      }
      
      this.outputAbs = Math.round(val * 100) / 100;
      this.outputPercent = this.updateOutputPercent(val);
  
      this.emitter.emit('mAbs', this.outputAbs);// DEPRECATED: Use measurements container instead

      this.logger.debug(`Updating type: ${this.config.asset.type}, variant: ${"measured"}, postition : ${this.config.functionality.positionVsParent} container with new value: ${this.outputAbs}`);
      this.measurements.type(this.config.asset.type).variant("measured").position(this.config.functionality.positionVsParent).distance(this.config.functionality.distance).value(this.outputAbs, Date.now(),this.config.asset.unit );
    }
  }

  updateOutputPercent(value) {

    let outputPercent;

    if (this.processRange <= 0) {
      this.logger.debug(`Process range is smaller or equal to 0 interpolating between input range`); 
      outputPercent = this.interpolateLinear( value, this.totalMinValue, this.totalMaxValue, this.config.interpolation.percentMin, this.config.interpolation.percentMax );
    } 
    else {
      outputPercent = this.interpolateLinear( value, this.config.scaling.absMin, this.config.scaling.absMax, this.config.interpolation.percentMin, this.config.interpolation.percentMax );
    }

    return Math.round(outputPercent * 100) / 100;
  }

  toggleSimulation(){
    this.config.simulation.enabled = !this.config.simulation.enabled;
  }

  toggleOutlierDetection() {
    this.config.outlierDetection = !this.config.outlierDetection;
  }

  getOutput() {
    return {
      mAbs: this.outputAbs,
      mPercent: this.outputPercent,
      totalMinValue: this.totalMinValue,
      totalMaxValue: this.totalMaxValue,
      totalMinSmooth: this.totalMinSmooth,
      totalMaxSmooth: this.totalMaxSmooth,
    };
  }
}

module.exports = Measurement;

/*
// Testing the class
const configuration = { 
  general: {
    name: "PT1",
    logging: {
      enabled: true,
      logLevel: "debug",
    },
  },
  scaling:{
    enabled: true,
    inputMin: 0,
    inputMax: 3000,
    absMin: 500,
    absMax: 4000,
    offset: 1000
  },
  asset: {
    type: "pressure",
    unit: "bar",
    category: "measurement",
    model: "PT1",
    uuid: "123e4567-e89b-12d3-a456-426614174000",
    tagCode: "PT1-001",
    supplier: "DeltaTech"
  },
  smoothing: {
    smoothWindow: 10,
    smoothMethod: 'mean',
  },
  simulation: {
    enabled: true,
  },
  functionality: {
    positionVsParent: "upstream"
  }
};


const m = new Measurement(configuration);

m.logger.info(`Measurement created with config : ${JSON.stringify(m.config)}`);

m.logger.setLogLevel("debug");

//look for flow updates
m.measurements.emitter.on('pressure.measured.upstream', (newVal) => {
  m.logger.info(`Received : ${newVal.value} ${newVal.unit}`);
  const repeatability = m.evaluateRepeatability();
  if (repeatability !== null) {
    m.logger.info(`Current repeatability (standard deviation): ${repeatability}`);
  }
});

const tickLoop = setInterval(changeInput,1000);

function changeInput(){
  m.logger.info(`tick...`);
  m.tick();
  //m.inputValue = 5;
}

// */