// Copyright (c) 2016 - Patrick Schäfer (patrick.schaefer@zib.de)
// Distributed under the GLP 3.0 (See accompanying file LICENSE)

//modified by @a.shifaz 2018

package dotnet54.tscore.data;

import java.io.Serializable;
import java.util.Arrays;

public class UTimeSeries implements Serializable {
  private static final long serialVersionUID = 6340030797230203868L;

  protected double[] data = null;

  protected double mean = 0;
  protected double stddev = 1;

  protected boolean normed = false;
  protected Integer label = null;
  protected double weight = 1.0;

  public static boolean APPLY_Z_NORM = true;
  
  //TODO temp hot fix --- dev
  public boolean transformed_series;
//  protected String transform_type;
//  protected Transformer transform;
  public UTimeSeries original_series;
  
  //end

  public UTimeSeries(){}

  public UTimeSeries(double[] data) {
    this.data = data;
  }

  public UTimeSeries(double[] data, Integer label) {
    this(data);
    this.label = label;
  }

  public boolean isNormed() {
    return this.normed;
  }

  public double[] getData() {
    return this.data;
  }

  public double getData(int i) {
    return this.data[i];
  }

  public void setData(double[] init) {
    this.data = init;
  }

  public int getLength() {
    return this.data == null ? 0 : this.data.length;
  }

  public Integer getLabel() {
    return this.label;
  }
  
  public void setLabel(Integer label) {
    this.label = label;
  }  
	  
  public double getWeight() {
    return this.weight;
  }

  public void setWeight(double weight) {
    this.weight = weight;
  }
  

  
//  public String toString() {
//	  return this.label.toString() + ":" + Arrays.toString();
//  }
  
  public String toString() {
	  StringBuilder sb = new StringBuilder();
	  sb.append(this.label.toString());
	  sb.append(":");

	  int max = 10;
	  int i;
	  for (i = 0; i < Math.min(data.length, max); i++) {
		sb.append(data[i]);
		sb.append(",");
	  }
	  
	  if ( i == max) {
		  sb.append("...");
	  }
	  
	  return sb.toString();
  }
  
  public void norm() {
    norm(true);
  }

  /**
   * After zero-mean-normalization the following holds:
   *  mean = 0
   *  stddev = 1
   * @param normMean defines, if the mean should be subtracted from the time series
   */
  public void norm(boolean normMean) {
    this.mean = calculateMean();
    this.stddev = calculateStddev();

    if (!isNormed()) {
      norm(normMean, this.mean, this.stddev);
    }
  }

  /**
   * Used for zero-mean normalization.
   * @param normMean defines, if the mean should be subtracted from the time series
   * @param mean the mean to set (usually set to 0)
   * @param stddev the stddev to set (usually set to 1)
   */
  public void norm(boolean normMean, double mean, double stddev) {
    this.mean = mean;
    this.stddev = stddev;

    if (APPLY_Z_NORM && !isNormed()) {
      double inverseStddev = (this.stddev != 0) ? 1.0 / this.stddev : 1.0;

      if (normMean) {
        for (int i = 0; i < this.data.length; i++) {
          this.data[i] = (this.data[i] - this.mean) * inverseStddev;
        }
        this.mean = 0.0;
      } else if (inverseStddev != 1.0) {
        for (int i = 0; i < this.data.length; i++) {
          this.data[i] *= inverseStddev;
        }
      }

      //      this.mean = 0.0;
      //      this.stddev = 1.0;
      this.normed = true;
    }
  }

  public double calculateStddev() {
    this.stddev = 0;

    // stddev
    double var = 0;
    for (double value : getData()) {
      var += value * value;
    }

    double norm = 1.0 / ((double) this.data.length);
    double buf = norm * var - this.mean * this.mean;
    if (buf > 0) {
      this.stddev = Math.sqrt(buf);
    }

    return this.stddev;
  }


  public double calculateMean() {
    this.mean = 0.0;

    // get mean values
    for (double value : getData()) {
      this.mean += value;
    }
    this.mean /= (double) this.data.length;

    return this.mean;
  }

  public double getStddev() {
    return this.stddev;
  }

  public double getMean() {
    return this.mean;
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof UTimeSeries)) {
      throw new RuntimeException("Objects should be time series: " + (o.toString()));
    }

    UTimeSeries ts = (UTimeSeries) o;

    // Only compare normed time series
    if (ts.isNormed() != this.isNormed()) {
      throw new RuntimeException("Please norm both time series");
    }


    // same dimensionality?
    if (ts.getLength() != this.getLength()) {
      return false;
    }
    
    //TODO shifaz added check labels
    if (! this.label.equals(ts.getLabel())) {
    	return false;
    }
    
    //TODO weights?? nah

    for (int i = 0; i < ts.getLength(); i++) {
      // search for differences
      if (ts.data[i] != this.data[i]) {
        return false;
      }
    }

    return true;
  }


  /**
   * Get a subsequence starting at offset with queryLength windowSize.
   *
   * @param offset
   * @param windowSize
   * @return
   */
  public UTimeSeries getSubsequence(int offset, int windowSize) {
    double[] subsequenceData = Arrays.copyOfRange(this.data, offset, Math.min(data.length, offset + windowSize));
    UTimeSeries sequence = new UTimeSeries(subsequenceData, this.label);
    sequence.norm();
    return sequence;
  }

  /**
   * Get sliding windows with windowSize.
   *
   * @param windowSize
   * @param normMean defines, if the mean should be subtracted from the time series
   * @return
   */
  public UTimeSeries[] getSubsequences(int windowSize, boolean normMean) {
    // windowSize should not be larger than the  size
    int ws = Math.min(windowSize, this.data.length);

    // extract subsequences
    int size = (this.data.length - ws) + 1;
    UTimeSeries[] subsequences = new UTimeSeries[size];

    double[] means = new double[size];
    double[] stddevs = new double[size];

    calcIncrementalMeanStddev(windowSize, this.data, means, stddevs);

    for (int i = 0; i < subsequences.length; i++) {
      double subsequenceData[] = new double[windowSize];
      System.arraycopy(this.data, i, subsequenceData, 0, ws);

      // The newly created time series have queryLength windowSize and offset i
      subsequences[i] = new UTimeSeries(subsequenceData, getLabel());
      subsequences[i].norm(normMean, means[i], stddevs[i]);
    }
    return subsequences;
  }

  /**
   * Gets the means and stddevs for all sliding windows of a time series
   */
  public static void calcIncrementalMeanStddev(
      int windowLength,
      double[] tsData,
      double[] means,
      double[] stds) {
    double sum = 0;
    double squareSum = 0;

    // it is faster to multiply than to divide
    double rWindowLength = 1.0 / (double) windowLength;

    for (int ww = 0; ww < Math.min(tsData.length, windowLength); ww++) {
      sum += tsData[ww];
      squareSum += tsData[ww] * tsData[ww];
    }

    // first window
    means[0] = sum * rWindowLength;
    double buf = squareSum * rWindowLength - means[0] * means[0];
    stds[0] = buf > 0 ? Math.sqrt(buf) : 0;

    // remaining windows
    for (int w = 1, end = tsData.length - windowLength + 1; w < end; w++) {
      sum += tsData[w + windowLength - 1] - tsData[w - 1];
      means[w] = sum * rWindowLength;

      squareSum += tsData[w + windowLength - 1] * tsData[w + windowLength - 1] - tsData[w - 1] * tsData[w - 1];
      buf = squareSum * rWindowLength - means[w] * means[w];
      stds[w] = buf > 0 ? Math.sqrt(buf) : 0;
    }
  }

  /**
   * Extracts disjoint subsequences
   *
   * @param windowSize
   * @return
   */
  public UTimeSeries[] getDisjointSequences(int windowSize, boolean normMean) {

    // extract subsequences
    int amount = getLength() / windowSize;
    UTimeSeries[] subsequences = new UTimeSeries[amount];

    for (int i = 0; i < amount; i++) {
      double subsequenceData[] = new double[windowSize];
      System.arraycopy(this.data, i * windowSize, subsequenceData, 0, windowSize);
      subsequences[i] = new UTimeSeries(subsequenceData, getLabel());
      subsequences[i].norm(normMean);
    }

    return subsequences;
  }

}
