package net.osmand.plus.render;

import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.procedure.TIntObjectProcedure;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import net.osmand.binary.BinaryMapDataObject;
import net.osmand.binary.BinaryMapIndexReader.TagValuePair;
import net.osmand.data.QuadRect;
import net.osmand.data.QuadTree;
import net.osmand.plus.render.OsmandRenderer.RenderingContext;
import net.osmand.render.RenderingRuleSearchRequest;
import net.osmand.render.RenderingRulesStorage;
import net.osmand.util.Algorithms;
import net.sf.junidecode.Junidecode;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.util.FloatMath;

public class TextRenderer {

	private Paint paintText;
	private final Context context;
	private Paint paintIcon;

	static class TextDrawInfo {

		public TextDrawInfo(String text) {
			this.text = text;
		}

		String text = null;
		Path drawOnPath = null;
		QuadRect bounds = null;
		float vOffset = 0;
		float centerX = 0;
		float pathRotate = 0;
		float centerY = 0;
		float textSize = 0;
		float minDistance = 0;
		int textColor = Color.BLACK;
		int textShadow = 0;
		int textWrap = 0;
		boolean bold = false;
		String shieldRes = null;
		int textOrder = 100;

		public void fillProperties(RenderingContext rc, RenderingRuleSearchRequest render, float centerX, float centerY) {
			this.centerX = centerX;
			// used only for draw on path where centerY doesn't play role
			this.vOffset = (int) rc.getComplexValue(render, render.ALL.R_TEXT_DY, 0);
			this.centerY = centerY + this.vOffset;
			textColor = render.getIntPropertyValue(render.ALL.R_TEXT_COLOR);
			if (textColor == 0) {
				textColor = Color.BLACK;
			}
			textSize = rc.getComplexValue(render, render.ALL.R_TEXT_SIZE, 0);
			textShadow = (int) rc.getComplexValue(render, render.ALL.R_TEXT_HALO_RADIUS, 0);
			textWrap = (int) rc.getComplexValue(render, render.ALL.R_TEXT_WRAP_WIDTH, 0);
			bold = render.getIntPropertyValue(render.ALL.R_TEXT_BOLD, 0) > 0;
			minDistance = rc.getComplexValue(render, render.ALL.R_TEXT_MIN_DISTANCE, 0);
			if (render.isSpecified(render.ALL.R_TEXT_SHIELD)) {
				shieldRes = render.getStringPropertyValue(render.ALL.R_TEXT_SHIELD);
			}
			textOrder = render.getIntPropertyValue(render.ALL.R_TEXT_ORDER, 100);
		}
	}

	public TextRenderer(Context context) {
		this.context = context;
		paintText = new Paint();
		paintText.setStyle(Style.FILL);
		paintText.setStrokeWidth(1);
		paintText.setColor(Color.BLACK);
		paintText.setTextAlign(Align.CENTER);
		paintText.setTypeface(Typeface.create("Droid Serif", Typeface.NORMAL)); //$NON-NLS-1$
		paintText.setAntiAlias(true);

		paintIcon = new Paint();
		paintIcon.setStyle(Style.STROKE);
	}

	public Paint getPaintText() {
		return paintText;
	}

	private float sqr(float a) {
		return a * a;
	}

	boolean intersects(QuadRect tRect, float tRot, QuadRect sRect, float sRot) {
		if (Math.abs(tRot) < Math.PI / 15 && Math.abs(sRot) < Math.PI / 15) {
			return QuadRect.intersects(tRect, sRect);
		}
		float dist = FloatMath.sqrt(sqr(tRect.centerX() - sRect.centerX()) + sqr(tRect.centerY() - sRect.centerY()));
		if (dist < 3) {
			return true;
		}

		// difference close to 90/270 degrees
		if (Math.abs(Math.cos(tRot - sRot)) < 0.3) {
			// rotate one rectangle to 90 degrees
			tRot += Math.PI / 2;
			float l = tRect.centerX() - tRect.height() / 2;
			float t = tRect.centerY() - tRect.width() / 2;
			tRect = new QuadRect(l, t, l + tRect.height(), t + tRect.width());
		}

		// determine difference close to 180/0 degrees
		if (Math.abs(FloatMath.sin(tRot - sRot)) < 0.3) {
			// rotate t box
			// (calculate offset for t center suppose we rotate around s center)
			float diff = (float) (-Math.atan2(tRect.centerX() - sRect.centerX(), tRect.centerY() - sRect.centerY()) + Math.PI / 2);
			diff -= sRot;
			float left = sRect.centerX() + dist * FloatMath.cos(diff) - tRect.width() / 2;
			float top = sRect.centerY() - dist * FloatMath.sin(diff) - tRect.height() / 2;
			QuadRect nRect = new QuadRect(left, top, left + tRect.width(), top + tRect.height());
			return QuadRect.intersects(nRect, sRect);
		}

		// TODO other cases not covered
		return QuadRect.intersects(tRect, sRect);
	}

	void drawTestBox(Canvas cv, RectF r, float rot, String text) {
		cv.save();
		cv.translate(r.centerX(), r.centerY());
		cv.rotate((float) (rot * 180 / Math.PI));
		RectF rs = new RectF(-r.width() / 2, -r.height() / 2, r.width() / 2, r.height() / 2);
		cv.drawRect(rs, paintIcon);
		if (text != null) {
			paintText.setTextSize(paintText.getTextSize() - 4);
			cv.drawText(text, rs.centerX(), rs.centerY(), paintText);
			paintText.setTextSize(paintText.getTextSize() + 4);
		}
		cv.restore();
	}

	List<TextDrawInfo> tempSearch = new ArrayList<TextDrawInfo>();

	private boolean findTextIntersection(Canvas cv, RenderingContext rc, QuadTree<TextDrawInfo> boundIntersections, TextDrawInfo text) {
		// for test purposes
//		drawTestBox(cv, text.bounds, text.pathRotate, text.text);
		boundIntersections.queryInBox(text.bounds, tempSearch);
		for (int i = 0; i < tempSearch.size(); i++) {
			TextDrawInfo t = tempSearch.get(i);
			if (intersects(text.bounds, text.pathRotate, t.bounds, t.pathRotate)) {
				return true;
			}
		}
		if (text.minDistance > 0) {
			QuadRect boundsSearch = new QuadRect(text.bounds);
			boundsSearch.inset(-Math.max(rc.getDensityValue(5.0f), text.minDistance), -rc.getDensityValue(15));
			boundIntersections.queryInBox(boundsSearch, tempSearch);
			// drawTestBox(cv, &boundsSearch, text.pathRotate, paintIcon, text.text, NULL/*paintText*/);
			for (int i = 0; i < tempSearch.size(); i++) {
				TextDrawInfo t = tempSearch.get(i);
				if (t.minDistance > 0 && t.text.equals(text.text) &&
						intersects(boundsSearch, text.pathRotate, t.bounds, t.pathRotate)) {
					return true;
				}
			}
		}
		boundIntersections.insert(text, text.bounds);
		return false;
	}

	private void drawTextOnCanvas(Canvas cv, String text, float centerX, float centerY, Paint paint, float textShadow) {
		if (textShadow > 0) {
			int c = paintText.getColor();
			paintText.setStyle(Style.STROKE);
			paintText.setColor(Color.WHITE);
			paintText.setStrokeWidth(2 + textShadow);
			cv.drawText(text, centerX, centerY, paint);
			// reset
			paintText.setStrokeWidth(2);
			paintText.setStyle(Style.FILL);
			paintText.setColor(c);
		}
		cv.drawText(text, centerX, centerY, paint);
	}

	public void drawTextOverCanvas(RenderingContext rc, Canvas cv, boolean useEnglishNames) {
		int size = rc.textToDraw.size();

		// 1. Sort text using text order
		Collections.sort(rc.textToDraw, new Comparator<TextDrawInfo>() {
			@Override
			public int compare(TextDrawInfo object1, TextDrawInfo object2) {
				return object1.textOrder - object2.textOrder;
			}
		});
		QuadRect r = new QuadRect(0, 0, rc.width, rc.height);
		r.inset(-100, -100);
		QuadTree<TextDrawInfo> nonIntersectedBounds = new QuadTree<TextDrawInfo>(r, 4, 0.6f);

		for (int i = 0; i < size; i++) {
			TextDrawInfo text = rc.textToDraw.get(i);
			if (text.text != null && text.text.length() > 0) {
				if (useEnglishNames) {
					text.text = Junidecode.unidecode(text.text);
				}

				// sest text size before finding intersection (it is used there)
				float textSize = text.textSize;
				paintText.setTextSize(textSize);
				paintText.setFakeBoldText(text.bold);
				paintText.setColor(text.textColor);
				// align center y
				text.centerY += (-paintText.ascent());

				// calculate if there is intersection
				boolean intersects = findTextIntersection(cv, rc, nonIntersectedBounds, text);
				if (!intersects) {
					if (text.drawOnPath != null) {
						if (text.textShadow > 0) {
							paintText.setColor(Color.WHITE);
							paintText.setStyle(Style.STROKE);
							paintText.setStrokeWidth(2 + text.textShadow);
							cv.drawTextOnPath(text.text, text.drawOnPath, 0, text.vOffset, paintText);
							// reset
							paintText.setStyle(Style.FILL);
							paintText.setStrokeWidth(2);
							paintText.setColor(text.textColor);
						}
						cv.drawTextOnPath(text.text, text.drawOnPath, 0, text.vOffset, paintText);
					} else {
						if (text.shieldRes != null) {
							Bitmap ico = RenderingIcons.getIcon(context, text.shieldRes);
							if (ico != null) {
								float left = text.centerX - ico.getWidth() / 2 * rc.screenDensityRatio
										- 0.5f;
								float top = text.centerY - ico.getHeight() / 2 * rc.screenDensityRatio
										- rc.getDensityValue(4.5f);
								if(rc.screenDensityRatio != 1f){
									RectF rf = new RectF(left, top, left + ico.getWidth() * rc.screenDensityRatio , 
											top + ico.getHeight() * rc.screenDensityRatio);
									Rect src = new Rect(0, 0, ico.getWidth(), ico
											.getHeight());
									cv.drawBitmap(ico, src, rf, paintIcon);
								} else {
									cv.drawBitmap(ico, left, top, paintIcon);
								}
							}
						}

						drawWrappedText(cv, text, textSize);
					}
				}
			}
		}
	}

	private void drawWrappedText(Canvas cv, TextDrawInfo text, float textSize) {
		if (text.textWrap == 0) {
			// set maximum for all text
			text.textWrap = 40;
		}

		if (text.text.length() > text.textWrap) {
			int start = 0;
			int end = text.text.length();
			int lastSpace = -1;
			int line = 0;
			int pos = 0;
			int limit = 0;
			while (pos < end) {
				lastSpace = -1;
				limit += text.textWrap;
				while (pos < limit && pos < end) {
					if (!Character.isLetterOrDigit(text.text.charAt(pos))) {
						lastSpace = pos;
					}
					pos++;
				}
				if (lastSpace == -1 || pos == end) {
					drawTextOnCanvas(cv, text.text.substring(start, pos), text.centerX, text.centerY + line * (textSize + 2), paintText,
							text.textShadow);
					start = pos;
				} else {
					drawTextOnCanvas(cv, text.text.substring(start, lastSpace), text.centerX, text.centerY + line * (textSize + 2),
							paintText, text.textShadow);
					start = lastSpace + 1;
					limit += (start - pos) - 1;
				}
				line++;

			}
		} else {
			drawTextOnCanvas(cv, text.text, text.centerX, text.centerY, paintText, text.textShadow);
		}
	}
	
	private void createTextDrawInfo(final BinaryMapDataObject o, RenderingRuleSearchRequest render, RenderingContext rc, TagValuePair pair, final float xMid, float yMid,
			Path path, final PointF[] points, String name, String tagName) {
		render.setInitialTagValueZoom(pair.tag, pair.value, rc.zoom, o);
		render.setIntFilter(render.ALL.R_TEXT_LENGTH, name.length());
		render.setStringFilter(render.ALL.R_NAME_TAG, tagName);
		if(render.search(RenderingRulesStorage.TEXT_RULES)){
			if(render.getFloatPropertyValue(render.ALL.R_TEXT_SIZE) > 0){
				final TextDrawInfo text = new TextDrawInfo(name);
				text.fillProperties(rc, render, xMid, yMid);
				final String tagName2 = render.getStringPropertyValue(render.ALL.R_NAME_TAG2);
				if (!Algorithms.isEmpty(tagName2)) {
					o.getObjectNames().forEachEntry(new TIntObjectProcedure<String>() {
						@Override
						public boolean execute(int tagid, String nname) {
							String tagNameN2 = o.getMapIndex().decodeType(tagid).tag;
							if (tagName2.equals(tagNameN2)) {
								if (nname != null && nname.trim().length() > 0) {
									text.text += " " + nname;
								}
								return false;
							}
							return true;
						}
					});

				}
				paintText.setTextSize(text.textSize);
				Rect bs = new Rect();
				paintText.getTextBounds(name, 0, name.length(), bs);
				text.bounds = new QuadRect(bs.left, bs.top, bs.right, bs.bottom);
				text.bounds.inset(-rc.getDensityValue(3), -rc.getDensityValue(10));
				boolean display = true;
				if(path != null) {
					text.drawOnPath = path;
					display = calculatePathToRotate(rc, text, points, 
							render.getIntPropertyValue(render.ALL.R_TEXT_ON_PATH, 0) != 0);
				}
				if(text.drawOnPath == null) {
					text.bounds.offset(text.centerX, text.centerY);
					// shift to match alignment
					text.bounds.offset(-text.bounds.width()/2, 0);
				} else {
					text.bounds.offset(text.centerX - text.bounds.width()/2, text.centerY - text.bounds.height()/2);
				}
				if(display) {
					rc.textToDraw.add(text);
				}
			}
		}
	}
	
	public void renderText(final BinaryMapDataObject obj, final RenderingRuleSearchRequest render, final RenderingContext rc, 
			final TagValuePair pair, final float xMid, final float yMid, final Path path, final PointF[] points) {
		TIntObjectHashMap<String> map = obj.getObjectNames();
		if (map != null) {
			map.forEachEntry(new TIntObjectProcedure<String>() {
				@Override
				public boolean execute(int tag, String name) {
					if (name != null && name.trim().length() > 0) {
						createTextDrawInfo(obj, render, rc, pair, xMid, yMid, path, points, name, tag == obj.getMapIndex().nameEncodingType ? ""
								: obj.getMapIndex().decodeType(tag).tag);
					}
					return true;
				}
			});
		}
	}

	
	boolean calculatePathToRotate(RenderingContext rc, TextDrawInfo p, PointF[] points, boolean drawOnPath) {
		int len = points.length;
		if (!drawOnPath) {
			p.drawOnPath = null;
			// simply calculate rotation of path used for shields
			float px = 0;
			float py = 0;
			for (int i = 1; i < len; i++) {
				px += points[i].x - points[i - 1].x;
				py += points[i].y - points[i - 1].y;
			}
			if (px != 0 || py != 0) {
				p.pathRotate = (float) (-Math.atan2(px, py) + Math.PI / 2);
			}
			return true;
		}

		boolean inverse = false;
		float roadLength = 0;
		boolean prevInside = false;
		float visibleRoadLength = 0;
		float textw = p.bounds.width();
		int last = 0;
		int startVisible = 0;
		float[] distances = new float[points.length - 1];

		float normalTextLen = 1.5f * textw;
		for (int i = 0; i < len; i++, last++) {
			boolean inside = points[i].x >= 0 && points[i].x <= rc.width &&
					points[i].x >= 0 && points[i].y <= rc.height;
			if (i > 0) {
				float d = FloatMath.sqrt(sqr(points[i].x - points[i - 1].x) + 
						sqr(points[i].y - points[i - 1].y));
				distances[i-1]= d;
				roadLength += d;
				if(inside) {
					visibleRoadLength += d;
					if(!prevInside) {
						startVisible = i - 1;
					}
				} else if(prevInside) {
					if(visibleRoadLength >= normalTextLen) {
						break;
					}
					visibleRoadLength = 0;
				}

			}
			prevInside = inside;
		}
		if (textw >= roadLength) {
			return false;
		}
		int startInd = 0;
		int endInd = len;

		if(textw < visibleRoadLength &&  last - startVisible > 1) {
			startInd = startVisible;
			endInd = last;
			// display long road name in center
			if (visibleRoadLength > 3 * textw) {
				boolean ch ;
				do {
					ch = false;
					if(endInd - startInd > 2 && visibleRoadLength - distances[startInd] > normalTextLen){
						visibleRoadLength -= distances[startInd];
						startInd++;
						ch = true;
					}
					if(endInd - startInd > 2 && visibleRoadLength - distances[endInd - 2] > normalTextLen){
						visibleRoadLength -= distances[endInd - 2];
						endInd--;
						ch = true;
					}
				} while(ch);
			}
		}
		// shrink path to display more text
		if (startInd > 0 || endInd < len) {
			// find subpath
			Path path = new Path(); 
			for (int i = startInd; i < endInd; i++) {
				if (i == startInd) {
					path.moveTo(points[i].x, points[i].y);
				} else {
					path.lineTo(points[i].x, points[i].y);
				}
			}
			p.drawOnPath = path;
		}
		// calculate vector of the road (px, py) to proper rotate it
		float px = 0;
		float py = 0;
		for (int i = startInd + 1; i < endInd; i++) {
			px += points[i].x - points[i - 1].x;
			py += points[i].y - points[i - 1].y;
		}
		float scale = 0.5f;
		float plen = (float) Math.sqrt(px * px + py * py);
		// vector ox,oy orthogonal to px,py to measure height
		float ox = -py;
		float oy = px;
		if(plen > 0) {
			float rot = (float) (-Math.atan2(px, py) + Math.PI / 2);
			if (rot < 0) rot += Math.PI * 2;
			if (rot > Math.PI / 2f && rot < 3 * Math.PI / 2f) {
				rot += Math.PI;
				inverse = true;
				ox = -ox;
				oy = -oy;
			}
			p.pathRotate = rot;
			ox *= (p.bounds.height() / plen) / 2;
			oy *= (p.bounds.height() / plen) / 2;
		}

		p.centerX = points[startInd].x + scale * px + ox;
		p.centerY = points[startInd].y + scale * py + oy;
		p.vOffset += p.textSize / 2 - 1;
//		p.hOffset = 0;

		if (inverse) {
			Path path = new Path();
			for (int i = endInd - 1; i >= startInd; i--) {
				if (i == endInd - 1) {
					path.moveTo(points[i].x, points[i].y);
				} else {
					path.lineTo(points[i].x, points[i].y);
				}
			}
			p.drawOnPath = path;
		}
		return true;
	}


}
