package com.baselet.control;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.baselet.control.basics.geom.Rectangle;
import com.baselet.control.constants.SharedConstants;

public class SharedUtils {

	private static final Logger log = LoggerFactory.getLogger(SharedUtils.class);

	public static int realignToGrid(double d) {
		return realignTo(true, d, false, SharedConstants.DEFAULT_GRID_SIZE);
	}

	/**
	 * realigns a rectangle to the grid enlarging any side if necessary (eg: x=9, width=10, x2=19 will be realigned to x=0, width=20, x2=20)
	 *
	 * @param rectangle rectangle to realign
	 * @return a realigned copy of the input rectangle
	 * @param realignHeightAndWidth if true height and width are also realigned if necessary
	 */
	public static Rectangle realignToGrid(Rectangle rectangle, boolean realignHeightAndWidth) {
		int x = realignToGrid(false, rectangle.getX(), false);
		int y = realignToGrid(false, rectangle.getY(), false);
		if (realignHeightAndWidth) {
			int width = realignToGrid(false, rectangle.getX() - x + rectangle.getWidth(), true); // IMPORTANT: the difference from original x and realigned x must be added, otherwise the upper example would return x=0, width=10, x2=10. x2 would be too small
			int height = realignToGrid(false, rectangle.getY() - y + rectangle.getHeight(), true);
			return new Rectangle(x, y, width, height);
		}
		else {
			return new Rectangle(x, y, rectangle.getWidth(), rectangle.getHeight());
		}
	}

	/**
	 * rounds eg: 5 to 10, 4 to 0, -5 to -10, -4 to 0
	 */
	public static int realignToGridRoundToNearest(boolean logRealign, double val) {
		boolean roundUp;
		if (Math.abs(val % SharedConstants.DEFAULT_GRID_SIZE) < SharedConstants.DEFAULT_GRID_SIZE / 2) {
			roundUp = val < 0;
		}
		else {
			roundUp = val >= 0;
		}
		return realignTo(logRealign, val, roundUp, SharedConstants.DEFAULT_GRID_SIZE);
	}

	public static int realignToGrid(boolean logRealign, double val, boolean roundUp) {
		return realignTo(logRealign, val, roundUp, SharedConstants.DEFAULT_GRID_SIZE);
	}

	/**
	 * returns the integer which is nearest to val but on the grid (round down)
	 *
	 * @param logRealign
	 *            if true a realign is logged as an error
	 * @param val
	 *            value which should be rounded to the grid
	 * @param roundUp
	 *            if true the realign rounds up instead of down
	 * @return value on the grid
	 */
	public static int realignTo(boolean logRealign, double val, boolean roundUp, int gridSize) {
		double alignedVal = val;
		double mod = val % gridSize;
		if (mod != 0) {
			alignedVal -= mod; // ExampleA: 14 - 4 = 10 // ExampleB: -14 - -4 = -10 // (positive vals get round down, negative vals get round up)
			if (val > 0 && roundUp) { // eg ExampleA: 10 + 10 = 20 (for positive vals roundUp must be specifically handled by adding gridSize)
				alignedVal += gridSize;
			}
			if (val < 0 && !roundUp) { // eg ExampleB: -10 - 10 = -20 (for negative vals roundDown must be specifically handled by subtracting gridSize)
				alignedVal -= gridSize;
			}
			if (logRealign) {
				log.error("realignToGrid from " + val + " to " + alignedVal);
			}
		}
		return (int) alignedVal;
	}

	public static String listToString(String sep, Collection<?> list) {
		return listToStringHelper(new StringBuilder(), sep, list).toString();
	}

	private static StringBuilder listToStringHelper(StringBuilder sb, String sep, Collection<?> list) {
		for (Object line : list) {
			sb.append(line).append(sep);
		}
		if (sb.length() > 0) {
			sb.setLength(sb.length() - sep.length());
		}
		return sb;
	}

	public static String mapToString(Map<?, ?> map) {
		return mapToString("\n", ",", map);
	}

	public static String mapToString(String mapSep, String listSep, Map<?, ?> map) {
		StringBuilder sb = new StringBuilder();
		for (Entry<?, ?> e : map.entrySet()) {
			sb.append(e.getKey()).append(": ");
			if (e.getValue() instanceof Collection<?>) {
				listToStringHelper(sb, listSep, (Collection<?>) e.getValue());
			}
			else {
				sb.append(e.getValue().toString());
			}
			sb.append(mapSep);
		}
		if (sb.length() > 0) {
			sb.setLength(sb.length() - mapSep.length());
		}
		return sb.toString();
	}

	public static <T> List<T> mergeLists(List<T> listA, List<T> listB, List<T> listC) {
		List<T> returnList = new ArrayList<T>(listA);
		returnList.addAll(listB);
		returnList.addAll(listC);
		return Collections.unmodifiableList(returnList);
	}

	public static Double[][] cloneArray(Double[][] src) {
		int length = src.length;
		Double[][] target = new Double[length][src[0].length];
		for (int i = 0; i < length; i++) {
			System.arraycopy(src[i], 0, target[i], 0, src[i].length);
		}
		return target;
	}

	public static String[] cloneArray(String[] src) {
		String[] target = new String[src.length];
		System.arraycopy(src, 0, target, 0, src.length);
		return target;
	}

	public static int[] cloneArray(int[] src) {
		int[] target = new int[src.length];
		System.arraycopy(src, 0, target, 0, src.length);
		return target;
	}

	/**
	 * if the user types "\n" it should be translated to a linebreak. He can avoid this by typing \\n (escaping the linebreak)
	 */
	public static String[] splitAtLineEndChar(String text) {
		String rep = StringStyle.replaceNotEscaped(text, "\\n", "\n");
		if (rep.contains("\n")) {
			String[] split = rep.split("\n"); // split uses a RegEx therefore escape the linebreak char
			return split;
		}
		return new String[] { text };
	}

}
