import sk.uniba.fmph.pocprak.simplegraphics.*;
import java.awt.*;
import java.awt.event.*;

/**
 * Trieda reprezentujuca krivku v rovine x,y
 * Krivka je reprezentovana polom (array) bodov typu point
 * body su spravidla volene tesne jeden vedla druheho, aby uz nebolo treba
 * krivku medzi nimi rafinovane interpolovat. Ak je primarne bodov malo, trieda
 * poskytuje metodu makeDense, ktora pomocou inteligentnej interpolacie
 * doplni body do dostatocneho poctu
 */
public class Path {
  /**
   * V poli (array) point su ulozene body krivky
   */
  public point p[]=new point[0];
  /**
   * Pocet bodov krivky, ktorymi je reprezentovana
   */
  public int n = 0;


  private int ntmp = 0;
  private point tmp[] = new point[100];
  private boolean active = false;
  private GrGraphics gr;

  /**
   * Prida k (hoci aj prazdnej) krivke jeden bod na koniec
   * @param p point ktory bude pridany na koniec pola p[], reprezentujuceho krivku
   */
  public void addPoint(point p){
    if(n==0){
      n++; this.p=new point[n];this.p[0]=p; return;
    }
    tmp=this.p;
    this.p=new point[n+1];
    for(int i=0;i<n;i++) this.p[i]=tmp[i];
    this.p[n]=p;
    n++;
  }
  /**
   * Metoda, ktoru treba vyvolat, ked chcem body reprezentujuce krivku
   * doplnit interaktivne v okne danom grafikou g.
   * Krivka moze byt bud prazdna, alebo moze mat definovany pociatocny bod a
   * mysou pridam ostatne, alebo moze mat definovany pociatocny a koncovy bod
   * a mysou vyplnim medzeru medzi nimi.
   * Technika je vzdy rovnaka, prerusi sa vykonavanie programu,
   * enabluje sa mysaci adapter, ktory caka na kliknutie
   * lavym mysacim tlacidlom. Po kliknuti vykresli na obrazku pridany bod a zaradi ho
   * do vytvaranej krivky. Pri poslednom bode, ktory chcem pridat je treba urobit dvojklik.
   * To je signalom, ze ide o posledny bod, disabluje sa mysaci adapter a riadenie sa vrati na miesto,
   * odkial bola tato metoda vyvolana
   * @param g GrGraphics grafika okna, na ktorom sa maju pridavat body
   */

  public void addMousePoints(GrGraphics g){
    gr=g;
    tmp = new point[100];
    gr.display.addMouseListener(mouse);
    setActive(true);
    while (isActive()) {
      try {
        Thread.sleep(100);
      }
      catch (InterruptedException ex) {throw new Error(ex);
      }
    }
    gr.display.removeMouseListener(mouse);
  }

  /**
   * Zrusi vsetky body krivky, ak nejake ma, privedie do stavu prazdnej krivky
   */
  public void clear(){
    p=new point[0];
    n=0;
  }
  /**
   * Nakresli vsetky body krivky, volajuc jednotlive body, aby sa same nakreslili
   * @param g GrGraphics grafika okna, kam sa ma kreslit
   */
  public void draw(GrGraphics g){
    if(n<1) return;
    for(int i=0;i<n;i++){
      p[i].draw(g);
    }
  }
  /**
   * Nakresli vsetky body krivky, volajuc jednotlive body, aby sa same nakreslili
   * @param g GrGraphics grafika okna, kam sa ma kreslit
   * @param size int velkost bodov v pixeloch
   */

  public void draw(GrGraphics g, int size){
    if(n<1) return;
    for(int i=0;i<n;i++){
      p[i].draw(g,size);
    }
  }

  /**
   * Zahusti krivku. Teda do "medzier" medzi uz existujucimi bodmi prida pomocou
   * inteligentnej hladkej interpolacie dalsie body tak, aby celkovy pocet
   * bodov po doplneni bol priblizne ntotal. Po zahusteni celkovy pocet bodov krivky
   * nemusi byt presne rovny ziadanemu ntotal, metoda interpolacie dovoli
   * len priblizne dodrzat pozadovany pocet.
   * @param ntotal int celkovy zelany pocet bodov
   */
  public void makeDense(int ntotal){
    if(n<2) return;
    int ninsert = ntotal/(n-1)-1;
    if (ninsert < 1) return;
    int nfinal = (ninsert+1)*(n-1)+1;
    ntmp=0;
    tmp = new point[nfinal];
    if(n==2) {insertLinear(0,ninsert); endPath(1);n=ntmp;p=tmp;return;}
    if(n==3) {insertLeft(0,ninsert); insertRight(1,ninsert); endPath(2);n=ntmp;p=tmp;return;}
    insertLeft(0,ninsert);
    for (int i=1;i<(n-2);i++) insertMiddle(i,ninsert);
    insertRight(n-2,ninsert);
    endPath(n-1);
    n=ntmp;
    p=tmp;

  }
  /**
   * Sluzobna metoda, ktore by spravne mala byt deklarovana ako private
   * Je public, aby bolo mozne hackovanie pri dedeni
   * @param displayx double
   * @param displayy double
   */
  public void Clicpoint(double displayx, double displayy){
    double x = gr.userX(displayx);
    double y = gr.userY(displayy);
    //System.out.println(x + "  " +y);
    tmp[ntmp] = new point(x,y);
    tmp[ntmp].draw(gr,2);
    ntmp++;
    gr.repaint();
  }

  private synchronized boolean isActive(){
    return active;
  }

  private synchronized void setActive(boolean active){
    this.active = active;
  }

  /**
   * Sluzobna metoda, ktore by spravne mala byt deklarovana ako private
   * Je public, aby bolo mozne hackovanie pri dedeni
   */
  public void DoubleClicked(){
    setActive(false);
    if (n+ntmp<2) return;
    if(n==0){n=ntmp; p=tmp; return;}
    if(n>2) throw new Error("nvalid initial path");
    if(n==2){
      tmp[ntmp] = p[1];
      //ntmp++;
    }
    point ptmp = p[0];
    n = n + ntmp;
    p=new point[n];
    p[0]=ptmp;
    for(int i=1;i<n;i++){
      p[i]=tmp[i-1];
    }
  }

  /**
   * Sluzobna metoda, ktore by spravne mala byt deklarovana ako private
   * Je public, aby bolo mozne hackovanie pri dedeni
   */
  public MouseListener mouse = new MouseAdapter(){
    public long lastclicked =0;
    public void mouseClicked(MouseEvent e) {
         if (!isActive()) return;
         if (e.getButton()!=e.BUTTON1) return;
         long time = System.currentTimeMillis();
         if((time - lastclicked) <300) {
           DoubleClicked();
           return;
         }
         lastclicked = time;
         Clicpoint(e.getX(),e.getY());
    }
  };


  private void insertLinear(int A,int ninsert) {
  double ax=p[A+1].x-p[A].x;
  double ay=p[A+1].y-p[A].y;
  for(int i = 0; i<(ninsert+1); i++){
    double t =(1./(ninsert+1))*i;
    double x = p[A].x+t*ax;
    double y = p[A].y+t*ay;
    tmp[ntmp]=new point(x,y); ntmp++;
  }
}

  private void insertLeft(int A,int ninsert) {
    twovector a = alpha(A,A+1,A+2);
    twovector b = beta(A,A+1,A+2);
    for(int i = 0; i<(ninsert+1); i++){
      double t =(1./(ninsert+1))*i;
      double x = p[A].x+t*a.x+t*t*b.x;
      double y = p[A].y+t*a.y+t*t*b.y;
      tmp[ntmp]=new point(x,y); ntmp++;
    }

  }

  private void insertRight(int B,int ninsert) {
    int A = B-1;
    twovector a = alpha(A,A+1,A+2);
    twovector b = beta(A,A+1,A+2);
    for(int i = 0; i<(ninsert+1); i++){
      double t =(1./(ninsert+1))*i+1;
      double x = p[A].x+t*a.x+t*t*b.x;
      double y = p[A].y+t*a.y+t*t*b.y;
      tmp[ntmp]=new point(x,y); ntmp++;
    }

  }

  private void insertMiddle(int B,int ninsert) {
    int A = B-1;
    twovector a1 = alpha(A,A+1,A+2);
    twovector b1 = beta(A,A+1,A+2);
    twovector a2 = alpha(B,B+1,B+2);
    twovector b2 = beta(B,B+1,B+2);
    for(int i = 0; i<(ninsert+1); i++){
      double t = (1. / (ninsert + 1)) * i;
      double t1 = t + 1.;
      double x1 = p[A].x + t1 * a1.x + t1 * t1 * b1.x;
      double y1 = p[A].y + t1 * a1.y + t1 * t1 * b1.y;
      double x2 = p[B].x + t * a2.x + t * t * b2.x;
      double y2 = p[B].y + t * a2.y + t * t * b2.y;
      double x = x1*(1.-t)+x2*t;
      double y = y1*(1.-t)+y2*t;
      tmp[ntmp]=new point(x,y); ntmp++;
    }


  }
  private void endPath(int C){
    tmp[ntmp]=p[C];
    ntmp++;
  }
  private twovector alpha(int A,int B,int C){
    double x = 2.*(p[B].x-p[A].x)-0.5*(p[C].x-p[A].x);
    double y = 2.*(p[B].y-p[A].y)-0.5*(p[C].y-p[A].y);
    return new twovector(x,y);
  }

  private twovector beta(int A,int B,int C){
    double x = -(p[B].x-p[A].x)+0.5*(p[C].x-p[A].x);
    double y = -(p[B].y-p[A].y)+0.5*(p[C].y-p[A].y);
    return new twovector(x,y);
  }

  private class twovector{
    double x;
    double y;
    public twovector(double x, double y){
      this.x = x;
      this.y=y;
    }
  }

  /**
   * trieda Path nie je urcena na samostatne pouzitie, metoda main existuje len kvoli pripadnemu testovaniu
   */
  public static void main(String[] args) throws InterruptedException {
    Path path = new Path();
    //path.addPoint(new point(0.0,0.0));
    //path.addPoint(new point(0.4,0.0));
    Kreslic krxy=new Kreslic(new GrAxisX(-0.5, 0.5, 0),
                               new GrAxisY( -0.5, 0.5, 0));
    path.gr =krxy.gr;
    krxy.nakresliOsi();
    path.draw(krxy.gr,2);
    krxy.gr.repaint();
    path.addMousePoints(krxy.gr);
    path.makeDense(100);
    path.draw(krxy.gr);
    krxy.gr.repaint();

  }
}
