package squidpony.squidgrid.gui.gdx;

import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFont.BitmapFontData;
import com.badlogic.gdx.graphics.g2d.BitmapFontCache;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.utils.Align;
import squidpony.panel.IColoredString;
import squidpony.panel.IMarkup;

import java.util.LinkedList;
import java.util.ListIterator;

/**
 * An actor capable of drawing {@link IColoredString}s. It is lines-oriented:
 * putting a line may erase a line put before. It is designed to write text with
 * a variable-width font (as opposed to {@link SquidPanel}). It performs line wrapping by
 * default. It can write from top to bottom or from bottom to top (the default).
 * 
 * <p>
 * This
 * <a href="https://twitter.com/hgamesdev/status/736091292132724736">tweet</a>
 * shows an example. The panel at the top of the screenshot is implemented using
 * this class (with {@link #drawBottomUp} being {@code true}).
 * </p>
 * 
 * <p>
 * This class is usually used as follows:
 * 
 * <pre>
 * final int nbLines = LinesPanel.computeMaxLines(font, pixelHeight);
 * final LinesPanel<Color> lp = new LinesPanel(new GDXMarkup(), font, nbLines);
 * lp.setSize(pixelWidth, pixelHeight);
 * stage.addActor(lp);
 * </pre>
 * </p>
 * 
 * <p>
 * Contrary to {@link SquidMessageBox}, this panel doesn't support scrolling
 * (for now). So it's suited when it is fine forgetting old messages (as in
 * brogue's messages area).
 * </p>
 * 
 * @author smelC
 * @param <T>
 * 
 * @see SquidMessageBox An alternative, doing similar lines-drawing business,
 *      but being backed up by {@link SquidPanel}.
 */
public class LinesPanel<T extends Color> extends Actor {

	/** The markup used to typeset {@link #content}. */
	protected final IMarkup<T> markup;

	/** The font used to draw {@link #content}. */
	protected final BitmapFont font;

	/** What to display. Doesn't contain {@code null} entries. */
	protected final LinkedList<IColoredString<T>> content;

	/** The maximal size of {@link #content} */
	protected final int maxLines;

	/**
	 * The renderer used by {@link #clearArea(Batch)}. Do not access directly:
	 * use {@link #getRenderer()} instead.
	 */
	protected /* @Nullable */ ShapeRenderer renderer;

	/**
	 * The horizontal offset to use when writing. If you aren't doing anything
	 * weird, should be left to {@code 0}.
	 */
	public float xOffset = 0;

	/**
	 * The vertical offset to use when writing. If you aren't doing anything
	 * weird, should be left to {@code 0}.
	 */
	public float yOffset = 0;

	/**
	 * If {@code true}, draws:
	 * 
	 * <pre>
	 * ...
	 * content[1]
	 * content[0]
	 * </pre>
	 * 
	 * If {@code false}, draws:
	 * 
	 * <pre>
	 * content[0]
	 * content[1]
	 * ...
	 * </pre>
	 */
	public boolean drawBottomUp = false;

	/**
	 * The color to use to clear the screen before drawing. Set it to
	 * {@code null} if you clean on your own.
	 */
	public Color clearingColor = Color.BLACK;

	/* Now comes the usual libgdx options */

	/** Whether to wrap text */
	public boolean wrap = true;

	/** The alignment used when typesetting */
	public int align = Align.left;

	/**
	 * @param markup
	 *            The markup to use, or {@code null} if none. You likely want to
	 *            give {@link GDXMarkup}. If non-{@code null}, markup will be
	 *            enabled in {@code font}.
	 * @param font
	 *            The font to use.
	 * @param maxLines
	 *            The maximum number of lines that this panel should display.
	 *            Must be {@code >= 0}.
	 * @throws IllegalStateException
	 *             If {@code maxLines < 0}
	 */
	public LinesPanel(/* @Nullable */ IMarkup<T> markup, BitmapFont font, int maxLines) {
		this.markup = markup;
		this.font = font;
		if (markup != null)
			this.font.getData().markupEnabled |= true;
		this.content = new LinkedList<IColoredString<T>>();
		if (maxLines < 0)
			throw new IllegalStateException("The maximum number of lines in an instance of "
					+ getClass().getSimpleName() + " must be greater or equal than zero");
		this.maxLines = maxLines;
	}

	/**
	 * Used to help find the last parameter to give the constructor of this class.
	 * @param font the font being used
	 * @param height the height of the area you want to put text into
	 * @return The last argument to give to
	 *         {@link #LinesPanel(IMarkup, BitmapFont, int)} when the
	 *         desired <b>pixel</b> height is {@code height}
	 */
	public static int computeMaxLines(BitmapFont font, float height) {
		return MathUtils.ceil(height / font.getData().lineHeight);
	}

	/**
	 * Adds {@code ics} first in {@code this}, possibly removing the last entry,
	 * if {@code this}' size would grow over {@link #maxLines}.
	 * 
	 * @param ics
	 */
	public void addFirst(IColoredString<T> ics) {
		if (ics == null)
			throw new NullPointerException("Adding a null entry is forbidden");
		if (maxLines == 0)
			/* Empty panel. Can happen when screen size is absurdly small. */
			return;
		if (atMax())
			content.removeLast();
		content.addFirst(ics);
	}

	/**
	 * Adds {@code ics} last in {@code this}, possibly removing the last entry,
	 * if {@code this}' size would grow over {@link #maxLines}.
	 * 
	 * @param ics
	 */
	public void addLast(IColoredString<T> ics) {
		if (ics == null)
			throw new NullPointerException("Adding a null entry is forbidden");
		if (maxLines == 0)
			/* Empty panel. Can happen when screen size is absurdly small. */
			return;
		if (atMax())
			content.removeLast();
		content.addLast(ics);
	}

	@Override
	public void draw(Batch batch, float parentAlpha) {
		clearArea(batch);

		final float width = getWidth();

		final BitmapFontData data = font.getData();
		final float lineHeight = data.lineHeight;

		final float height = getHeight();

		final float x = getX() + xOffset;
		float y = getY() + (drawBottomUp ? lineHeight : height) - data.descent + yOffset;

		final ListIterator<IColoredString<T>> it = content.listIterator();
		int ydx = 0;
		float consumed = 0;
		while (it.hasNext()) {
			final IColoredString<T> ics = it.next();
			final String str = toDraw(ics, ydx);
			/* Let's see if the drawing would go outside this Actor */
			final BitmapFontCache cache = font.getCache();
			cache.clear();
			final GlyphLayout glyph = cache.addText(str, 0, y, width, align, wrap);
			if (height < consumed + glyph.height)
				/* We would draw outside this Actor's bounds */
				break;
			final int increaseAlready;
			if (drawBottomUp) {
				/*
				 * If the text span multiple lines and we draw bottom-up, we
				 * must go up *before* drawing.
				 */
				final int nbLines = MathUtils.ceil(glyph.height / lineHeight);
				if (1 < nbLines) {
					increaseAlready = nbLines - 1;
					y += increaseAlready * lineHeight;
				} else
					increaseAlready = 0;
			} else
				increaseAlready = 0;
			/* Actually draw */
			font.draw(batch, str, x, y, width, align, wrap);
			y += (drawBottomUp ? /* Go up */ 1 : /* Go down */ -1) * glyph.height;
			y -= increaseAlready * lineHeight;
			consumed += glyph.height;
			ydx++;
		}
	}

	/**
	 * Paints this panel with {@link #clearingColor}
	 */
	protected void clearArea(Batch batch) {
		if (clearingColor != null) {
			batch.end();
			UIUtil.drawRectangle(getRenderer(), getX(), getY(), getWidth(), getHeight(), ShapeType.Filled,
					clearingColor);
			batch.begin();
		}
	}

	protected boolean atMax() {
		return content.size() == maxLines;

	}

	protected String toDraw(IColoredString<T> ics, int ydx) {
		return applyMarkup(transform(ics, ydx));
	}

	protected String applyMarkup(IColoredString<T> ics) {
		if (ics == null)
			return null;
		else
			return markup == null ? ics.toString() : ics.presentWithMarkup(markup);
	}

	/**
	 * If you want to grey out "older" messages, you would do it in this method,
	 * when {@code ydx > 0} (using an {@link squidpony.IColorCenter} maybe ?).
	 * 
	 * @param ics an IColorCenter with the same generic color type as this LinesPanel
	 * @param ydx
	 *            The index of {@code ics} within {@link #content}.
	 * @return A variation of {@code ics}, or {@code ics} itself.
	 */
	protected IColoredString<T> transform(IColoredString<T> ics, int ydx) {
		return ics;
	}

	protected ShapeRenderer getRenderer() {
		if (renderer == null)
			renderer = new ShapeRenderer();
		return renderer;
	}

}
