package org.stegosuite.image.embedding.point;

import org.stegosuite.image.format.ImageFormat;
import org.stegosuite.util.CryptoUtils;

import java.awt.*;
import java.util.*;
import java.util.List;

/**
 * This class is used for data spreading. It generates random 2D points using a key as a seed. Use
 * the same key to generate the same sequence of points. All generated points are limited to the
 * given bounds (width and height)
 */
public class PointGenerator<T extends ImageFormat> {

	/**
	 * The image to generate points for
	 */
	private T image = null;

	/**
	 * The filter to apply to the list of all possible points in order to avoid embedding into
	 * certain pixels
	 */
	private PointFilter<T> pointFilter = null;

	/**
	 * PRNG, initialized in constructor if seed is provided, otherwise null
	 */
	private Random random = null;

	/**
	 * Contains all points that haven't been visited
	 */
	private List<Point> points = new LinkedList<>();

	/**
	 * The maximum number of times the points list should be seeded
	 */
	private int maxIterations = 1;

	/**
	 * The number of times the points list is seeded
	 */
	private int iterationCount = 0;

	/**
	 * Constructor
	 *
	 * @param image The image to generate points for
	 * @param key The key to seed the PRNG with. If key is null, no PRNG is used
	 * @param pointFilter The filter to apply to the list of all possible points in order to skip
	 *        certain pixels when embedding
	 */
	public PointGenerator(T image, String key, PointFilter<T> pointFilter) {
		this.image = image;
		this.pointFilter = pointFilter;
		this.random = key == null ? null : CryptoUtils.seededRandom(key);
		this.maxIterations = pointFilter.maxLsbCount();
	}

	/**
	 * Generates all possible points and shuffles them
	 */
	private int seedPoints() {
		if (iterationCount >= maxIterations) {
			throw new NoSuchElementException("No more points to generate");
		}

		int width = image.getWidth();
		int height = image.getHeight();

		Collection<Point> filteredPoints = pointFilter.getFilteredPoints(image);

		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				Point point = new Point(x, y);
				if (!filteredPoints.contains(point)) {
					points.add(point);
				}
			}
		}

		if (random != null) {
			Collections.shuffle(points, random);
		}

		return ++iterationCount;
	}

	/**
	 * Returns the next random point
	 *
	 * @return
	 * @throws NoSuchElementException
	 */
	public Point nextPoint()
			throws NoSuchElementException {
		if (points.isEmpty()) {
			seedPoints();
		}
		return points.remove(0);
	}

	/**
	 * Returns true if the passed point has been generated by nextPoint() in the current iteration
	 * phase, otherwise false
	 *
	 * @param point
	 * @return
	 */
	public boolean wasGenerated(Point point) {
		return !points.contains(point);
	}

	public int getMaxIterations() {
		return maxIterations;
	}

	/**
	 * Returns the number of times the points list has been seeded
	 *
	 * @return
	 */
	public int getIterationCount() {
		return iterationCount;
	}
}
