
import java.io.File;
import java.util.HashSet;
import processing.event.MouseEvent;
import drop.*;

boolean painting = false;

float brushRadius = 20;
float sourceZoom = 1.0;
float brushOpacity = 1.0;

PicDropper picDropper = new PicDropper();

PGraphics finalPic;

ArrayList<String> picsUrls = new ArrayList<String>();
int currentPicIndex = 0;
String statusMessage = "Drag and drop a folder with pics onto the window.";
boolean hasPicsLoaded = false;
SDrop dropHandler;

void setup() {
  size(1000, 1000);
  finalPic = createGraphics(width, height);
  finalPic.beginDraw();
  finalPic.background(255);
  finalPic.endDraw();
  background(255);
  dropHandler = new SDrop(this);
}

void draw() {
  background(255);
   
  picDropper.update();
  
  if (painting && picDropper.im != null) {
    paintFromPic();
  }
  
  image(finalPic, 0, 0);
  drawBrushOverlay();
  drawUi();
}

void paintFromPic() {
  int r = int(brushRadius);
  int r2 = r * r;
  float zoom = max(0.1, sourceZoom);
  int alphaMix = int(brushOpacity * 255);
  if (alphaMix <= 0) return;
  boolean opaqueBrush = alphaMix >= 255;
  
  finalPic.beginDraw();
  finalPic.loadPixels();
  picDropper.im.loadPixels();
  
  for (int dx = -r; dx <= r; dx++) {
    for (int dy = -r; dy <= r; dy++) {
      if (dx * dx + dy * dy <= r2) {
        int sx = int(picDropper.pos.x + dx / zoom);
        int sy = int(picDropper.pos.y + dy / zoom);
        int dxWin = mouseX + dx;
        int dyWin = mouseY + dy;
        
        if (sx >= 0 && sx < picDropper.im.width &&
            sy >= 0 && sy < picDropper.im.height &&
            dxWin >= 0 && dxWin < finalPic.width &&
            dyWin >= 0 && dyWin < finalPic.height) {
          int dstIdx = dyWin * finalPic.width + dxWin;
          int srcCol = picDropper.im.pixels[sy * picDropper.im.width + sx];
          if (opaqueBrush) {
            finalPic.pixels[dstIdx] = srcCol;
          } else {
            int dstCol = finalPic.pixels[dstIdx];
            int sr = (srcCol >> 16) & 0xFF;
            int sg = (srcCol >> 8) & 0xFF;
            int sb = srcCol & 0xFF;

            int dr = (dstCol >> 16) & 0xFF;
            int dg = (dstCol >> 8) & 0xFF;
            int db = dstCol & 0xFF;

            dr += ((sr - dr) * alphaMix) >> 8;
            dg += ((sg - dg) * alphaMix) >> 8;
            db += ((sb - db) * alphaMix) >> 8;

            finalPic.pixels[dstIdx] = (0xFF << 24) | (dr << 16) | (dg << 8) | db;
          }
        }
      }
    }
  }
  
  finalPic.updatePixels();
  finalPic.endDraw();
}

void drawBrushOverlay() {
  int alphaMix = int(brushOpacity * 120);
  if (picDropper.im != null && alphaMix > 0) {
    int r = int(brushRadius);
    int r2 = r * r;
    float zoom = max(0.1, sourceZoom);
    loadPixels();
    picDropper.im.loadPixels();
    int winW = width;
    int winH = height;
    int srcW = picDropper.im.width;
    int srcH = picDropper.im.height;

    for (int dx = -r; dx <= r; dx++) {
      int px = mouseX + dx;
      if (px < 0 || px >= winW) continue;
      int dx2 = dx * dx;
      for (int dy = -r; dy <= r; dy++) {
        if (dx2 + dy * dy > r2) continue;
        int py = mouseY + dy;
        if (py < 0 || py >= winH) continue;

        int sx = int(picDropper.pos.x + dx / zoom);
        int sy = int(picDropper.pos.y + dy / zoom);
        if (sx < 0 || sx >= srcW || sy < 0 || sy >= srcH) continue;

        int dstIdx = py * winW + px;
        int srcIdx = sy * srcW + sx;
        int dst = pixels[dstIdx];
        int src = picDropper.im.pixels[srcIdx];

        int sr = (src >> 16) & 0xFF;
        int sg = (src >> 8) & 0xFF;
        int sb = src & 0xFF;

        int dr = (dst >> 16) & 0xFF;
        int dg = (dst >> 8) & 0xFF;
        int db = dst & 0xFF;

        dr += ((sr - dr) * alphaMix) >> 8;
        dg += ((sg - dg) * alphaMix) >> 8;
        db += ((sb - db) * alphaMix) >> 8;

        pixels[dstIdx] = (0xFF << 24) | (dr << 16) | (dg << 8) | db;
      }
    }
    updatePixels();
  }
  noFill();
  stroke(0, 100);
  strokeWeight(1);
  ellipse(mouseX, mouseY, brushRadius * 2, brushRadius * 2);
}

void drawUi() {
  fill(0, 180);
  noStroke();
  rect(10, 10, 180, 150);
  fill(255);
  textSize(12);
  textAlign(LEFT, TOP);
  String info = "Brush: " + nf(brushRadius, 0, 1) + " px\n";
  info += "Zoom: " + nf(sourceZoom, 0, 2) + "x\n";
  info += "Opacity: " + nf(brushOpacity * 100, 0, 0) + "%\n";
  info += "Images: " + picsUrls.size() + "\n";
  info += "left click: paint\n";
  info += "right click: change pic\n";  
  info += "mouse wheel: brush size\n";
  info += "+/-: brush zoom\n";
  info += "o/p: brush transparency\n";
  info += "tab: save\n";
  text(info, 16, 16);
  if (!hasPicsLoaded) {
    fill(0, 200);
    rect(width/2 - 210, height/2 - 50, 420, 100);
    fill(255);
    textAlign(CENTER, CENTER);
    text("Drag and drop a folder with pictures onto this window", width/2, height/2);
  }
  fill(0, 200);
  textAlign(LEFT, BOTTOM);
  text(statusMessage, 16, height - 16);
}

class PicDropper {
  PImage im;
  PVector pos;
  PVector direction;
  
  PicDropper() {
    pos = new PVector(width/2, height/2);
    direction = PVector.random2D();
  }
  
  void setImage(PImage img) {
    im = img;
    if (im != null) {
      pos = new PVector(im.width/2, im.height/2);
      direction = PVector.random2D();
      direction.mult(2);
      clampToBounds();
    }
  }
  
  void update() {
    if (im == null) return;
    
    // random walk on direction
    direction.x += random(-0.3, 0.3);
    direction.y += random(-0.3, 0.3);
    direction.limit(3);
    
    // move position
    pos.add(direction);
    
    clampToBounds();
  }

  void clampToBounds() {
    if (im == null) return;
    float margin = samplingMargin();
    float minX = margin;
    float maxX = im.width - 1 - margin;
    float minY = margin;
    float maxY = im.height - 1 - margin;

    if (pos.x < minX) {
      pos.x = minX;
      direction.x = abs(direction.x);
    }
    if (pos.x > maxX) {
      pos.x = maxX;
      direction.x = -abs(direction.x);
    }
    if (pos.y < minY) {
      pos.y = minY;
      direction.y = abs(direction.y);
    }
    if (pos.y > maxY) {
      pos.y = maxY;
      direction.y = -abs(direction.y);
    }
  }
}

void dragAndDropPic(String folderPath) {
  loadPicsFromFolder(new File(folderPath));
}

void loadPicsFromFolder(File folder) {
  if (folder == null) return;
  picsUrls.clear();
  HashSet<String> unique = new HashSet<String>();
  
  if (!folder.exists() || !folder.isDirectory()) {
    println("Not a valid folder: " + folder.getAbsolutePath());
    statusMessage = "Not a valid folder: " + folder.getAbsolutePath();
    hasPicsLoaded = false;
    return;
  }
  
  collectImages(folder, unique);
  picsUrls.addAll(unique);
  
  if (picsUrls.size() > 0) {
    currentPicIndex = 0;
    loadCurrentPic();
    hasPicsLoaded = true;
    statusMessage = "Loaded " + picsUrls.size() + " images from " + folder.getAbsolutePath();
  } else {
    println("No image files found in folder: " + folder.getAbsolutePath());
    statusMessage = "No image files found in " + folder.getAbsolutePath();
    hasPicsLoaded = false;
  }
}

void collectImages(File target, HashSet<String> unique) {
  if (target == null) return;
  if (target.isDirectory()) {
    File[] files = target.listFiles();
    if (files == null) return;
    for (File f : files) {
      collectImages(f, unique);
    }
  } else {
    String name = target.getName().toLowerCase();
    if (name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".gif")) {
      unique.add(target.getAbsolutePath());
    }
  }
}

void loadCurrentPic() {
  if (picsUrls.size() == 0) return;
  currentPicIndex = (currentPicIndex % picsUrls.size() + picsUrls.size()) % picsUrls.size();
  String path = picsUrls.get(currentPicIndex);
  println("Loading image: " + path);
  PImage img = loadImage(path);
  if (img != null) {
    picDropper.setImage(img);
    clampBrushToImage();
    picDropper.clampToBounds();
  } else {
    println("Could not load image: " + path);
    statusMessage = "Could not load image: " + path;
  }
}

void mousePressed() {
  if (mouseButton==RIGHT) {
    if (picsUrls.size() > 0) {
      currentPicIndex = (currentPicIndex + 1) % picsUrls.size();
      loadCurrentPic();
    }
  }
  if (mouseButton==LEFT) {
    painting = true;
  }
}

void mouseReleased() {
  if (mouseButton==LEFT) {
    painting = false;
  }
}

void keyPressed() {
  if (key == '+' || key == '=') {
    sourceZoom = min(sourceZoom + 0.1, 10);
    clampBrushToImage();
    picDropper.clampToBounds();
  }
  if (key == '-' || key == '_') {
    sourceZoom = max(sourceZoom - 0.1, 0.1);
    clampBrushToImage();
    picDropper.clampToBounds();
  }
  if (key == 'o' || key == 'O') {
    brushOpacity = max(0, brushOpacity - 0.05);
  }
  if (key == 'p' || key == 'P') {
    brushOpacity = min(1, brushOpacity + 0.05);
  }
  if (key == TAB) {
    String filename = "export_" + year() +
                      nf(month(), 2) +
                      nf(day(), 2) + "_" +
                      nf(hour(), 2) +
                      nf(minute(), 2) +
                      nf(second(), 2) + ".png";
    String path = sketchPath(filename);
    finalPic.save(path);
    statusMessage = "Exported to " + filename;
    println(statusMessage + " (" + path + ")");
  }
  println("brush radius : " + brushRadius + " | zoom : " + sourceZoom + " | opacity : " + nf(brushOpacity * 100, 0, 0) + "%");
}

void mouseWheel(MouseEvent event) {
  float delta = event.getCount();
  brushRadius = max(1, brushRadius - delta * 2);
  clampBrushToImage();
}

void dropEvent(DropEvent theDropEvent) {
  File file = theDropEvent.file();
  if (file == null) return;
  File folder = file.isDirectory() ? file : file.getParentFile();
  loadPicsFromFolder(folder);
}

float samplingMargin() {
  if (picDropper.im == null) return 0;
  float zoom = max(0.1, sourceZoom);
  float margin = brushRadius / zoom;
  float maxMargin = min((picDropper.im.width - 1) / 2.0, (picDropper.im.height - 1) / 2.0);
  return constrain(margin, 0, maxMargin);
}

float getMaxBrushRadius() {
  if (picDropper.im == null) return 500;
  float minDim = min(picDropper.im.width, picDropper.im.height);
  return max(1, minDim * sourceZoom * 0.5);
}

void clampBrushToImage() {
  brushRadius = constrain(brushRadius, 1, getMaxBrushRadius());
}
