/*
 * This file is part of the UEA Time Series Machine Learning (TSML) toolbox.
 *
 * The UEA TSML toolbox is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License as published 
 * by the Free Software Foundation, either version 3 of the License, or 
 * (at your option) any later version.
 *
 * The UEA TSML toolbox is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with the UEA TSML toolbox. If not, see <https://www.gnu.org/licenses/>.
 */
 

package statistics.simulators;

import statistics.distributions.NormalDistribution;
import fileIO.*;

public class SinusoidalModel extends Model{
    boolean warpSeries=false;
    
    void setWarp(boolean w){warpSeries=w;}
 
    static double maxAmp=3;
//RANGE 0 ..1, offset gives the starting phase of the cycle. Note this can be randomised for each series at reset() by setting
// fixedOffset to false. This shifts ALL the offsets by a fixed, random amount, to retain relative effect.     
    double[] offset;
    boolean fixedOffset=true;
    public void setFixedOffset(boolean b){fixedOffset=b;}
    
//RANGE: 0 ... 1, 100*frequency gives the number of cycles between 0 and pi()
    double[] frequency;
//RANGE:0 .. 1, gives the max height: maxAmp
    double[] amplitude;
    int maxN=100;
    double interval = Math.PI/maxN;
//Default min max range
    static double defaultMin=0;
    static double defaultMax=1;

    public SinusoidalModel(double[] p){
        if(p.length%3!=0){
            System.out.println(" Error in Sinusoidal Model, the parameter list must be divisible by three (offset, frequency and amplitude");
            throw new IllegalArgumentException(" Error in Sinusoidal Model, the parameter list must be divisible by three (offset, frequency and amplitude");
        }
        offset=new double[p.length/3];
        frequency=new double[p.length/3];
        amplitude=new double[p.length/3];
        int count=0;
        for(int i=0;i<offset.length;i++)
            offset[i]=p[count++];
        for(int i=0;i<frequency.length;i++)
            frequency[i]=p[count++];
        for(int i=0;i<amplitude.length;i++)
            amplitude[i]=p[count++];
    }
    public void setInterval(int n)
    {
      maxN=n;
      interval = Math.PI/maxN;
     }
/** from the base class. Simply split the array into three    
 * 
 * @param p 
 */
    @Override
    public void setParameters(double[] p){
        if(p.length%3!=0){
            System.out.println(" Error in Sinusoidal Model, the parameter list must be divisible by three (offset, frequency and amplitude");
        }
        offset=new double[p.length/3];
        frequency=new double[p.length/3];
        amplitude=new double[p.length/3];
        int count=0;
        for(int i=0;i<offset.length;i++)
            offset[i]=p[count++];
        for(int i=0;i<frequency.length;i++)
            frequency[i]=p[count++];
        for(int i=0;i<amplitude.length;i++)
            amplitude[i]=p[count++];
    }
 	public SinusoidalModel(double[] offset, double[] frequency, double[] amplitude)
	{
            this.offset=new double[offset.length];
            System.arraycopy(offset, 0, this.offset, 0, offset.length);
            this.frequency=new double[frequency.length];
            System.arraycopy(frequency, 0, this.frequency, 0, frequency.length);
            this.amplitude=new double[amplitude.length];
            System.arraycopy(amplitude, 0, this.amplitude, 0, amplitude.length);
            t=0;
            
            error = new NormalDistribution(0,variance);
	}
	public SinusoidalModel(double[] offset, double[] frequency, double[] amplitude, double s)
	{
            this.offset=new double[offset.length];
            System.arraycopy(offset, 0, this.offset, 0, offset.length);
            this.frequency=new double[frequency.length];
            System.arraycopy(frequency, 0, this.frequency, 0, frequency.length);
            this.amplitude=new double[amplitude.length];
            System.arraycopy(amplitude, 0, this.amplitude, 0, amplitude.length);
            t=0;
            setVariance(s);
	}
	public void setSigma(double s){
		setVariance(s);
	}
    @Override
	public double generate(double x){
		double res=0;
		for(int i=0;i<offset.length;i++)
                    res+=((maxAmp)*amplitude[i])*(Math.sin(Math.PI*offset[i]+(2+100*frequency[i])*x));
		res+= error.simulate();
        return res;
	}

	public	double generateError(){	return error.simulate();}
    @Override
	public	double generate()
	{
		double res=0;
 		for(int i=0;i<offset.length;i++)
                    res+=((maxAmp)*amplitude[i])*(Math.sin(Math.PI*offset[i]+(2+100*frequency[i])*t));
		this.t+=interval;
		res+= error.simulate();
		return res;
	}
/** @Override
 * 
 * @param l
 * @return 
 */        
    @Override
	public	double[] generateSeries(int l)
	{
	    double[] data = new double[l];
            reset();
            setInterval(l);
	    for(int k=0;k<l;k++)
                data[k] =generate();
            if(!warpSeries)
                return data;
//Warping.
            double offsetPercent=10;
            int offset=(int)(data.length*offsetPercent/100);
            double[] newD=new double[data.length];
            System.arraycopy(data,0,newD,0,data.length);
//             Pick a random point somewhere between the first 10% and last 90%
            int warpPoint=(int)(offset+Math.random()*(data.length-offset*2));
// Warp offset points into offset*2 points
            newD[warpPoint+offset]=data[warpPoint];
//Warp offset points to fill the gap.            
            newD[warpPoint-offset]=data[warpPoint-offset];
            for(int i=1;i<=offset;i++){
                newD[warpPoint-offset+2*i-1]=data[warpPoint-offset+i];
                newD[warpPoint-offset+2*i]=(data[warpPoint-offset+i]+data[warpPoint-offset+i+1])/2;
 //               System.out.println(i+","+warpPoint+","+offset+","+data[warpPoint-offset+i]+","+newD[warpPoint-offset+2*i-1]+","+newD[warpPoint-offset+2*i]);
            }
            
//            OutFile of = new OutFile("");
            return newD;
	}
	static public	double[][] generateSinusoidalData(SinusoidalModel[] models,int length,int nosPerModel)
	{
	    double[][] data = new double[models.length*nosPerModel][];

        for(int i=0;i<models.length;i++)
        {

            for(int j=0;j<nosPerModel;j++)
               data[j+i*nosPerModel]= models[i].generateSeries(length);
        }
        return data;
    }
/** @Override
 * 
 */
    @Override
    public void reset(){
//If th        
       super.reset(); 
       if(!fixedOffset)  //Shift all the offsets by a random amount  
       {
           //Find the min and max of the whole offset, ta avoid shifting too much
           double min=offset[0];
           double max=offset[0];
           for(int i=1;i<offset.length;i++){
               if(min>offset[i])
                   min=offset[i];
               if(max<offset[i])
                   max=offset[i];
           }
//So now, any shift between -min to (1-max) should retain the relative offsets
           double shift=-min+(1-max+min)*Math.random();
           for(int i=1;i<offset.length;i++)
               offset[i]+=shift;
       }
    }
        
        
/** A Load of static methods 
 * 
 * @param r
 * @param min
 * @param max
 * @return 
 */        

    public static SinusoidalModel generateRandomModel(int r, double min, double max)
    {
           double[] freq=new double[r];
           double[] off = new double[r];
           double[] amp=new double[r];
           for(int i=0;i<r;i++)
           {
//              freq[i]=min+(max-min)*Distribution.RNG.nextDouble();
//              off[i]=min+(max-min)*Distribution.RNG.nextDouble();
//              amp[i]=min+(max-min)*Distribution.RNG.nextDouble();
          }
           SinusoidalModel a = new SinusoidalModel(off,freq,amp);
           return a;
    }
    public static SinusoidalModel generateRandomModelAmp(SinusoidalModel m, double maxDeviation)
    {
    	int r = m.frequency.length;
    	double[] freq=new double[r];
    	double[] off = new double[r];
    	double[] amp=new double[r];
    	double min,max;
    	for(int i=0;i<r;i++)
    	{
    		freq[i]=m.frequency[i];
    		off[i]=m.offset[i];
      		min=m.amplitude[i]-maxDeviation;
    		if(min<0) min=0;
    		max=m.amplitude[i]+maxDeviation;
    		if(max>1) max=1;    		
//    		amp[i]=min+(max-min)*Distribution.RNG.nextDouble();
    	}
    	SinusoidalModel a = new SinusoidalModel(off,freq,amp);
    	return a;
    }		
    public static SinusoidalModel generateRandomModelOff(SinusoidalModel m, double maxDeviation)
    {
    	int r = m.frequency.length;
    	double[] freq=new double[r];
    	double[] off = new double[r];
    	double[] amp=new double[r];
    	double min,max;
    	for(int i=0;i<r;i++)
    	{
    		freq[i]=m.frequency[i];
    		amp[i]=m.amplitude[i];
      		min=m.offset[i]-maxDeviation;
    		if(min<0) min=0;
    		max=m.offset[i]+maxDeviation;
    		if(max>1) max=1;    		
//    		off[i]=min+(max-min)*Distribution.RNG.nextDouble();
    	}
    	SinusoidalModel a = new SinusoidalModel(off,freq,amp);
    	return a;
    }		
    public static SinusoidalModel generateRandomModelFreq(SinusoidalModel m, double maxDeviation)
    {
    	int r = m.frequency.length;
    	double[] freq=new double[r];
    	double[] off = new double[r];
    	double[] amp=new double[r];
    	double min,max;
    	for(int i=0;i<r;i++)
    	{
    		amp[i]=m.amplitude[i];
    		off[i]=m.offset[i];
      		min=m.frequency[i]-maxDeviation;
    		if(min<0) min=0;
    		max=m.frequency[i]+maxDeviation;
    		if(max>1) max=1;    		
//    		freq[i]=min+(max-min)*Distribution.RNG.nextDouble();
    	}
    	SinusoidalModel a = new SinusoidalModel(off,freq,amp);
    	return a;
    }		
    public static SinusoidalModel generateRandomModel(SinusoidalModel m, double maxDeviation)
    {
    	
    	int r = m.frequency.length;
    	
    	double[] freq=new double[r];
    	double[] off = new double[r];
    	double[] amp=new double[r];
    	double min,max;
    	for(int i=0;i<r;i++)
    	{
    		min=m.frequency[i]-maxDeviation;
    		if(min<0) min=0;
    		max=m.frequency[i]+maxDeviation;
    		if(max>1) max=1;    		
 //   		freq[i]=min+(max-min)*Distribution.RNG.nextDouble();
    		min=m.offset[i]-maxDeviation;
    		if(min<0) min=0;
    		max=m.offset[i]+maxDeviation;
    		if(max>1) max=1;    		
 //   		off[i]=min+(max-min)*Distribution.RNG.nextDouble();
    		min=m.amplitude[i]-maxDeviation;
    		if(min<0) min=0;
    		max=m.amplitude[i]+maxDeviation;
//    		if(max>1) max=1;    		
//    		amp[i]=min+(max-min)*Distribution.RNG.nextDouble();
       	}
    	SinusoidalModel a = new SinusoidalModel(off,freq,amp);
    	return a;
    }
    public static SinusoidalModel generateRandomModel(int r,
    		double minO, double maxO,double minF, double maxF,double minA, double maxA)
    {
    	double[] freq=new double[r];
    	double[] off = new double[r];
    	double[] amp=new double[r];
    	for(int i=0;i<r;i++)
    	{
//    		freq[i]=minF+(maxF-minF)*Distribution.RNG.nextDouble();
//    		off[i]=minO+(maxO-minO)*Distribution.RNG.nextDouble();
 //   		amp[i]=0.5+minA+(maxA-minA)*Distribution.RNG.nextDouble();
    		
    		//            off[i]=min+(max-min)*Distribution.RNG.nextDouble();
    		//         amp[i]=min+(max-min)*Distribution.RNG.nextDouble();
    	}
    	SinusoidalModel a = new SinusoidalModel(off,freq,amp);
    	return a;
    }
    public static SinusoidalModel generateRandomModel(int r)
    {
    	return generateRandomModel(r,defaultMin,defaultMax);
    }
    public static SinusoidalModel perturbSinusoidalModel(SinusoidalModel base, int maxPercentDev)
    {
           int r= base.frequency.length;
           double[] freq=new double[r];
           double[] off = new double[r];
           double[] amp=new double[r];
           int percentDev;
           for(int i=0;i<r;i++)
           {
 /*              percentDev = Distribution.RNG.nextInt(maxPercentDev);
              if(Distribution.RNG.nextDouble()<0.5)
                 freq[i]=base.frequency[i]*(100.0-percentDev)/100.0;
               else
                 freq[i]=base.frequency[i]*(100.0+percentDev)/100.0;

               percentDev = Distribution.RNG.nextInt(maxPercentDev);
               if(Distribution.RNG.nextDouble()<0.5)
                  off[i]=base.offset[i]*(100.0-percentDev)/100.0;
               else
                  off[i]=base.offset[i]*(100.0+percentDev)/100.0;
                percentDev = Distribution.RNG.nextInt(maxPercentDev);
               if(Distribution.RNG.nextDouble()<0.5)
                  amp[i]=base.amplitude[i]*(100.0-percentDev)/100.0;
               else
                  amp[i]=base.amplitude[i]*(100.0+percentDev)/100.0;
*/
         }
          SinusoidalModel a = new SinusoidalModel(off,freq,amp);
          return a;
    }
    public void randomiseOffset()
    {
 //          for(int i=0;i<offset.length;i++)
 //             offset[i]=Distribution.RNG.nextDouble();
     }
    
    double[] getOffset(){ return offset;}
    double[] getFrequency(){ return frequency;}
    double[] getAmplitude(){return amplitude;}
    void setAmplitude(double[] a){ amplitude=a;}
    void setOffset(double[] o){ offset=o;}

    public String toString(){
      String str="";
     for(int i=0;i<offset.length;i++)
              str+=offset[i]+",";
      str+="\n";
      for(int i=0;i<frequency.length;i++)
              str+=frequency[i]+",";
      str+="\n";
      for(int i=0;i<amplitude.length;i++)
              str+=amplitude[i]+",";
      str+="\n";
      return str;
    }
    public static void sampleData()
    {
           OutFile of= new OutFile("randomFFTData_3Waves.csv");
           int k=2;
           int n=256;
           int l=5;
           SinusoidalModel[] models= new SinusoidalModel[k];
           double[][] data = new double[k*l][n];
           for(int i=0;i<k;i++)
           {
                models[i]= generateRandomModel(3);
                for(int j=0;j<l;j++)
                  data[i*l+j]=models[i].generateSeries(n);
          }
          for(int i=0;i<n;i++)
          {
                  for(int j=0;j<k*l;j++)
                          of.writeString(data[j][i]+",");
                  of.writeString("\n");
           }
           for(int i=0;i<k;i++)
                   of.writeLine(models[i].toString());
        }
 	static public void main(String[] args){
                System.out.println(" To do: test harness for Sinusoidal models");
            }
        }


