package com.cringe_studios.christmastreescanning;

import java.awt.Color;
import java.awt.Point;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

public class ChristmasTreeScanning {
	//TODO maybe use Point2D.Double as return type, so the Points don't have to be converted to Points2D later down the pipeline during the normalization step
	private Point getBrightestSpotGrayscale(BufferedImage grayScaleImage, byte gammaDelta) {
		int[] imageColors = grayScaleImage.getRGB(0, 0, grayScaleImage.getWidth(), grayScaleImage.getHeight(), null, 0, 0);
		
		for(int i = 0; i < imageColors.length; i++) {
			imageColors[i] &= 0x0000_00FF; // because the image is grayscale, only one component is needed
		}
		
		int brightestValue = 0;
		
		for(int i = 0; i < imageColors.length; i++) {
			if(brightestValue < imageColors[i]) {
				brightestValue = imageColors[i];
			}
		}
		
		List<Integer> brightSpots = new ArrayList<Integer>();
		
		for(int i = 0; i < imageColors.length; i++) {
			if(imageColors[i] >= brightestValue - gammaDelta) {
				brightSpots.add(Integer.valueOf(i));
			}
		}
		
		//The following line might be incorrect; TODO double check and write a test
		Point brightPoints[] = brightSpots.stream().map(s -> new Point(s.intValue() % grayScaleImage.getWidth(), s.intValue() / grayScaleImage.getWidth())).toArray(Point[]::new);
		
		Point brightestSpot = new PointInterpolator(brightPoints).getCenter();
		
		return brightestSpot;
	}
	
	//gammaDelta is the difference between the brightest pixel and the darkest pixel included in the search set for the brightest spot
	//The brightest point Operation is done per color dimension, so you could provide different images for red, green and blue brightest points
	public Point getBrightestSpot(BufferedImage redImage, BufferedImage greenImage, BufferedImage blueImage, byte gammaDelta) {
		Point brightestRedSpot = getBrightestSpotGrayscale(redImage, gammaDelta);
		Point brightestGreenSpot = getBrightestSpotGrayscale(greenImage, gammaDelta);
		Point brightestBlueSpot = getBrightestSpotGrayscale(blueImage, gammaDelta);
		
		double RGdistance = brightestRedSpot.distanceSq(brightestGreenSpot);
		double GBdistance = brightestGreenSpot.distanceSq(brightestBlueSpot);
		double BRdistance = brightestBlueSpot.distanceSq(brightestRedSpot);
		
		PointInterpolator brightestSpot;
		
		//Get the center between the two closest points; a better implementation would work with a confidentnes-value
		if(RGdistance < GBdistance && RGdistance < BRdistance) {
			brightestSpot = new PointInterpolator(brightestRedSpot, brightestGreenSpot);
		}else if(GBdistance < RGdistance && GBdistance < BRdistance) {
			brightestSpot = new PointInterpolator(brightestGreenSpot, brightestBlueSpot);
		}else {
			brightestSpot = new PointInterpolator(brightestBlueSpot, brightestRedSpot);
		}
		
		
		return brightestSpot.getCenter();
	}
	
	public Point getBrightestSpot(BufferedImage image, byte gammaDelta) {
		BufferedImage redImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
		BufferedImage greenImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
		BufferedImage blueImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
		
		for(int i = 0; i < image.getWidth(); i++) {
			for(int j = 0; j < image.getHeight(); j++) {
				int currentRGB = image.getRGB(i, j);
				int currentBlue = currentRGB & 0xFF;
				int currentGreen = (currentRGB >> 8) & 0xFF;
				int currentRed = (currentRGB >> 16) & 0xFF;
				
				redImage.setRGB(i, j, (currentRed   << 16) | (currentRed   << 8) | currentRed);
				greenImage.setRGB(i, j, (currentGreen << 16) | (currentGreen << 8) | currentGreen);
				blueImage.setRGB(i, j, (currentBlue  << 16) | (currentBlue  << 8) | currentBlue);
			}
		}
		
		return getBrightestSpot(redImage, greenImage, blueImage, gammaDelta);
	}
	
	public Point getBrightestSpot(BufferedImage image) {
		return getBrightestSpot(image, (byte)0);
	}
	
	
	
	// This method normalizes the Point coordinates,
	// Optionally flips them horizontally
	// Adjusts the height (y-coordinate) to make the virtual points not squished (puts them into a rectangle)
	// Returns the Points as Point2D
	public Point2D[] normalizePoints(Point[] points, boolean isBacksite) {
		if(points == null) return null;
		
		Point2D normalizedPoints[] = new Point2D.Double[points.length];
		for(int i = 0; i < normalizedPoints.length; i++) {
			
		}
		
		//TODO is this still needed?
		
		if(isBacksite) {
			for(int i = 0; i < normalizedPoints.length; i++) {
				normalizedPoints[i].setLocation(-normalizedPoints[i].getX(), normalizedPoints[i].getY()); 
			}
		}
		
		return normalizedPoints;
	}
	
	/**
	 * Normalizes 3D points with the given parameters and returns a NEW array with the normalized points. To see which coordinate dimension represents what direction, view Point3D class
	 * @param points
	 * @param baseline for z coordinates. Can be different to the lowest LED
	 * @param minX minimal X value of LED box
	 * @param minY minimal Y value of LED box
	 * @param maxX maximal X value of LED box
	 * @param maxY maximal Y value of LED box
	 * @return new Array with  normalized Points
	 */
	public Point3D[] normalizePoints(Point3D[] points, double baseline, double minX, double minY, double maxX, double maxY) {		
		Point3D normalizedPoints[] = new Point3D[points.length];
		for(int i = 0; i < normalizedPoints.length; i++) {
			double normalizedX = (points[i].x - minX) / (maxX - minX);
			double normalizedY = (points[i].y - minY) / (maxY - minY);
			double normalizedZ = ((baseline - points[i].z) - minX) / (maxX - minX);
			
			normalizedX = normalizedX * 2.0 - 1.0;
			normalizedY = normalizedY * 2.0 - 1.0;
			
			normalizedPoints[i] = new Point3D(normalizedX, normalizedY, normalizedZ);
		}
		
		return normalizedPoints;
	}
	
	public Point3D[] normalizePoints(Point3D[] points, double baseline) {
		double lowestX = Double.POSITIVE_INFINITY;
		double highestX = Double.NEGATIVE_INFINITY;
		
		for(int i = 0; i < points.length; i++) {
			if(lowestX > points[i].x) {
				lowestX = points[i].x;
			}
			
			if(highestX < points[i].x) {
				highestX = points[i].x;
			}
		}
		
		return normalizePoints(points, baseline, lowestX, highestX, lowestX, highestX);
	}
	
	public Point3D[] normalizePoints(Point3D[] points) {
		double lowestZ = Double.POSITIVE_INFINITY;
		
		for(int i = 0; i < points.length; i++) {
			if(lowestZ > points[i].z) {
				lowestZ = points[i].z;
			}
		}
		
		return normalizePoints(points, lowestZ);
	}
}
