package jp.riken.brain.ni.samuraigraph.figure.java2d;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

import jp.riken.brain.ni.samuraigraph.base.SGUtility;
import jp.riken.brain.ni.samuraigraph.base.SGUtilityText;
import jp.riken.brain.ni.samuraigraph.figure.SGDrawingElementString;

/**
 * An extended class of string element. This drawing element represent a string
 * with superscript and subscript.
 */
public class SGDrawingElementString2DExtended extends SGDrawingElementString2D {

	/**
	 * The super/subscript font size scaling factor
	 */
	private final float SCRIPT_FONTFACTOR = 1.4f;

	/**
	 * The list of string elements of base characters.
	 */
	private ArrayList mBaseElementList = new ArrayList();

	/**
	 * The list of string elements of subscript.
	 */
	private ArrayList mSubscriptElementList = new ArrayList();

	/**
	 * The list of string elements of superscript.
	 */
	private ArrayList mSuperscriptElementList = new ArrayList();

	/**
	 *  
	 */
	public SGDrawingElementString2DExtended() {
		super();
		this.createStringElements();
	}

	/**
	 *  
	 */
	public SGDrawingElementString2DExtended(final String str) {
		super(str);
		this.createStringElements();
	}

	/**
	 *  
	 */
	public SGDrawingElementString2DExtended(final SGDrawingElementString element) {
		super(element);
		this.createStringElements();
	}

	/**
	 *  
	 */
	public SGDrawingElementString2DExtended(final String str,
			final String fontName, final int fontStyle, final float fontSize) {
		super(str, fontName, fontStyle, fontSize);
		this.createStringElements();
	}

	/**
	 *  
	 */
	public SGDrawingElementString2DExtended(final String str,
			final String fontName, final int fontStyle, final float fontSize,
			final float x, final float y) {
		super(str, fontName, fontStyle, fontSize, x, y);
		this.createStringElements();
	}

	/**
	 *  
	 */
	public void dispose() {
		super.dispose();
		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) list.get(ii);
			el.dispose();
		}
		this.mBaseElementList.clear();
		this.mSuperscriptElementList.clear();
		this.mSubscriptElementList.clear();
		this.mBaseElementList = null;
		this.mSuperscriptElementList = null;
		this.mSubscriptElementList = null;
	}

	/**
	 * Returns whether this string element has superscripts.
	 * 
	 * @return
	 */
	public boolean containsSuperscripts() {
		boolean bEscape = false;
		for( int ii=0; ii< this.mString.length(); ii++ ) {
			char c = this.mString.charAt(ii);
			if( bEscape ) {
				bEscape = false;
			} else {
				if( c == '\\' )
					bEscape = true;
				if( c == '^' )
					return true;
			}
		}
		return false;
	}

	/**
	 * Returns whether this string element has subscripts.
	 * 
	 * @return
	 */
	public boolean containsSubscripts() {
		boolean bEscape = false;
		for( int ii=0; ii< this.mString.length(); ii++ ) {
			char c = this.mString.charAt(ii);
			if( bEscape ) {
				bEscape = false;
			} else {
				if( c == '\\' )
					bEscape = true;
				if( c == '_' )
					return true;
			}
		}
		return false;
	}

	/**
	 * Returns whether this string element has superscripts or subscripts.
	 * 
	 * @return
	 */
	public boolean containsSubscriptsOrSuperscripts() {
		return (this.containsSubscripts() | this.containsSuperscripts());
	}

	/**
	 * Returns a list of all string elements without distinction of base
	 * character, subscript or superscript.
	 * 
	 * @return
	 */
	protected ArrayList getAllStringElement() {
		ArrayList list = new ArrayList();

		if (this.mBaseElementList != null) {
			list.addAll(this.mBaseElementList);
		}

		if (this.mSubscriptElementList != null) {
			list.addAll(this.mSubscriptElementList);
		}

		if (this.mSuperscriptElementList != null) {
			list.addAll(this.mSuperscriptElementList);
		}

		for (int ii = list.size() - 1; ii >= 0; ii--) {
			Object obj = list.get(ii);
			if (obj == null) {
				list.remove(ii);
			}
		}

		return list;
	}

	/**
	 *  
	 */
	public boolean setMagnification(final float mag) {
		super.setMagnification(mag);

		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) list.get(ii);
			el.setMagnification(mag);
		}

		this.updateLocation();

		return true;
	}

	/**
	 *  
	 */
	public boolean setColor(final ArrayList colorList) {
		super.setColor(colorList);

		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString el = (SGDrawingElementString) list.get(ii);
			el.setColor(colorList);
		}
		return true;
	}

	/**
	 *  
	 */
	public boolean setColor(final Color color) {
		super.setColor(color);

		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString el = (SGDrawingElementString) list.get(ii);
			el.setColor(color);
		}
		return true;
	}

	/**
	 * 
	 * @return
	 */
	public boolean addColor(final Color color) {
		super.addColor(color);

		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString el = (SGDrawingElementString) list.get(ii);
			el.addColor(color);
		}
		return true;
	}

	/**
	 *  
	 */
	public boolean setLocation(final float x, final float y) {
		final float dx = x - this.getX();
		final float dy = y - this.getY();

		super.setLocation(x, y);

		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString2D el = 
				(SGDrawingElementString2D) list.get(ii);
			el.translate(dx, dy);
		}

		return true;
	}

	/**
	 *  
	 */
	public boolean setFont(final String name, final int style, final float size) {
		super.setFont(name, style, size);

		if (this.mBaseElementList == null) {
			return true;
		}

		Font f = new Font(name, style, (int) size);
		Font f2 = new Font(name, style, (int) (size / SCRIPT_FONTFACTOR));

		ArrayList bList = this.mBaseElementList;
		for (int ii = 0; ii < bList.size(); ii++) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) bList.get(ii);
			el.setFont(f.getFamily(), f.getStyle(), f.getSize());
		}

		ArrayList subList = this.mSubscriptElementList;
		for (int ii = 0; ii < subList.size(); ii++) {
			SGDrawingElementString2DExtended el =
				(SGDrawingElementString2DExtended) subList.get(ii);
			if (el != null) {
				el.setFont(f2.getFamily(), f2.getStyle(), f2.getSize());
			}
		}

		ArrayList superList = this.mSuperscriptElementList;
		for (int ii = 0; ii < superList.size(); ii++) {
			SGDrawingElementString2DExtended el =
				(SGDrawingElementString2DExtended) superList.get(ii);
			if (el != null) {
				el.setFont(f2.getFamily(), f2.getStyle(), f2.getSize());
			}
		}
		this.updateLocation();

		return true;
	}

	/**
	 *  
	 */
	public boolean setString(final String str) {
		boolean ret = super.setString(str);
		this.createStringElements();
		if(! ret )	return ret;
		this.updateLocation();
		return true;
	}

	/**
	 *  
	 */
	public boolean setAngle(final float angle) {
		if( !super.setAngle(angle) )
			return false;;
		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) list.get(ii);
			el.setAngle(angle);
		}
		this.updateLocation();
		return true;
	}


	/**
	 * 
	 * @return
	 */
	private boolean createStringElementsDirectly() {
		SGDrawingElementString2D el = new SGDrawingElementString2D( SGUtilityText.unescapeString( this.mString ) );
		this.mBaseElementList.add(el);
		this.mSubscriptElementList.add(null);
		this.mSuperscriptElementList.add(null);
		this.setPropertiesToAllStringElements();

		return true;
	}

	/**
	 * Create all string elements. This method is called when the text is set or
	 * changed.
	 * 
	 * @return
	 */
	private boolean createStringElements() {

		// clear all lists
		this.mBaseElementList.clear();
		this.mSubscriptElementList.clear();
		this.mSuperscriptElementList.clear();

		// if the string does not contain subscripts nor superscripts,
		// set the string to the base element
		if (this.containsSubscriptsOrSuperscripts() == false) {
			return this.createStringElementsDirectly();
		}

		// get information of the input line
		String line = this.getString();
		ArrayList baseList = new ArrayList();
		ArrayList superList = new ArrayList();
		ArrayList subList = new ArrayList();
		if (!SGUtilityText.getSubscriptAndSuperscriptInfo(line, baseList,
				superList, subList)) {
			return this.createStringElementsDirectly();
		}

		//
		for (int ii = 0; ii < baseList.size(); ii++) {
			// base characters
			{
				// create a string
				String str = (String) baseList.get(ii);
				SGDrawingElementString2D el = new SGDrawingElementString2D(str);
				this.mBaseElementList.add(el);
			}

			// subscript characters
			Object sb = subList.get(ii);
			if (sb != null) {
				String str = (String) sb;
				SGDrawingElementString2DExtended el = new SGDrawingElementString2DExtended(str);
				this.mSubscriptElementList.add(el);
			} else {
				this.mSubscriptElementList.add(null);
			}

			// superscript characters
			Object sp = superList.get(ii);
			if (sp != null) {
				String str = (String) sp;
				SGDrawingElementString2DExtended el = new SGDrawingElementString2DExtended(str);
				this.mSuperscriptElementList.add(el);
			} else {
				this.mSuperscriptElementList.add(null);
			}

		}

		this.setPropertiesToAllStringElements();

		return true;
	}

	/**
	 * Set properties to the base, superscript and subscript elements.
	 * 
	 * @return
	 */
	private boolean setPropertiesToAllStringElements() {
		final float mag = this.getMagnification();
		final List colorList = this.getColorList();
		final float size = this.getFontSize();
		final String name = this.getFontName();
		final int style = this.getFontStyle();
		final float angle = this.getAngle();

		ArrayList baseList = this.mBaseElementList;
		ArrayList subList = this.mSubscriptElementList;
		ArrayList superList = this.mSuperscriptElementList;
		for (int ii = 0; ii < baseList.size(); ii++) {
			// base characters
			{
				SGDrawingElementString2D el =
					(SGDrawingElementString2D) baseList.get(ii);
				el.setMagnification(mag);
				el.setColor(colorList);
				el.setFont(name, style, size);
				el.setAngle(angle);
			}

			// subscript and superscript characters
			SGDrawingElementString2DExtended subscript =
				(SGDrawingElementString2DExtended) subList.get(ii);
			if (subscript != null) {
				this.setSubAndSuper(subscript, mag, name, style, size, angle,
						colorList);
			}

			SGDrawingElementString2DExtended superscript =
				(SGDrawingElementString2DExtended) superList.get(ii);
			if (superscript != null) {
				this.setSubAndSuper(superscript, mag, name, style, size, angle,
						colorList);
			}
		}

		return true;
	}

	private void setSubAndSuper(final SGDrawingElementString2DExtended el,
			final float mag, final String name, final int style,
			final float size, final float angle, final List colorList) {
		el.setMagnification(mag);
		el.setColor(colorList);
		el.setFont(name, style, size / SCRIPT_FONTFACTOR);
		el.setAngle(angle);
		el.setPropertiesToAllStringElements();
	}

	/**
	 * get ascent
	 */
	protected float getAscent() {
		final float vy = this.getBaseVisualY();
		final float harfleading = this.getLeading() / 2.0f;
		final float offset = this.getBaseStrikethroughOffset();
		final float middle = -vy + offset;
		float ascent = 0.0f;
		float dhsuper = 0.0f;
		ArrayList superList = this.mSuperscriptElementList;
		for (int ii = 0; ii < superList.size(); ii++) {
			Object sp = superList.get(ii);
			if (sp != null) {
				SGDrawingElementString2DExtended el = (SGDrawingElementString2DExtended) sp;
				// get a bounding box
				Rectangle2D stringRect = el.getStringRect();
				float dysuper = (float) - stringRect.getHeight() + middle
						- harfleading;
				dhsuper = (dhsuper < dysuper) ? dhsuper : dysuper;
			}
		}
		ascent = -vy - dhsuper;
		return ascent;
	}
	
	/**
	 * get descent
	 */
	protected float getDescent() {
		final float as = this.getAscent();
		final float descent = (float)this.getStringRect().getHeight() - as;
		return descent;
	}
	
	/**
	 * get leading, maybe this method can replace with method of super class.
	 */
	protected float getLeading() {
		float leading = 0.0f;
		ArrayList baseList = this.mBaseElementList;
		if (baseList.size() != 0) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) baseList.get(0);
			leading = el.getLeading();
		}
		return leading;
	}

	/**
	 * get Y-location of baselist strings
	 */
	private float getBaseVisualY() {
		float ascent = 0.0f;
		ArrayList baseList = this.mBaseElementList;
		// calculate base line height (ascent)
		for (int ii = 0; ii < baseList.size(); ii++) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) baseList.get(ii);
			float vy = (float) el.getStringRect().getY();
			ascent = (ascent < -vy) ? -vy : ascent;
		}
		return -ascent;
	}

	/**
	 * get center line height
	 */
	private float getBaseStrikethroughOffset() {
		float offset = 0.0f;
		ArrayList baseList = this.mBaseElementList;
		// calculate middle line height
		if (baseList.size() != 0) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) baseList.get(0);
			offset = el.getStrikethroughOffset();
		}
		return offset;
	}

	/**
	 *  
	 */
	public Rectangle2D getStringRect() {
		//System.out.println("<< SGDrawingElementString2D::getStringRect >>");

		//
		float width = 0.0f;
		float height = 0.0f;
		float dhsuper = 0.0f;
		float dhsub = 0.0f;
		float lpadding = 0.0f;
		final float vy = this.getBaseVisualY();
		final float offset = this.getBaseStrikethroughOffset();
		final float middle = -vy + offset;
		final float harfleading = this.getLeading() / 2.0f;

		ArrayList baseList = this.mBaseElementList;
		ArrayList subList = this.mSubscriptElementList;
		ArrayList superList = this.mSuperscriptElementList;

		for (int ii = 0; ii < baseList.size(); ii++) {
			// characters
			{
				SGDrawingElementString2D el = 
					(SGDrawingElementString2D) baseList.get(ii);

				// get a bounding box
				Rectangle2D stringRect = el.getStringRect();
				float basey = (float) stringRect.getY();
				width += el.getAdvance();

				float tmpheight = -vy + basey + (float) stringRect.getHeight();
				height = (height < tmpheight) ? tmpheight : height;
				if (ii == 0)
					lpadding = (float) stringRect.getX();

			}

			// superscript characters
			Object sp = superList.get(ii);
			float widthSuper = 0.0f;
			if (sp != null) {
				SGDrawingElementString2DExtended el = (SGDrawingElementString2DExtended) sp;

				// get a bounding box
				Rectangle2D stringRect = el.getStringRect();
				float dxsuper = (float) stringRect.getX();
				float dysuper = (float) -stringRect.getHeight() + middle
						- harfleading;
				dhsuper = (dhsuper < dysuper) ? dhsuper : dysuper;
				widthSuper += (float) stringRect.getWidth() + dxsuper;
			}

			// subscript characters
			Object sb = subList.get(ii);
			float widthSub = 0.0f;
			if (sb != null) {
				SGDrawingElementString2DExtended el = (SGDrawingElementString2DExtended) sb;

				// get a bounding box
				Rectangle2D stringRect = el.getStringRect();
				float dxsub = (float) stringRect.getX();
				float dysub = (float) stringRect.getHeight() + middle + harfleading;
				dhsub = (dhsub > dysub) ? dhsub : dysub;
				widthSub += (float) stringRect.getWidth() + dxsub;
			}

			if (sp != null || sb != null) {
				width += Math.max(widthSuper, widthSub);
			}

		}
		dhsub = (dhsub > height) ? dhsub - height : 0.0f;
		height = height - dhsuper + dhsub;
		// create a rectangle
		Rectangle2D rect = new Rectangle2D.Float(lpadding, -(-vy - dhsuper),
				width, height);

		return rect;

	}

	/**
	 *  
	 */
	public Rectangle2D getElementBounds() {
		ArrayList rectList = new ArrayList();

		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) list.get(ii);
			rectList.add(el.getElementBounds());
		}

		// create a rectangle
		Rectangle2D rect = SGUtility.createUnion(rectList);

		return rect;
	}

	/**
	 * Update the location of the base, superscript and subscript elements.
	 * 
	 * @return
	 */
	protected boolean updateLocation() {

		if( this.mFont == null )
			return false;
		// angle
		final float angle = this.getAngle();
		final float cv = (float) Math.cos(angle);
		final float sv = (float) Math.sin(angle);

		// location
		final float x = this.getX();
		final float y = this.getY();
		final float ascent = this.getAscent();
		final float offset = this.getBaseStrikethroughOffset();
		final float middle = ascent + offset;
		final float harfleading = this.getLeading() / 2.0f;

		ArrayList baseList = this.mBaseElementList;
		ArrayList subList = this.mSubscriptElementList;
		ArrayList superList = this.mSuperscriptElementList;

		float dx = 0.0f;
		for (int ii = 0; ii < baseList.size(); ii++) {

			// characters
			{
				SGDrawingElementString2D el = 
					(SGDrawingElementString2D) baseList.get(ii);
				final Rectangle2D stringRect = el.getStringRect();
				final float _x = (float)stringRect.getX();
				final float _y = (float)stringRect.getY();
				final float as = (float)el.getAscent();
				final float dxbase = _x;
				final float dybase = ascent + _y;
//				final float nx = x + (dx + dxbase) * cv + dybase * sv;
//				final float ny = y - (dx + dxbase) * sv + dybase * cv;
				final float nx = x + dx * cv + dybase * sv;
				final float ny = y - dx * sv + dybase * cv;
				el.setLocation( nx, ny );
//				System.out.println("el=" + el.getString() + ", as=" + ascent + ", off=" + offset + ", x=" + x + ", y=" + y);
//				System.out.println("as=" + as + "+ _x=" + _x + ", _y=" + _y + ", nx=" + nx + ", ny=" + ny +"\n" );
				// update the location
				dx += el.getAdvance();
//				dx += (float) stringRect.getWidth() + dxbase;
			}

			// superscript characters
			Object sp = superList.get(ii);
			float widthSuper = 0.0f;
			if (sp != null) {
				SGDrawingElementString2DExtended el =
					(SGDrawingElementString2DExtended) sp;
				final Rectangle2D stringRect = el.getStringRect();
				final float _x = (float) stringRect.getX();
				final float _h = (float) stringRect.getHeight();
				final float dxsuper = _x;
				final float dysuper = -_h + middle - harfleading;
//				final float nx = x + (dx + dxsuper) * cv + dysuper * sv;
//				final float ny = y - (dx + dxsuper) * sv + dysuper * cv;
				final float nx = x + dx * cv + dysuper * sv;
 				final float ny = y - dx * sv + dysuper * cv;
				// update the location
				el.setLocation( nx, ny );

				// update the location of subscript and superscript
				el.updateLocation();
				widthSuper += (float) stringRect.getWidth() + dxsuper;
			}

			// subscript characters
			Object sb = subList.get(ii);
			float widthSub = 0.0f;
			if (sb != null) {
				SGDrawingElementString2DExtended el =
					(SGDrawingElementString2DExtended) sb;
				final Rectangle2D stringRect = el.getStringRect();
				final float _x = (float) stringRect.getX();
				final float dxsub = _x;
				final float dysub = middle + harfleading;
//				final float nx = x + (dx + dxsub) * cv + dysub * sv;
//				final float ny = y - (dx + dxsub) * sv + dysub * cv;
				final float nx = x + dx * cv + dysub * sv;
				final float ny = y - dx * sv + dysub * cv;
				// update the location
				el.setLocation( nx, ny );

				// update the location of subscript and superscript
				el.updateLocation();

				widthSub += (float) stringRect.getWidth() + dxsub;
			}

			if (sp != null | sb != null) {
				dx += Math.max(widthSuper, widthSub);
			}
		}

		return true;
	}

	/**
	 *  
	 */
	public void paintElement(final Graphics2D g2d) {
		if (g2d == null) {
			return;
		}

		if (this.isVisible() == false) {
			return;
		}

		// draw strings
		ArrayList list = this.getAllStringElement();
		for (int ii = 0; ii < list.size(); ii++) {
			SGDrawingElementString2D el =
				(SGDrawingElementString2D) list.get(ii);
			el.paintElement(g2d);
		}
	}

}
