/* Reads a file of coordinates
 */
import java.awt.AWTException;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PointerInfo;
import java.awt.Robot;
import java.awt.event.InputEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Cursor {
	
	public static final int NUMBER_OF_DISTANCES = 2203; // For 1080p screen
	public static final int MINIMUM_CLICK_LENGTH = 120;
	public static final int MAXIMUM_CLICK_LENGTH = 240;
	
	private Robot robot;
	private Randomizer randomizer;
	private Random random;

	private ArrayList<ArrayList<CursorPath>> cursorPathsByDistance;
	
	public Cursor() throws AWTException {
		System.out.println("Initializing cursor...");
		initializeCursorPathsByDistanceFromFile("/home/dpapp/GhostMouse/coordinates.txt");

		robot = new Robot();
		randomizer = new Randomizer();
		random = new Random();
	}
	
	private void initializeCursorPathsByDistanceFromFile(String path) {
		initializeCursorPathsByDistance();
		ArrayList<CursorPath> cursorPaths = getArrayListOfCursorPathsFromFile(path);
		assignCursorPathsByDistance(cursorPaths);
	}
	
	private void initializeCursorPathsByDistance() {
		this.cursorPathsByDistance = new ArrayList<ArrayList<CursorPath>>();
		for (int i = 0; i < NUMBER_OF_DISTANCES; i++) {
			this.cursorPathsByDistance.add(new ArrayList<CursorPath>());
		}
	}
	
	private ArrayList<CursorPath> getArrayListOfCursorPathsFromFile(String path) {
		CursorDataFileParser cursorDataFileParser = new CursorDataFileParser(path);
		return cursorDataFileParser.getArrayListOfCursorPathsFromFile();
	}
	
	private void assignCursorPathsByDistance(ArrayList<CursorPath> cursorPaths) {
		for (CursorPath cursorPath : cursorPaths) {
			if (cursorPath.isCursorPathReasonable()) { 
				addCursorPathToCursorPathsByDistance(cursorPath);
			}
		}
	}
	
	private void addCursorPathToCursorPathsByDistance(CursorPath cursorPath) {
		this.cursorPathsByDistance.get(cursorPath.getCursorPathDistance()).add(cursorPath);
	}
	
	
	// TODO: make sure these are reasonable
	private int getRandomClickLength() {
		return randomizer.nextGaussianWithinRange(MINIMUM_CLICK_LENGTH, MAXIMUM_CLICK_LENGTH);
	}
	
	public void leftClickCursor() throws InterruptedException {
		robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
		Thread.sleep(getRandomClickLength());
		robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
		Thread.sleep(getRandomClickLength());
	}
	
	public void rightClickCursor() throws InterruptedException {
		robot.mousePress(InputEvent.BUTTON3_DOWN_MASK);
		Thread.sleep(20 + getRandomClickLength());
		robot.mouseRelease(InputEvent.BUTTON3_DOWN_MASK);
		Thread.sleep(getRandomClickLength());
	}
	
	public void moveAndLeftClickAtCoordinates(Point goalPoint) throws Exception {
		moveCursorToCoordinates(goalPoint);
		leftClickCursor();
	}
	
	public void moveAndRightClickAtCoordinates(Point goalPoint) throws Exception {
		moveCursorToCoordinates(goalPoint);
		rightClickCursor();
	}
	
	public Point moveAndLeftClickAtCoordinatesWithRandomness(Point goalPoint, int xTolerance, int yTolerance) throws Exception {
		Point randomizedGoalPoint = randomizePoint(goalPoint, xTolerance, yTolerance);
		moveCursorToCoordinates(randomizedGoalPoint);
		leftClickCursor();
		return randomizedGoalPoint; // Return the point we moved to in case we need precise movement afterwards
	}
	
	public Point moveAndRightlickAtCoordinatesWithRandomness(Point goalPoint, int xTolerance, int yTolerance) throws Exception {
		Point randomizedGoalPoint = randomizePoint(goalPoint, xTolerance, yTolerance);
		moveCursorToCoordinates(randomizedGoalPoint);
		rightClickCursor();
		return randomizedGoalPoint; // Return the point we moved to in case we need precise movement afterwards
	}

	public void moveCursorToCoordinates(Point goalPoint) throws Exception {
		Point startingPoint = getCurrentCursorPoint();
		int distanceToMoveCursor = getDistanceBetweenPoints(startingPoint, goalPoint);
		double angleToRotateCursorPathTo = getThetaBetweenPoints(startingPoint, goalPoint);
		System.out.println("R:" + distanceToMoveCursor + ", theta:" + angleToRotateCursorPathTo);
		if (distanceToMoveCursor == 0) { 
			return;
		}
		
		CursorPath cursorPathWithDistanceSet = chooseCursorPathToFollowBasedOnDistance(distanceToMoveCursor);
		CursorPath cursorPathWithDistanceAndAngleSet = cursorPathWithDistanceSet.getRotatedCopyOfCursorPath(angleToRotateCursorPathTo);
		System.out.println("Rotated the points: ");
		//cursorPathWithDistanceAndAngleSet.displayCursorPoints();
		followCursorPath(cursorPathWithDistanceAndAngleSet, startingPoint);
	}
	
	public int getDistanceBetweenPoints(Point startingPoint, Point goalPoint) {
		return (int) (Math.hypot(goalPoint.x - startingPoint.x, goalPoint.y - startingPoint.y));
	}
	
	public double getThetaBetweenPoints(Point startingPoint, Point goalPoint) {
		return Math.atan2((goalPoint.x - startingPoint.x), (goalPoint.y - startingPoint.y));
	}

	private void followCursorPath(CursorPath cursorPathToFollow, Point startingPoint) throws InterruptedException {
		for (CursorPoint cursorPoint : cursorPathToFollow.getCursorPathPoints()) {
			Point translatedPointToClick = new Point(cursorPoint.x + startingPoint.x, cursorPoint.y + startingPoint.y);
			robotMouseMove(translatedPointToClick);
			Thread.sleep(cursorPoint.delay);
		}
	}
	
	public void robotMouseMove(Point pointToMoveCursorTo) {
		robot.mouseMove(pointToMoveCursorTo.x, pointToMoveCursorTo.y);
	}

	private CursorPath chooseCursorPathToFollowBasedOnDistance(int distanceToMoveCursor) throws Exception {		
		int newDistanceToMoveCursor = findNearestPathLengthThatExists(distanceToMoveCursor);
		double scaleToFactorBy = getScaleToFactorBy(newDistanceToMoveCursor, distanceToMoveCursor);
		System.out.println("new distance to follow cursor is: " + newDistanceToMoveCursor + " from scaling by " + scaleToFactorBy);
		ArrayList<CursorPath> cursorPathsWithSameDistance = cursorPathsByDistance.get(newDistanceToMoveCursor);
		
		CursorPath scaledCursorPath = cursorPathsWithSameDistance.get(random.nextInt(cursorPathsWithSameDistance.size())).getScaledCopyOfCursorPath(scaleToFactorBy);
		
		return scaledCursorPath;
		//return cursorPathsByDistance.get(newDistanceToMoveCursor).get(indexOfRandomPathToFollow);//scaledCursorPath;
	}
	
	public int findNearestPathLengthThatExists(int distanceToMoveCursor) throws Exception {		
		int offset = 0;
		boolean reachedMinimumLimit = false;
		boolean reachedMaximumLimit = false;
		while (cursorPathsByDistance.get(distanceToMoveCursor + offset).size() == 0) {
			// Go up
			if (distanceToMoveCursor + Math.abs(offset) + 1 >= NUMBER_OF_DISTANCES) {
				reachedMaximumLimit = true;
			}
			if (distanceToMoveCursor - Math.abs(offset) - 1 < 0) {
				reachedMinimumLimit = true;
			}
			if (reachedMaximumLimit && reachedMinimumLimit) {
				throw new Exception("No paths exist.");
			}
			
			if (offset < 0) {
				if (!reachedMaximumLimit) {
					offset = -offset + 1;
				}
				else {
					offset--;
				}
			}
			else {
				if (!reachedMinimumLimit) {
					offset = -offset - 1;
				}
				else {
					offset++;
				}
			}
		}
		return distanceToMoveCursor + offset;
	}
	
	private double getScaleToFactorBy(int newDistanceToMoveCursor, int distanceToMoveCursor) {
		return (1.0 * distanceToMoveCursor / newDistanceToMoveCursor);
	}

	
	public Point getCurrentCursorPoint() {
		return MouseInfo.getPointerInfo().getLocation();
	}
	
	private Point randomizePoint(Point goalPoint, int xTolerance, int yTolerance) {
		Randomizer randomizer = new Randomizer();
		return new Point(goalPoint.x + randomizer.nextGaussianWithinRange(-xTolerance, xTolerance), goalPoint.y + randomizer.nextGaussianWithinRange(-yTolerance, yTolerance));
	}
	
	public void displayCursorPaths() {
		for (int i = 0; i < NUMBER_OF_DISTANCES; i++) {
			System.out.println("There are " + cursorPathsByDistance.get(i).size() + " paths of size " + i);
		}
		System.out.println("--------------");
		/*for (int i = 0; i < cursorPathsByDistance.get(1).size(); i++) {
			cursorPathsByDistance.get(1).get(i).displayCursorPoints();
		}*/
		//cursorPathsByDistance.get(0).get(0).displayCursorPoints();
	}
}