package net.richardsenior.java.primenumbers;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.stream.IntStream;

import javax.swing.JFrame;
import javax.swing.JPanel;

import org.apache.commons.math3.analysis.differentiation.UnivariateDifferentiableFunction;
import org.apache.commons.math3.analysis.function.Gaussian;
import org.apache.commons.math3.analysis.function.HarmonicOscillator;
import org.apache.commons.math3.analysis.polynomials.PolynomialFunction;
import org.apache.commons.math3.fitting.AbstractCurveFitter;
import org.apache.commons.math3.fitting.GaussianCurveFitter;
import org.apache.commons.math3.fitting.HarmonicCurveFitter;
import org.apache.commons.math3.fitting.PolynomialCurveFitter;
import org.apache.commons.math3.fitting.WeightedObservedPoints;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.LogAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.statistics.HistogramDataset;
import org.jfree.data.statistics.HistogramType;
import org.jfree.data.xy.TableXYDataset;
import org.jfree.data.xy.XYBarDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
/*
 * 1's = 0.7957747, 1.2566371
 * 3's = 2.387324, 3.7699113 ((2pi/5) * 3) etc.
 * 7's = 1.5915494, 2.5132742
 * 9's = 3.1830988, 5.0265484
 */
public class PrimeNumberPlotter extends JFrame {	
	private static final float TPC = new Float(0.6601618158);
	private static final float GR = new Float(1.61803398875);
	private static final float SR2 = new Float(1.4142135623);	
	private static final float SR5 = new Float(2.23606797);
	private static final float MILLS = new Float(1.3063778838);
	private static final float BRUN = new Float(1.90216058);
	private Color color = Color.white;
	private int width = 800;
	private int height = 800;
	private int max = 9000000;
	private float scale = 0.01f;
	private Point2D.Float centre;
	private float scaleFactor;
	private float numRadiansPerTurn = new Float(2 * Math.PI);
	private AbstractPrimePanel panel;
	private PrimeIterator iterator;
	//private static final float numRadiansPerTurn = new Float(GR);
	//private static final float numRadiansPerTurn = new Float(SR5);
	//private static final float numRadiansPerTurn = new Float(TPC);
	//private static final float numRadiansPerTurn = new Float(MILLS);
	//private static final float numRadiansPerTurn = new Float(BRUN);
	//private static final float numRadiansPerTurn = new Float(10);	
	private PrimeNumberPlotter() {
		super("Fame And Riches");
	    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	    setPlotSize(this.width,this.height);
	    setIterator(new PrimeIterator());
	    setPanel(new PrimeSpiralPanel());
	}	
//***************************
//*** Fluent builder methods
//***************************
	public Color getColour() {return this.color;}
	public PrimeNumberPlotter setColour(Color colour) {this.color = colour;return this;}
	public PrimeIterator getIterator() {return this.iterator;}
	public PrimeNumberPlotter setIterator(PrimeIterator iterator) {
		this.iterator=iterator;
		this.iterator.setMax(getMax());
		return this;
	}
	public float getRadiansPerTurn() {return this.numRadiansPerTurn;}
	public float getScale() {return this.scale;}
	public PrimeNumberPlotter setScale(float scale) {this.scale = scale;return this;}
	public float getScaleFactor() {return this.scaleFactor;}
	public Point2D.Float getCentre() {return this.centre;}
	public PrimeNumberPlotter setRadiansPerTurn(float rpt) {
		this.numRadiansPerTurn = rpt;
		this.scaleFactor = new Float(2 * Math.PI) / numRadiansPerTurn;
		return this;
	}
	public int getMax() {return this.max;}
	public PrimeNumberPlotter setMax(int max) {
		this.max = max;
		this.iterator.setMax(max);
		return this;
	}
	public PrimeNumberPlotter setPlotSize(int width, int height) {
		setSize(width,height);
		this.width = width;
		this.height = height;
	    this.centre = new Point2D.Float(new Float(width/2),new Float(height/2));
	    this.scaleFactor = new Float(2 * Math.PI) / numRadiansPerTurn;
	    return this;
	}
	public AbstractPrimePanel getPanel() {return this.panel;}
	public PrimeNumberPlotter setPanel(AbstractPrimePanel panel) {
		this.panel = null;
		this.panel = panel;
		this.panel.setParentPlotter(this);
		this.setContentPane(this.panel);
		return this;
	}
	public void plot() {setVisible(true);}
	public static final PrimeNumberPlotter build() {return new PrimeNumberPlotter();}
//***************************
//*** Static methods
//***************************
	public static final int getPrimeTheoryNumber(int num) {
		if (num == 1) return 1;
		double fln = new Double(num);
		double n = fln / (Math.log(fln));
		int rn = (int)Math.round(n);
		return rn;
	}
	public static final boolean isPrimeNumber(int number) {
	    return number==1 || number > 2 && IntStream.rangeClosed(2, (int) Math.sqrt(number)).noneMatch(n -> (number % n == 0));
	}	
	public static final int numPrimes(int n) {
		float num = new Float(n) / new Float(Math.log(n) - 1);
		return Math.round(num);
	}
	private static final int maxPrimeGap(int n) {
		Float f = new Float(Math.log(n) * Math.log(n));
		return Math.round(f);
	}
//*************************************************************************
//************************** Iterators ************************************
//*************************************************************************
	/**
	 * Iterates through prime numbers
	 * @author Richard Senior
	 */
	private static class PrimeIterator implements Iterator<Integer> {
		private int max = 1000000;
		private int prev = 1;
		private int curr = 0;
		private int shell = 0;
		private int gap = 0;
		private float relativeGap = 0f;
	
		public int getPrevious() {return this.prev;}
		public int getCurrent() {return this.curr;}
		public int getShell() {return this.shell;}
		public int getGap() {return this.gap;}
		public static Color getColor(int c) {
			switch (c % 5) {
			case 1 : return Color.yellow;
			case 2 : return Color.cyan;
			case 3 : return Color.magenta;
			default : return Color.white;
		}			
		}
		public Color getColor() {
			return getColor(getCurrent());
		}
		public PrimeIterator setMax(int max) {this.max = max;return this;}
		public int getMax() {return this.max;}
		public float getRelativeGap() {return this.relativeGap;}
		@Override
		public boolean hasNext() {return getCurrent() < max;}				
		protected boolean test() {return isPrimeNumber(getCurrent());}
		
		@Override
		public Integer next() {			
			for (int a=curr+1;a<max+1;a++) {				
				curr = a;
				if (test()) {
					shell ++;
					relativeGap = new Float(curr) / new Float(prev);
					gap = curr - prev;
					prev = curr;					
					return curr;
				}				
			}
			return curr;
		}		
	}
		
	/**
	 * Iterates through twin primes (or primes of any gap size)
	 * twin primes (2), Cousin primes (4), sexy primes (6)
	 * 70,000,000
	 * Can't have odd gap because one number would be even!
	 * @author Richard Senior
	 */
	private static class GappedPrimeIterator extends PrimeIterator {
		private int gap = 2;
		public void setGap(int gap) {this.gap = gap;}
		//default to twin primes
		public GappedPrimeIterator() {}
		public GappedPrimeIterator(int gap) {this.gap = gap;}
		@Override
		protected boolean test() {
			if (!super.test()) return false;
			if (isPrimeNumber(getCurrent() + gap) || isPrimeNumber(getCurrent() - gap)) return true;
			return false;
		}
	}
	
	/**
	 * Iterates through all primes between a min and max value
	 * @author Richard Senior
	 */
	private static class RangeIterator extends PrimeIterator {
		private int min = 1;
		public RangeIterator() {}
		public RangeIterator(int min, int max) {
			setMin(min);
			setMax(max);
		}
		public RangeIterator setMin(int min) {this.min=min; return this;}
		public int getMin() {return this.min;}
		@Override
		protected boolean test() {
			if (!super.test()) return false;
			if (getCurrent()>getMin() && getCurrent()<getMax()) {
				return true;
			}
			return false;
		}
	}	

	/**
	 * filters out such that only numbers which match (or don't) 
	 * K * N plusorminus x
	 * defaults to 6n+-1
	 * @author Richard Senior
	 */
	private static class KNPlusXIterator extends PrimeIterator {
		private int factor = 6;
		private int x = 1;
		public KNPlusXIterator() {}
		
		@Override
		protected boolean test() {
			if (!super.test()) return false;
			boolean p = isPrimeNumber((factor * getCurrent()) + x);
			boolean m = isPrimeNumber((factor * getCurrent()) - x);
			if (!p && !m) {
				return true;
			}
			return false;
		}
	}	
	
	private static class ModIterator extends PrimeIterator {
		/**
		 * mod 1 gets all primes ending in a 1
		 * mod 2 gets all primes ending in a 7
		 * mod 3 gets all primes ending in a 3
		 * mod 4 gets all primes ending in a 9
		 */
		private int mod = 1;
		public ModIterator() {}
		public ModIterator(int mod) {this.mod=mod;}
		@Override
		protected boolean test() {
			if (!super.test()) return false;
			if (getCurrent() % 5 == mod) return true;
			return false;
		}
	}
//*************************************************************************
//************************** Predictors! **********************************
//*************************************************************************
	/**
	 * Attempts to create polynomials which can predict prime numbers
	 * @author Richard Senior
	 */
	private static class Predictor {
		PrimeNumberPlotter parent;
		PrimeIterator i;
		PolynomialCurveFitter f; 
		WeightedObservedPoints p;
		PolynomialFunction fn;
		
		public Predictor(PrimeNumberPlotter parent) {
			this.i = new PrimeIterator();
			this.parent = parent;
		}
		protected PrimeNumberPlotter getParent() {return this.parent;}
		
		protected void analyse() {
			p = new WeightedObservedPoints();
			while (i.hasNext()) {
				Integer next = i.next();
				p.add(new Double(i.getShell()),new Double(i.getRelativeGap()));
			}
			f = PolynomialCurveFitter.create(3);
			double[] coeffs = f.fit(p.toList());
			fn = new PolynomialFunction(coeffs);
			System.out.println(f.toString());
		}
		
		public float getAccuracy() {		
			int numPredictions = 0;
			int numCorrect = 0;
			for (int a=1;a<getParent().getMax();a++) {				
				numPredictions++;
				if (isPrimeNumber(predict(a))) {
					numCorrect++;
				}
			}
			float accuracy =  ((new Float(numCorrect) / new Float(numPredictions)) * 100f);
			//System.out.println("got " + accuracy + "% correct");
			return accuracy;			
		}
		
		/* returns the percentage accuracy of predictions */
		public int predict(int n) {
			if (fn==null) analyse();
			long pv = Math.round(fn.value(n));
			return (int)pv;
		}	
	}
//*************************************************************************
//************************** Plotters *************************************
//*************************************************************************
	/** 
	 * Abstract prime number plotter
	 * @author Richard Senior
	 */
	private static abstract class AbstractPrimePanel extends JPanel {		
		private PrimeNumberPlotter parent;
		private AbstractPrimePanel() {
			super();
			setOpaque(true);
			setBackground(Color.black);
			
		}
		private AbstractPrimePanel setParentPlotter(PrimeNumberPlotter parent) {
			this.parent = parent;
			return this;
		}
		protected PrimeNumberPlotter getParentPlotter() {return this.parent;}	
		@Override
		public void paintComponent(Graphics g) {
			g.setColor(getParentPlotter().getColour());
			if (this.parent==null) throw new IllegalStateException("Panel must be part of a plotter in order to paint itself");
			super.paintComponent(g);
			PrimeIterator i = getParentPlotter().getIterator();
			while (i.hasNext()) {
				Integer next = i.next();
				plot(i.getShell(),next,g);
			}
			plot(g);
		}
		/**
		 * For radial plots, gets the radius of the shell, of the current iterator value
		 */
		protected float getShellRadius(int shell) {
			float fLength = getParentPlotter().getScale() * new Float(shell);
			return fLength;
		}
		/** utility method for plotting radial points **/
		protected void plotRadialPoint(int shell, float number, Graphics g) {
			float fRemain = number % getParentPlotter().getRadiansPerTurn();			
			float fRads = fRemain * getParentPlotter().getScaleFactor();
			float fLength = getShellRadius(shell);
		    float x = new Float((fLength * Math.cos(fRads)) + getParentPlotter().getCentre().getX());
		    float y = new Float((fLength * Math.sin(fRads)) + getParentPlotter().getCentre().getY());
		    int xRound = Math.round(x);
		    int yRound = Math.round(y);
		    g.drawRect(xRound,yRound,1,1);
		}
		
		/** use this to plot after all numbers have been calculated **/
		public abstract void plot(Graphics g);
		/** plot as each number is calculated 
		 * or place numbers in some internal structure for later
		 * plotting by the plot(g) method
		 * @param shell
		 * @param number
		 * @param g
		 */
		public abstract void plot(int shell, int number, Graphics g);
	}
	
	/**
	 * Plots prime spirals with an arbitrary number of radians per turn.
	 * @author Richard Senior
	 */
	private static class PrimeSpiralPanel extends AbstractPrimePanel {
		private int prev = 1;
		@Override
		public void plot(Graphics g) {}
		@Override
		public void plot(int shell, int number, Graphics g) {
			super.plotRadialPoint(shell,new Float(number), g);
		}		
	}
	
	/**
	 * Plots the 'relative' gaps between primes in polar coordinates
	 * That is currentprime / previous prime % some number
	 * @author Richard Senior
	 */
	private static class RadialRelativeGapsPanel extends AbstractPrimePanel {
		private int scale = Integer.MAX_VALUE;
		public RadialRelativeGapsPanel() {super();}
		public RadialRelativeGapsPanel(int scale) {
			super();
			this.scale = scale;
		}
		public void plot(int shell, int number, Graphics g) {
			PrimeIterator i = getParentPlotter().getIterator();
			super.plotRadialPoint(shell, i.getRelativeGap() * this.scale, g);
		}
		@Override
		public void plot(Graphics g) {}
	}
	
	
	public static class PredictorPlotter extends AbstractPrimePanel {
		private AbstractCurveFitter f;
		private WeightedObservedPoints p;
		private UnivariateDifferentiableFunction fn;
		private int numCorrect = 0;
		private int numPredictions = 0;
		@Override
		public void plot(Graphics g) {
			System.out.println("fitting curve..");			
			f = PolynomialCurveFitter.create(1);
			double[] coeffs = f.fit(p.toList());
			fn = new PolynomialFunction(coeffs);
			System.out.println(fn.toString());
			for (int a=1;a<getParentPlotter().getMax();a++) {
				numPredictions++;							
				double fv = fn.value(a);				
				int pp = (int)Math.round(fv + 217058);
				if (!isPrimeNumber(pp)) continue;
				numCorrect++;
				float val = new Float(fv);
				super.plotRadialPoint(a,val,g);
			}
			float accuracy =  ((new Float(numCorrect) / new Float(numPredictions)) * 100f);
			System.out.println("accuracy is " + accuracy + "%");
		}

		@Override
		public void plot(int shell, int number, Graphics g) {
			if (this.p==null) this.p = new WeightedObservedPoints();
			PrimeIterator i = getParentPlotter().getIterator();
			//this.p.add(new Double(shell),new Double(number));
			this.p.add(new Double(shell),new Double(number));
			//plot this number?
		}		
	}
	
	public static class PolynomialPlotter extends AbstractPrimePanel {
		//private PolynomialFunction two = new PolynomialFunction(new double[] {-167,1,1}); 4.86%
		private PolynomialFunction two = new PolynomialFunction(new double[] {-217072,15}); //12.27% //magenta only?
		//private PolynomialFunction two = new PolynomialFunction(new double[] {-217073,16}); //13%
		//private PolynomialFunction two = new PolynomialFunction(new double[] {-217071,14}); //15%				
		
		private int numCorrect = 0;
		private int numPredictions = 0;
		@Override
		public void plot(Graphics g) {
			float accuracy =  ((new Float(numCorrect) / new Float(numPredictions)) * 100f);
			System.out.println("accuracy is " + accuracy + "%");			
		}

		@Override
		public void plot(int shell, int number, Graphics g) {
			numPredictions++;
			double n = two.value(shell);
			int rn = (int)(Math.round(n));
			if (rn>0 && rn<1000) System.out.println(rn);
			if (! isPrimeNumber(rn)) return;
			numCorrect++;
			g.setColor(PrimeIterator.getColor(rn));
			super.plotRadialPoint(shell,new Float(rn),g);
		}
		
	}
	
	public static class LogXPlotter extends AbstractPrimePanel {
		private int numCorrect = 0;
		private int numPredictions = 0;
		@Override
		public void plot(Graphics g) {
			float accuracy =  ((new Float(numCorrect) / new Float(numPredictions)) * 100f);
			System.out.println("accuracy is " + accuracy + "%");			
		}

		@Override
		public void plot(int shell, int number, Graphics g) {
			numPredictions++;
			int rn = getPrimeTheoryNumber(number);
			if (! isPrimeNumber(rn)) return;
			numCorrect++;
			super.plotRadialPoint(shell,new Float(rn),g);
		}
		
	}
	
	/**
	 * Plots points that match (2pi / n)
	 * @author Richard Senior
	 */
	public static class PolarAnglePlotter extends AbstractPrimePanel {
		@Override
		public void plot(Graphics g) {				
		}

		@Override
		public void plot(int shell, int number, Graphics g) {
		}		
	}
	
	/*	
	private static class PrimeGapLinearPlotter extends AbstractPrimePanel {
		PolynomialCurveFitter f = PolynomialCurveFitter.create(3); 
		WeightedObservedPoints p = new WeightedObservedPoints();
		float scalex = new Float(width) / new Float(numPrimes(max));
		float scaley = new Float(height) / new Float(maxPrimeGap(max));
		
		@Override
		public void plot(Graphics g) {
			//plot the distribution curve
			double[] coeffs = f.fit(p.toList());
			PolynomialFunction f = new PolynomialFunction(coeffs);
			float yscale = new Float(f.value(max)) / new Float(height);
			for (int a=1;a<max;a++) {
				int x = Math.round(scalex * a);
				int y = Math.round(height - Math.round(yscale * f.value(a)));
				g.drawRect(x,y,1,1);
			}
			
		}

		@Override
		public void plot(int shell, int number, Graphics g) {
			int gap = getPrimeIterator().getGap();
			//add value to linear regretion calculator
			p.add(new Double(shell),new Double(gap));
			//plot the point
			g.setColor(getPrimeIterator().getColor());
			int x = Math.round(scalex * shell);
			int y = height - Math.round(scaley * gap);
		    g.drawRect(x,y,1,1);	
		}	
	}
	*/
	public static final void main(final String[] args) throws Exception {
		/*
		PolynomialFunction f = new PolynomialFunction(new double[] {-217072.84646757634,15.12040824165769});
		double d = f.value(5);
		int p = (int)Math.round(d + 217059d);
		System.out.println("got : " + p);
		if (isPrimeNumber(p)) {
			System.out.println("direct hit");
		} else if (isPrimeNumber(p+1)) {
			System.out.println("plus 1 is a hit");
		} else if (isPrimeNumber(p-1 )) {
			System.out.println("minus 1 is a hit");
		}
		if (1==1) return;
		*/		
		PrimeNumberPlotter.build().
			setMax(90000000).
			setPlotSize(800,800).
			setScale(0.001f).
			setRadiansPerTurn(new Float(2 * Math.PI)).
			setIterator(new PrimeIterator()).
			//setIterator(new ModIterator(1)).
			//setIterator(new GappedPrimeIterator(2)).
			//setPanel(new PrimeSpiralPanel()).
			//setPanel(new RadialRelativeGapsPanel()).
			//setPanel(new LogXPlotter()).
			//setPanel(new PredictorPlotter()).
			setPanel(new PolynomialPlotter()).
			plot();
	}
}