import processing.core.*; 
import processing.data.*; 
import processing.event.*; 
import processing.opengl.*; 

import moonlander.library.*; 
import ddf.minim.*; 

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

public class Graffathon2018 extends PApplet {

// Trefoil, by Andres Colubri
// A parametric surface is textured procedurally
// by drawing on an offscreen PGraphics surface.




ArrayList<PShape> buildings;
ParticleSystem ps;
//PShader lightShader;

float speed = 0.5f;

Moonlander moonlander;

public void setup() {
  
  frameRate(58);
  
  fftInit();
  
  PImage part = createImage(5, 5, RGB); 
  part.loadPixels();
  for(int i = 0; i < part.pixels.length; i++)
    part.pixels[i] = color(128);

  part.updatePixels();

  ps = new ParticleSystem(100, new PVector(0,0,0), part);

  textureMode(IMAGE);
  ((PGraphicsOpenGL)g).textureSampling(2);
  
  noiseSeed(123561245);
  
  strokeWeight(1);
  stroke(100);
  //windowShader = loadShader("texfrag.glsl", "texvert.glsl");
  

  buildings = createCity(10, 10);
  //addTexture(buildings);
  
  moonlander = Moonlander.initWithSoundtrack(this, "UNITY - Eternal Youth.mp3", 145, 4);
  //moonlander = new Moonlander(this, new TimeController(4));
  moonlander.start();
}

float camx = 0, camy = 0, camz = 0;
float treshold;

public void draw() {
  moonlander.update();
  
  treshold = (float)moonlander.getValue("treshold");
  
  int roadCol = moonlander.getIntValue("roadCol");
  int FFT = moonlander.getIntValue("doFFT");
  
  camx = (float)moonlander.getValue("camX");
  camy = (float)moonlander.getValue("camY");
  camz = (float)moonlander.getValue("camZ");
  
  
  float lightx = (float)moonlander.getValue("lightX");
  float lighty = (float)moonlander.getValue("lightY");
  float lightz = (float)moonlander.getValue("lightZ");
  float lightSize = (float)moonlander.getValue("lightSize");
  
  boolean makeSun = moonlander.getIntValue("makeSun") == 1;
  
  float windowBrightness = (float)moonlander.getValue("windowBrightness");
  
  float bg = (float)moonlander.getValue("background");
  float buildingColor = bg;
  
  background(lerp(0, 79, bg), lerp(0, 108, bg), lerp(0, 155, bg));
  
  ps.setOrigin(new PVector(lightx, lighty, lightz));
  for(int i = 0; i < moonlander.getIntValue("particles"); i++){
    ps.addParticle();
  }
  ps.run();
  
  //180, 40, 30
  if(FFT > 0 || frameCount % 6 == 1)
    updateTextures(buildings, FFT, moonlander.getCurrentTime(), roadCol, windowBrightness, buildingColor);

  // Set perspective and cam position
  // cam position, scene center position, up vector
  camera(camx, camy, camz, lightx, lighty, lightz, 0, 0, -1);
  // Fov, aspect ratio, near, far
  perspective(PI/3, width/height, 1, 8000);
  
  //translate(-frameCount, 0, 0);
  //rotateZ(-frameCount * PI / 500);

  lights();
  ambientLight(150, 150, 150);
  specular(150);
  drawLight(lightx + 2 * noise(frameCount * 0.1f + 332),
            lighty + 2 * noise(frameCount * 0.1f + 676),
            lightz + 2 * noise(frameCount * 0.1f - 257), lightSize);
  if(makeSun)
    drawLight(-2000, 2000, 1000, 40);
  
  pushMatrix();
  //shader(windowShader);
  for(PShape b: buildings)
    shape(b);
  popMatrix();
}
JSONArray fftData;
JSONArray times;

public void fftInit() {
  fftData = loadJSONArray("fft");
  times = loadJSONArray("times");
}

public float[] fftGet(float t) {
  int idx = -1;
  for(int i = 0; i < 3366; i++) {
     if(times.getFloat(i) > t) {
       idx = i;
       break;
     }
  }
  JSONArray arr = fftData.getJSONArray(idx);
  float[] res = new float[20];
  for(int i = 0; i < 20; i++) {
     res[i] = arr.getFloat(i);
  }
  return res;
}



public void drawLight(float x, float y, float z, float size){
  
  pushMatrix();
  //emissive(255, 255, 128);
  fill(255, 235, 100);
  
  //filter( BLUR, 6 );
  
  noStroke();
  translate(x, y, z);
  sphere(size);
  //filter( BLUR, 0 );
  popMatrix();
}

int meshScale = 100;
int textScale = 64;
int maxh = 4;

int yellowR = 235;
int yellowG = 215;
int yellowB = 40;

public void updateTextures(ArrayList<PShape> arr, int FFT, double t, int roadColor,
                    float windowBrightness, float buildingColor) {
  if(FFT > 0){
    float[] fft = fftGet((float)t);
    PImage tex[] = new PImage[20];
    for(int i = 0; i < 20; i++)
      tex[i] = FFT == 1 ? makeTextureFFT((int)(fft[i] * 128))
                     : makeTexture(windowBrightness, buildingColor, fft[i]);
                     
    for(int i = 0; i < arr.size() - 1; i++)
      arr.get(i).setTexture(tex[i / 20]);
                     
  }else{
    PImage tex = makeTexture(windowBrightness, buildingColor, 1);
    for(int i = 0; i < arr.size() - 1; i++)
      arr.get(i).setTexture(tex);
  }
  arr.get(arr.size() - 1).setTexture(makeTextureRoad(roadColor));
}
public ArrayList<PShape> createCity(int ny, int nx) {
  
  ArrayList<PShape> objs = new ArrayList();
  //PShape objs[] = new PShape[(2*ny + 1) * (2*nx + 1) + 1];
  
  PImage tex = makeTexture(0, 0, 1);
  
  PVector back = new PVector(0, -1, 0);
  PVector front = new PVector(0, 1, 0);
  PVector left = new PVector(1, 0, 0);
  PVector right = new PVector(-1, 0, 0);
  PVector up = new PVector(0, 0, 1);
  PVector down = new PVector(0, 0, -1);
  
  // The building size
  float bsizeX = 0.5f;
  float bsizeY = 0.5f;
  
  for (int j = -nx; j < nx; j++) {
    for (int i = -ny; i < ny; i++) {
      PShape obj = createShape();
      obj.beginShape(QUADS);
      obj.texture(tex);
      
      int buildingResolution = 128;
      float h = (int)(buildingResolution * noise(i + camx * 0.0f, j + camy * 0.0f));
      h /= buildingResolution;
      h *= maxh;
      
      PVector p000 = new PVector(i, j, 0);
      PVector p100 = new PVector(i + bsizeX, j, 0);
      PVector p200 = new PVector(i + 1, j, 0);      
      PVector p010 = new PVector(i, j + bsizeY, 0);
      PVector p110 = new PVector(i + bsizeX, j + bsizeY, 0);
      PVector p210 = new PVector(i + 1, j + bsizeY, 0);
      PVector p020 = new PVector(i, j + 1, 0);
      PVector p120 = new PVector(i + bsizeX, j + 1, 0);
      PVector p220 = new PVector(i + 1, j + 1, 0);
      
      // Roof
      PVector p001 = new PVector(i, j, h);
      PVector p101 = new PVector(i + bsizeX, j, h);
      PVector p011 = new PVector(i, j + bsizeY, h);
      PVector p111 = new PVector(i + bsizeX, j + bsizeY, h);

      // Walls: front, left, back, right
        makeQuad(obj, p000, p001, p101, p100, back, h, true, true);
        makeQuad(obj, p010, p011, p001, p000, left, h, true, false);
        makeQuad(obj, p110, p111, p011, p010, front, h, true, true);
        makeQuad(obj, p100, p101, p111, p110, right, h, true, false);
        
      // Roof
        makeQuad(obj, p001, p011, p111, p101, up, h, false, false);
        
      // Street
        //makeQuad(obj, p100, p120, p220, p200, up, h, false);
        //makeQuad(obj, p010, p020, p120, p110, up, h, false);
          
      obj.endShape();
      objs.add(obj);
    }
  }
  
  PShape roads = createShape();
  roads.beginShape(QUADS);
  makeQuad(roads, new PVector(-nx * meshScale, -ny * meshScale, 0),
                new PVector(-nx * meshScale, ny * meshScale, 0),
                new PVector(nx * meshScale, ny * meshScale, 0),
                new PVector(nx * meshScale, -ny * meshScale, 0),
                up, 1, false, false);
  roads.endShape();
  roads.setTexture(makeTextureRoad(0));
  objs.add(roads);
  return objs;
}

public void makeQuad(PShape obj, PVector vec0, PVector vec1, PVector vec2, PVector vec3,
        PVector normal, float h, boolean windows, boolean transpose){
      int texLeft, texRight, texBottom, texTop;
      if(windows){
        texLeft = (int)(96 * noise(vec0.x, vec0.y));
        texRight = texLeft + 32;
        texBottom = 0;
        texTop = (int)(textScale * h / maxh);
      }else{
        texLeft = 0;
        texRight = 1;
        texBottom = 127;
        texTop = 128;
      }
      
      setNormal(obj, normal);
      setVertex(obj, vec0, texLeft, texBottom, transpose);
      setNormal(obj, normal);
      setVertex(obj, vec1, texLeft, texTop, transpose);
      setNormal(obj, normal);
      setVertex(obj, vec2, texRight, texTop, transpose);
      setNormal(obj, normal);
      setVertex(obj, vec3, texRight, texBottom, transpose);
}

public void setVertex(PShape obj, PVector vec, float xtex, float ytex, boolean transpose){
  if(transpose)
    obj.vertex(meshScale * vec.x, meshScale * vec.y, meshScale * vec.z, ytex, xtex);
  else
    obj.vertex(meshScale * vec.x, meshScale * vec.y, meshScale * vec.z, xtex, ytex);
}
public void setNormal(PShape obj, PVector vec){
  obj.normal(vec.x, vec.y, vec.z);
}


public PImage makeTexture(float windowBrightness, float buildingColor, float fft){
  
  int imgScale = 128;
  PImage img = createImage(imgScale, imgScale, ARGB);
  for(int i = 0; i < imgScale; i++) {
    for(int j = 0; j < imgScale; j++) {
      
      // walls
      int r = (int)(map(j, 0, imgScale, 60, 0) * buildingColor);
      int g = (int)(map(j, 0, imgScale, 50, 20) * buildingColor);
      int b = (int)(map(j, 0, imgScale, 30, 30) * buildingColor);
      int a = 255;
      img.pixels[i + j * imgScale] = color(r, g, b, a); 
      
      if(i % 2 == 1 && j % 2 == 1){
        float noise = noise(i * 2 + camx * 0.002f, j * 2 + camy * 0.002f);
        noise = max(0, min(noise, 1));
        if(noise < treshold)
          noise = 0;
        noise += windowBrightness;
        noise *= fft;
        img.pixels[i + j * imgScale] = color(lerp(r, yellowR, noise),
                                            lerp(g, yellowG, noise), 
                                            lerp(b, yellowB, noise)); 
      }
    }
  }
/*
  // roads and rooftops
  for(int i = half; i < imgScale; i++) {
    for(int j = 0; j < imgScale; j++) {
      img.pixels[i + j * imgScale] = color(5, 5, 5, 255); 
    }
  }
  */
  return img;
}

public PImage makeTextureFFT(int h){
  
  int imgScale = 128;
  PImage img = createImage(imgScale, imgScale, ARGB);
  for(int i = 0; i < imgScale; i++) {
    for(int j = 0; j < imgScale; j++) {
      // windows
      if(j < h)
        img.pixels[i + j * imgScale] = color(yellowR, yellowG, yellowB); 
      else
        img.pixels[i + j * imgScale] = color(0, 0, 0); 
    }
  }
/*
  // roads and rooftops
  for(int i = half; i < imgScale; i++) {
    for(int j = 0; j < imgScale; j++) {
      img.pixels[i + j * imgScale] = color(5, 5, 5, 255); 
    }
  }
  */
  return img;
}
public PImage makeTextureRoad(int roadColor){
  
  PImage img = createImage(1, 1, ARGB);
  img.pixels[0] = color(roadColor);
  return img;
}
class ParticleSystem {

  ArrayList<Particle> particles;    // An arraylist for all the particles
  PVector origin;                   // An origin point for where particles are birthed
  PImage img;

  ParticleSystem(int num, PVector v, PImage img_) {
    particles = new ArrayList<Particle>();              // Initialize the arraylist
    origin = v.copy();                                   // Store the origin point
    img = img_;
    for (int i = 0; i < num; i++) {
      particles.add(new Particle(origin, img));         // Add "num" amount of particles to the arraylist
    }
  }
  
  public void setOrigin(PVector v) {
    origin = v.copy(); 
  }

  public void run() {
    for (int i = particles.size()-1; i >= 0; i--) {
      Particle p = particles.get(i);
      p.run();
      if (p.isDead()) {
        particles.remove(i);
      }
    }
  }

  // Method to add a force vector to all particles currently in the system
  public void applyForce(PVector dir) {
    // Enhanced loop!!!
    for (Particle p : particles) {
      p.applyForce(dir);
    }
  }  

  public void addParticle() {
    particles.add(new Particle(origin, img));
  }
}



// A simple Particle class, renders the particle as an image

class Particle {
  PVector loc;
  PVector vel;
  PVector acc;
  float lifespan;
  PImage img;

  Particle(PVector l, PImage img_) {
    acc = new PVector(0, 0, 0);
    float vx = randomGaussian()*0.15f;
    float vz = randomGaussian()*0.15f + 0.1f;
    float vy = randomGaussian()*0.15f;
    vel = new PVector(vx, vy, vz);
    loc = l.copy();
    lifespan = 255.0f;
    img = img_;
  }

  public void run() {
    update();
    render();
  }

  // Method to apply a force vector to the Particle object
  // Note we are ignoring "mass" here
  public void applyForce(PVector f) {
    acc.add(f);
  }  

  // Method to update position
  public void update() {
    vel.add(acc);
    loc.add(vel);
    lifespan -= 2.5f;
    acc.mult(0); // clear Acceleration
  }

  // Method to display
  public void render() {
    fill(255, 255, 200, lifespan);
    translate(loc.x, loc.y, loc.z);
    sphere(1);
    translate(-loc.x, -loc.y, -loc.z);
    // Drawing a circle instead
    // fill(255,lifespan);
    // noStroke();
    // ellipse(loc.x,loc.y,img.width,img.height);
  }

  // Is the particle still useful?
  public boolean isDead() {
    if (lifespan <= 0.0f) {
      return true;
    } else {
      return false;
    }
  }
}
  public void settings() {  size(1920, 1080, P3D);  smooth(8); }
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "--present", "--window-color=#666666", "--hide-stop", "Graffathon2018" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
