package textlocator;

import java.awt.Rectangle;
import java.util.LinkedList;
import java.util.List;

/**
 * Locates text area at a given image resolution. See section IV.B
 * NOTE: 1. only a rough outline of the algorithm is provided and certain
 *          parts had to be outright guessed.
 *       2. this class assumes that the text is horizontal
 * @author MX-Futhark
 */
class UniresolutionTextPositionDetector {

	// TODO: not ideal to rely on a factor to discriminate peaks from valleys
	// find several strategies and compare them
	private static final float PEAK_VALLEY_THRESHOLD_FACTOR = 0.1f;
	private static final int HORIZONTAL_PEAK_ABS_MIN_THRESHOLD = 2048;
	private static final int VERTICAL_PEAK_ABS_MIN_THRESHOLD = 256;

	private static final int HORIZONTAL_MERGEABLE_HOLE_SIZE = 1;
	private static final int VERTICAL_MERGEABLE_HOLE_SIZE = 4;

	private static final int MIN_FONT_SIZE = 8;
	private static final int MAX_FONT_SIZE = 24;
	private static final float MIN_CHAR_ASPECT_RATIO = 1.0f;

	/**
	 * Detects text region in an edge map. See Fig6.
	 * @param edgeMap The already computed edge map.
	 * @return All text regions found in the edge map.
	 */
	public static List<Rectangle> getRegions(GrayImage edgeMap) {

		List<Rectangle>
			regionsQueue = new LinkedList<>(),
			validRegions = new LinkedList<>(),
			tentativeHorizontalRegions = new LinkedList<>(),
			tentativeVerticalRegions = new LinkedList<>();

		regionsQueue.add(
			new Rectangle(edgeMap.getWidth(), edgeMap.getHeight())
		);

		while (!regionsQueue.isEmpty()) {

			Rectangle region = regionsQueue.remove(0);
			tentativeHorizontalRegions =
				getHorizontalSubRegions(edgeMap, region);

			for (Rectangle hSubRegion : tentativeHorizontalRegions) {

				boolean hIndivisible = hSubRegion.equals(region);
				tentativeVerticalRegions =
					getVerticalSubRegions(edgeMap, hSubRegion, hIndivisible);

				for (Rectangle vSubRegion : tentativeVerticalRegions) {

					// indivisible region
					if (vSubRegion.equals(hSubRegion)) {
						validRegions.add(vSubRegion);
					} else {
						regionsQueue.add(vSubRegion);
					}
				}
			}
		}

		return validRegions;
	}

	private static int[] getProjection(GrayImage edgeMap,
		Rectangle region, boolean horizontal) {

		int aMin = (int) (horizontal ? region.getY() : region.getX()),
			bMin = (int) (horizontal ? region.getX() : region.getY()),
			aLen = (int) (horizontal ? region.getHeight() : region.getWidth()),
			bLen = (int) (horizontal ? region.getWidth() : region.getHeight()),
			ind = 0;

		int[] res = new int[aLen];

		for (int a = aMin; a < aMin + aLen; ++a) {
			int total = 0;
			for (int b = bMin; b < bMin + bLen; ++b) {
				// TODO other possible strategy: counting edge pixels instead of
				//      taking their value into account
				total +=
					edgeMap.getValue(horizontal ? b : a, horizontal ? a : b);
			}
			res[ind] = total;
			++ind;
		}

		return res;
	}

	// TODO: do without this, try not to split regions in the first place
	private static List<Rectangle> mergeSubRegions(List<Rectangle> subRegions,
		boolean horizontal, boolean wasHorizontalIndivisible) {

		List<Rectangle> res = new LinkedList<>();

		boolean mergeProgresses;

		// merge regions with negligible holes
		Rectangle previousSubRegion = null, currentSubRegion = null;
		for (Rectangle subRegion : subRegions) {

			mergeProgresses = false;
			currentSubRegion = subRegion;

			if (previousSubRegion != null) {
				if (horizontal) {
					if (subRegion.getY() - (previousSubRegion.getY()
							+ previousSubRegion.getHeight())
						<= HORIZONTAL_MERGEABLE_HOLE_SIZE) {

						mergeProgresses = true;
					}
				} else {

					double valleyWidth =
						subRegion.getX() - (previousSubRegion.getX()
						+ previousSubRegion.getWidth());
					double subRegionHeight = subRegion.getHeight();

					if (valleyWidth <= VERTICAL_MERGEABLE_HOLE_SIZE
						|| (wasHorizontalIndivisible
							&& subRegionHeight >= MIN_FONT_SIZE
							&& subRegionHeight <= MAX_FONT_SIZE
							&& valleyWidth < 1.5 * MIN_CHAR_ASPECT_RATIO
								* subRegionHeight)) {

						mergeProgresses = true;
					}
				}
			}
			if (mergeProgresses) {
				previousSubRegion =
					previousSubRegion.union(subRegion);
				currentSubRegion = previousSubRegion;
			} else if (previousSubRegion != null) {
				res.add(previousSubRegion);
			}
			previousSubRegion = currentSubRegion;
		}
		if (previousSubRegion != null) {
			res.add(previousSubRegion);
		}

		return res;

	}

	private static List<Rectangle> getSubRegions(GrayImage edgeMap,
		Rectangle region, boolean horizontal,
		boolean wasHorizontalIndivisible) {

		int[] projection = getProjection(edgeMap, region, horizontal);
		int min = Integer.MAX_VALUE, max = 0;

		// TODO: local thresholding with a window of size 2*MAX_FONT_SIZE,
		//       moving 2*MIN_FONT_SIZE per iteration?
		for (int i = 0; i < projection.length; ++i) {
			if (projection[i] < min) {
				min = projection[i];
			}
			if (projection[i] > max) {
				max = projection[i];
			}
		}

		int threshold = Math.max(
			(int)(min + (max - min) * PEAK_VALLEY_THRESHOLD_FACTOR),
			horizontal
				? HORIZONTAL_PEAK_ABS_MIN_THRESHOLD
				: VERTICAL_PEAK_ABS_MIN_THRESHOLD
		);


		int i = 0, regionStart = 0;
		List<Rectangle> subRegions = new LinkedList<>();

		while(i < projection.length) {

			while (i < projection.length && projection[i] < threshold) ++i;

			regionStart = i;

			while (i < projection.length && projection[i] >= threshold) ++i;

			if (regionStart < projection.length) {
				subRegions.add(new Rectangle(
					(int) region.getX() + (horizontal ? 0 : regionStart),
					(int) region.getY() + (horizontal ? regionStart : 0),
					horizontal ? (int) region.getWidth() : i - regionStart,
					horizontal ? i - regionStart : (int) region.getHeight()
				));
			}
		}

		return
			mergeSubRegions(subRegions, horizontal, wasHorizontalIndivisible);
	}

	private static List<Rectangle> getHorizontalSubRegions(GrayImage edgeMap,
		Rectangle region) {

		List<Rectangle>
			subRegions = getSubRegions(edgeMap, region, true, false),
			res = new LinkedList<>();

		for (Rectangle subRegion : subRegions) {
			if (subRegion.getHeight() < MIN_FONT_SIZE) continue;
			res.add(subRegion);
		}

		return res;
	}

	private static List<Rectangle> getVerticalSubRegions(GrayImage edgeMap,
		Rectangle region, boolean wasHorizontalIndivisible) {

		List<Rectangle>
			subRegions =
				getSubRegions(edgeMap, region, false, wasHorizontalIndivisible),
			res = new LinkedList<>();

		for (Rectangle subRegion : subRegions) {

			double height = subRegion.getHeight(),
				width = subRegion.getWidth();

			if (height <= MAX_FONT_SIZE
				&& width >= height * MIN_CHAR_ASPECT_RATIO) {

				res.add(subRegion);
			}
		}

		return res;
	}

}