/**
 *
 */
package jp.sourceforge.nicoro;

import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.regex.Matcher;

import jp.gr.java_conf.shiseissi.commonlib.ThreadLocalStringBuilder;
import jp.gr.java_conf.shiseissi.commonlib.ThreadSoftReference;
import jp.sourceforge.nicoro.nicoscript.NicoScriptReplace;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.TextUtils;

import static jp.sourceforge.nicoro.Log.LOG_TAG;
import static jp.sourceforge.nicoro.PlayerConstants.REAL_PLAYER_WIDTH_PX_4_3;
import static jp.sourceforge.nicoro.PlayerConstants.REAL_PLAYER_HEIGHT_PX_4_3;
import static jp.sourceforge.nicoro.PlayerConstants.REAL_PLAYER_WIDTH_PX_16_9;

/**
 * コメント (chat)
 */
public class MessageChat {
//	private static final boolean DEBUG_LOGD = Release.IS_DEBUG & true;
    private static final boolean DEBUG_DRAW = Release.IS_DEBUG & false;

	// コメントカラー
	public static final int COLOR_WHITE = 0xFFFFFFFF;
	public static final int COLOR_RED = 0xFFFF0000;
	public static final int COLOR_PINK = 0xFFFF8080;
	public static final int COLOR_ORANGE = 0xFFFFCC00;
	public static final int COLOR_YELLOW = 0xFFFFFF00;
	public static final int COLOR_GREEN = 0xFF00FF00;
	public static final int COLOR_CYAN = 0xFF00FFFF;
	public static final int COLOR_BLUE = 0xFF0000FF;
	public static final int COLOR_PURPLE = 0xFFC000FF;
	public static final int COLOR_BLACK = 0xFF000000;
	public static final int COLOR_NICONICOWRITE = 0xFFCCCC99;
	public static final int COLOR_WHITE2 = COLOR_NICONICOWRITE;
	public static final int COLOR_TRUERED = 0xFFCC0033;
	public static final int COLOR_RED2 = COLOR_TRUERED;
	public static final int COLOR_PASSIONORANGE = 0xFFFF6600;
	public static final int COLOR_ORANGE2 = COLOR_PASSIONORANGE;
	public static final int COLOR_MADYELLOW = 0xFF999900;
	public static final int COLOR_YELLOW2 = COLOR_MADYELLOW;
	public static final int COLOR_ELEMENTALGREEN = 0xFF00CC66;
	public static final int COLOR_GREEN2 = COLOR_ELEMENTALGREEN;
	public static final int COLOR_MARINEBLUE = 0xFF33FFFC;
	public static final int COLOR_BLUE2 = COLOR_MARINEBLUE;
	public static final int COLOR_NOBLEVIOLET = 0xFF6633CC;
	public static final int COLOR_PURPLE2 = COLOR_NOBLEVIOLET;

	public static final int FONTSIZE_TYPE_BIG = 0;
	public static final int FONTSIZE_TYPE_MEDIUM = 1;
	public static final int FONTSIZE_TYPE_SMALL = 2;

	/** コメント フォントサイズ big 通常表示 */
	public static final int FONTSIZE_PX_BIG = 39;
    /** コメント フォントサイズ （指定無し） 通常表示 */
	public static final int FONTSIZE_PX_MEDIUM = 24;
    /** コメント フォントサイズ small 通常表示 */
	public static final int FONTSIZE_PX_SMALL = 15;

    /** コメント フォントサイズ big 縮小表示 */
    public static final int FONTSIZE_PX_SMALL_BIG = 20;
    /** コメント フォントサイズ （指定無し） 縮小表示 */
    public static final int FONTSIZE_PX_SMALL_MEDIUM = 13;
    /** コメント フォントサイズ small 縮小表示 */
    public static final int FONTSIZE_PX_SMALL_SMALL = 8;

    /** 改行でない次のコメントとの間隔 通常表示 */
    public static final int MARGIN_NEXT = 5;
    /** 改行でない次のコメントとの間隔 縮小表示 */
    public static final int MARGIN_NEXT_SMALL = 3;

    /** コメント 行高さ big 通常表示 */
    public static final int LINEHEIGHT_PX_BIG = 45;
    /** コメント 行高さ （指定無し） 通常表示 */
    public static final int LINEHEIGHT_PX_MEDIUM = 29;
    /** コメント 行高さ small 通常表示 */
    public static final int LINEHEIGHT_PX_SMALL = 18;

    /** コメント 行高さ big 縮小表示 */
    public static final int LINEHEIGHT_PX_SMALL_BIG = 24;
    /** コメント 行高さ （指定無し） 縮小表示 */
    public static final int LINEHEIGHT_PX_SMALL_MEDIUM = 15;
    /** コメント 行高さ small 縮小表示 */
    public static final int LINEHEIGHT_PX_SMALL_SMALL = 10;

	// コメント 位置
	public static final int POS_NAKA = 0;
	public static final int POS_UE = 1;
	public static final int POS_SHITA = 2;

	// コメント 方向
	public static final int DIR_NORMAL = 0;
	public static final int DIR_GYAKU = 1;

	/**
	 * コメント表示時間（10ms単位）
	 */
	public static final int DISPLAY_TIME_VPOS_NAKA = 400;
	public static final int DISPLAY_TIME_VPOS_UE = 300;
	public static final int DISPLAY_TIME_VPOS_SHITA = 300;

//	private static final int TEXT_SIZE = 60 * 2;

	// XMLから設定するデータ
	private String mMail;
	private int mVpos;
	private String mText;
	private int mNo;
	private long mDate;
	private String mUserId;
	private int mScore;

	private ArrayList<String> mSplitText;

	public int line;

	private int mWidth;
	private float mSpeed;

	private int mColor;
	private int mFontSize;
    private int mFontSizeType = -1;
    private int mLineHeight;
	private int mPos;
	private int mDir;
    private boolean mInvisible = false;
	private boolean mIsDefaultColor;
	private boolean mIsDefaultFontSize;
	private boolean mIsDefaultPos;
	private boolean mIsSaveLocal = false;
	private int mMarginNext = MARGIN_NEXT;
	/**
	 * コメントの表示時間、ニコスクリプトの実行時間
	 */
	protected int mTime = -1;
	private float mX;
	private float mY;
	private int mVirtualY;
	private int mHeight = 0;

	// 逆再生のため位置情報計算変更
	int mLastVpos;

	private String mDrawnText;

	private boolean mMySendComment;

	/**
	 * NGコマンドでフィルタ後の {@link #mMail}
	 */
	private String mFilteredMail;

	// 改行調整
	private static String PATTERN_REMOVE_EDGE_LF = "^\\n+(.*)\\n+$";
	private static String PATTERN_ADJUST_STRAIGHT_LF = "\\n{4,}";
	private static ThreadSoftReference<Matcher> mMatcherRemoveEdgeLF =
	    new ThreadSoftReference<Matcher>();
	private static ThreadSoftReference<Matcher> mMatcherAdjustStraightLF =
	    new ThreadSoftReference<Matcher>();

	public MessageChat(String mail, int vpos, int no,
            long date, String userId, int score) {
		mMail = mail;
		mFilteredMail = mail;
		setVpos(vpos);
		mNo = no;
		mDate = date;
		mUserId = userId;
		mScore = score;

		mIsSaveLocal = false;
        mDir = DIR_NORMAL;
        setMail(mFilteredMail);
	}

	/**
	 * mail によって色やサイズ等の設定を更新
	 * @param mail
	 */
	private void setMail(String mail) {
		boolean setColor = false;
		boolean setFontSize = false;
		boolean setPos = false;
		if (mail != null) {
		    boolean isFork = isFork();
			String[] mails = mail.split(" ");
			for (String s : mails) {
				if (s.startsWith("#")) {
					// 色
					if (!setColor) {
						if (s.length() >= 1) {
							try {
								mColor = 0xFF000000 | Integer.parseInt(s.substring(1), 16);
								setColor = true;
							} catch (NumberFormatException e) {
								Log.d(LOG_TAG, Log.buf().append("Invalid color value=")
										.append(s).toString());
								assert setColor == false;
							}
						} else {
							Log.d(LOG_TAG, Log.buf().append("Invalid color value=")
									.append(s).toString());
						}
					}
				} else if (s.equals("white")) {
					if (!setColor) {
						mColor = COLOR_WHITE;
						setColor = true;
					}
				} else if (s.equals("red")) {
					if (!setColor) {
						mColor = COLOR_RED;
						setColor = true;
					}
				} else if (s.equals("pink")) {
					if (!setColor) {
						mColor = COLOR_PINK;
						setColor = true;
					}
				} else if (s.equals("orange")) {
					if (!setColor) {
						mColor = COLOR_ORANGE;
						setColor = true;
					}
				} else if (s.equals("yellow")) {
					if (!setColor) {
						mColor = COLOR_YELLOW;
						setColor = true;
					}
				} else if (s.equals("green")) {
					if (!setColor) {
						mColor = COLOR_GREEN;
						setColor = true;
					}
				} else if (s.equals("cyan")) {
					if (!setColor) {
						mColor = COLOR_CYAN;
						setColor = true;
					}
				} else if (s.equals("blue")) {
					if (!setColor) {
						mColor = COLOR_BLUE;
						setColor = true;
					}
				} else if (s.equals("purple")) {
					if (!setColor) {
						mColor = COLOR_PURPLE;
						setColor = true;
					}
				} else if (s.equals("black")) {
					if (!setColor) {
						mColor = COLOR_BLACK;
						setColor = true;
					}
				} else if (s.equals("niconicowhite")) {
					if (!setColor) {
						mColor = COLOR_NICONICOWRITE;
						setColor = true;
					}
				} else if (s.equals("white2")) {
					if (!setColor) {
						mColor = COLOR_WHITE2;
						setColor = true;
					}
				} else if (s.equals("truered")) {
					if (!setColor) {
						mColor = COLOR_TRUERED;
						setColor = true;
					}
				} else if (s.equals("red")) {
					if (!setColor) {
						mColor = COLOR_RED2;
						setColor = true;
					}
				} else if (s.equals("passionorange")) {
					if (!setColor) {
						mColor = COLOR_PASSIONORANGE;
						setColor = true;
					}
				} else if (s.equals("orange2")) {
					if (!setColor) {
						mColor = COLOR_ORANGE2;
						setColor = true;
					}
				} else if (s.equals("madyellow")) {
					if (!setColor) {
						mColor = COLOR_MADYELLOW;
						setColor = true;
					}
				} else if (s.equals("yellow2")) {
					if (!setColor) {
						mColor = COLOR_YELLOW2;
						setColor = true;
					}
				} else if (s.equals("elementalgreen")) {
					if (!setColor) {
						mColor = COLOR_ELEMENTALGREEN;
						setColor = true;
					}
				} else if (s.equals("green2")) {
					if (!setColor) {
						mColor = COLOR_GREEN2;
						setColor = true;
					}
				} else if (s.equals("marineblue")) {
					if (!setColor) {
						mColor = COLOR_MARINEBLUE;
						setColor = true;
					}
				} else if (s.equals("blue2")) {
					if (!setColor) {
						mColor = COLOR_BLUE2;
						setColor = true;
					}
				} else if (s.equals("nobleviolet")) {
					if (!setColor) {
						mColor = COLOR_NOBLEVIOLET;
						setColor = true;
					}
				} else if (s.equals("purple2")) {
					if (!setColor) {
						mColor = COLOR_PURPLE2;
						setColor = true;
					}
				} else if (s.equals("big")) {
					if (!setFontSize) {
					    mFontSizeType = FONTSIZE_TYPE_BIG;
						mFontSize = FONTSIZE_PX_BIG;
						mLineHeight = LINEHEIGHT_PX_BIG;
						setFontSize = true;
					}
				} else if (s.equals("medium")) {
					if (!setFontSize) {
                        mFontSizeType = FONTSIZE_TYPE_MEDIUM;
						mFontSize = FONTSIZE_PX_MEDIUM;
                        mLineHeight = LINEHEIGHT_PX_MEDIUM;
						setFontSize = true;
					}
				} else if (s.equals("small")) {
					if (!setFontSize) {
                        mFontSizeType = FONTSIZE_TYPE_SMALL;
						mFontSize = FONTSIZE_PX_SMALL;
                        mLineHeight = LINEHEIGHT_PX_SMALL;
						setFontSize = true;
					}
				} else if (s.equals("naka")) {
					if (!setPos) {
						mPos = POS_NAKA;
						setPos = true;
					}
				} else if (s.equals("ue")) {
					if (!setPos) {
						mPos = POS_UE;
						setPos = true;
					}
				} else if (s.equals("shita")) {
					if (!setPos) {
						mPos = POS_SHITA;
						setPos = true;
					}
				} else if (s.equals("invisible")) {
					mInvisible = true;
				} else if (s.equals("from_button")) {
					// TODO: 現状無視
				} else if (s.equals("is_button")) {
					// TODO: 現状無視
				} else if (s.equals("184")) {
					// 無視
				} else if (s.equals("sage")) {
					// コメント一覧表示はないので無視
				} else if (s.equals("docomo")) {
                    // TODO: 現状無視
                } else if (s.equals("full")) {
                    // TODO: 現状無視
                } else if (s.equals("patissier")) {
                    // TODO: これ今のプレイヤー側で反映できるか分からないので無視
				} else if (isFork) {
					if (s.startsWith("@") || s.startsWith("＠")) {
						if (s.length() > 1) {
							if (Character.isDigit(s.charAt(1))) {
								try {
									mTime = Integer.parseInt(s.substring(1));
								} catch (NumberFormatException e) {
									Log.d(LOG_TAG, Log.buf().append("Invalid time value=")
											.append(s).toString());
								}
							} else if (s.equals("@button")) {
								// TODO: 現状無視
							} else {
								Log.d(LOG_TAG, Log.buf().append("Unknown mail=")
										.append(s).toString());
							}
						} else {
							Log.d(LOG_TAG, Log.buf().append("Invalid @ value=")
									.append(s).toString());
						}
					} else if (s.equals("migi")) {
						// TODO: 現状無視
					} else if (s.equals("hidari")) {
						// TODO: 現状無視
					} else if (s.equals("hidden")) {
						// TODO: 現状無視
	                } else if (s.equals("local")) {
	                    mIsSaveLocal = true;
					} else {
						Log.d(LOG_TAG, Log.buf().append("Unknown mail=")
								.append(s).toString());
					}
				} else {
					Log.d(LOG_TAG, Log.buf().append("Unknown mail=")
							.append(s).toString());
				}
			}
		}

		if (!setColor) {
			mColor = COLOR_WHITE;
		}
		mIsDefaultColor = !setColor;
		if (!setFontSize) {
            mFontSizeType = FONTSIZE_TYPE_MEDIUM;
			mFontSize = FONTSIZE_PX_MEDIUM;
            mLineHeight = LINEHEIGHT_PX_MEDIUM;
		}
		mIsDefaultFontSize = !setFontSize;
		if (!setPos) {
			mPos = POS_NAKA;
		}
		mIsDefaultPos = !setPos;
	}

	private static ThreadLocal<SoftReference<StringBuilder>> sStringBuilder =
		new ThreadLocalStringBuilder();

	@Override
	public String toString() {
	    SoftReference<StringBuilder> ref = sStringBuilder.get();
	    StringBuilder sb = (ref == null) ? null : ref.get();
	    if (sb == null) {
	        sb = new StringBuilder(ThreadLocalStringBuilder.DEFAULT_CAPACITY_SIZE);
	        sStringBuilder.set(new SoftReference<StringBuilder>(sb));
	    } else {
	        sb.setLength(0);
	    }
		return sb.append("mail=").append(mMail)
			.append(" vpos=").append(mVpos)
			.append(" text=").append(mText)
			.append(" no=").append(mNo)
			.append(" date=").append(mDate)
			.append(" ").append(new Date(mDate * 1000L).toLocaleString())
			.append(" user_id=").append(mUserId)
            .append(" score=").append(mScore)
			.toString();
	}

	@Override
	public boolean equals(Object o) {
	    if (o == this) {
	        return true;
	    }
	    if (!(o instanceof MessageChat)) {
	        return false;
	    }
	    MessageChat obj = (MessageChat) o;
	    return (mNo == obj.mNo && mDate == obj.mDate && mVpos == obj.mVpos
                && TextUtils.equals(mUserId, obj.mUserId)
	            && TextUtils.equals(mMail, obj.mMail)
	            && TextUtils.equals(mText, obj.mText));
	}

	@Override
	public int hashCode() {
	    int result = 17;
	    result = 31 * result + mVpos;
	    result = 31 * result + mNo;
        result = 31 * result + (int) mDate;
	    if (mMail != null) {
	        result = 31 * result + mMail.hashCode();
	    }
	    if (mText != null) {
	        result = 31 * result + mText.hashCode();
	    }
	    if (mUserId != null) {
	        result = 31 * result + mUserId.hashCode();
	    }
	    return result;
	}

//	static private SoftReference<float[]> sComputeWidthBuffer = null;
//	private static float[] getComputeWidthBuffer(final int textLength) {
//		float[] buffer;
//		if (sComputeWidthBuffer == null) {
//			buffer = new float[Math.max(TEXT_SIZE, textLength)];
//			sComputeWidthBuffer = new SoftReference<float[]>(buffer);
//		} else {
//			buffer = sComputeWidthBuffer.get();
//			if (buffer == null) {
//				buffer = new float[Math.max(TEXT_SIZE, textLength)];
//				sComputeWidthBuffer = new SoftReference<float[]>(buffer);
//			} else {
//				if (buffer.length < textLength) {
//					buffer = new float[textLength];
//					sComputeWidthBuffer = new SoftReference<float[]>(buffer);
//				}
//			}
//		}
//		return buffer;
//	}

	public void computeWidth(Paint paint) {
	    String text = getDrawnText();
		if (text == null) {
			mWidth = 0;
		} else {
			paint.setTextSize(mFontSize);
			if (mSplitText == null) {
	//			float[] widths = getComputeWidthBuffer(mText.length());
	//			int num = paint.getTextWidths(mText, widths);
	//			float w = 0.0f;
	//			for (int i = 0; i < num; ++i) {
	//				w += widths[i];
	//			}
	//			mWidth = (int) w;

				mWidth = (int) paint.measureText(text);
			} else {
				float maxWidth = 0.0f;
				for (String t : mSplitText) {
					maxWidth = Math.max(paint.measureText(t), maxWidth);
				}
				mWidth = (int) maxWidth;
			}
			if (mPos == POS_SHITA || mPos == POS_UE) {
				if (mWidth > REAL_PLAYER_WIDTH_PX_4_3) {
					mFontSize = mFontSize * REAL_PLAYER_WIDTH_PX_4_3 / mWidth;
                    mLineHeight = mLineHeight * REAL_PLAYER_WIDTH_PX_4_3 / mWidth;
					mWidth = REAL_PLAYER_WIDTH_PX_4_3;
				}
			}
		}
	}

	public void computeSpeed() {
//		switch (mPos) {
//		case POS_NAKA:
//			mSpeed = (float) -(REAL_PLAYER_WIDTH_PX_4_3 + mWidth)
//				/ (float) DISPLAY_TIME_VPOS_NAKA;
//			break;
//		case POS_SHITA:
//		case POS_UE:
//			mSpeed = 0;
//			break;
//		default:
//			assert false;
//			mSpeed = (float) -(REAL_PLAYER_WIDTH_PX_4_3 + mWidth)
//				/ (float) DISPLAY_TIME_VPOS_NAKA;
//			break;
//		}

		if (mPos == POS_SHITA || mPos == POS_UE) {
			mSpeed = 0;
		} else {
			assert mPos == POS_NAKA;
			mSpeed = (float) -(REAL_PLAYER_WIDTH_PX_4_3 + mWidth)
				/ (float) DISPLAY_TIME_VPOS_NAKA;
//			if (mDir == DIR_GYAKU) {
//				mSpeed = -mSpeed;
//			} else {
//				assert mDir == DIR_NORMAL;
//			}
		}
	}

	public void computeLineHeight(Paint paint) {
		paint.setTextSize(mFontSize);
//		mHeight = paint.getFontMetricsInt(null);
		mHeight = mLineHeight;
	}

	public int getWidth() {
		return mWidth;
	}

	public int getColor() {
		return mColor;
	}
	public int getShadowColor() {
		if (mColor == COLOR_BLACK) {
			return 0xFFD0D0D0;
		} else {
			return 0xFF000000;
		}
	}

	public int getFontSize() {
		return mFontSize;
	}

	public int getFontSizeType() {
	    return mFontSizeType;
	}

	public float getSpeed() {
		return mSpeed;
	}

	public int getPos() {
		return mPos;
	}

	public int getLineHeight() {
	    return mLineHeight;
	}

	public int getStartX() {
//		switch (mPos) {
//		case POS_NAKA:
//			return REAL_PLAYER_WIDTH_PX_4_3;
//		case POS_SHITA:
//		case POS_UE:
//			return (REAL_PLAYER_WIDTH_PX_4_3 - mWidth) / 2;
//		default:
//			assert false;
//			return REAL_PLAYER_WIDTH_PX_4_3;
//		}

		if (mPos == POS_SHITA || mPos == POS_UE) {
			return (REAL_PLAYER_WIDTH_PX_4_3 - mWidth) / 2;
		} else {
			assert mPos == POS_NAKA;
			if (mDir == DIR_GYAKU) {
				return -mWidth;
			} else {
				assert mDir == DIR_NORMAL;
				return REAL_PLAYER_WIDTH_PX_4_3;
			}
		}
	}

	public float computeX(int vposCurrent) {
//		// TODO: ＠逆のためにはこれも現在地からの相対的な値で計算しないと駄目？
////		float x = (float) getStartX() + mSpeed * (vposCurrent - mVpos);
//		float x = (float) getStartX() + mSpeed * (vposCurrent - mVpos + 100);
//		return x;
		float distance;
		if (mDir == DIR_GYAKU) {
			distance = -mSpeed * (vposCurrent - mVpos + 100);
		} else {
			assert mDir == DIR_NORMAL;
			distance = mSpeed * (vposCurrent - mVpos + 100);
		}
		float x = (float) getStartX() + distance;
		return x;
	}

	public float computeXRelatively(int vposCurrent) {
//		mX += mSpeed * (vposCurrent - mLastVpos);
		float distance;
		if (mDir == DIR_GYAKU) {
			distance = -mSpeed * (vposCurrent - mLastVpos);
		} else {
			assert mDir == DIR_NORMAL;
			distance = mSpeed * (vposCurrent - mLastVpos);
		}
		mX += distance;
		mLastVpos = vposCurrent;
		return mX;
	}

	public void initializeX(int vposCurrent) {
////		mX = (float) getStartX() + mSpeed * (vposCurrent - mVpos);
//		mX = (float) getStartX() + mSpeed * (vposCurrent - mVpos + 100);
		float distance;
		if (mDir == DIR_GYAKU) {
			distance = -mSpeed * (vposCurrent - mVpos + 100);
		} else {
			assert mDir == DIR_NORMAL;
			distance = mSpeed * (vposCurrent - mVpos + 100);
		}
		mX = (float) getStartX() + distance;
		mLastVpos = vposCurrent;
	}

	/**
	 *
	 * @param y nakaまたはueなら上端、shitaなら下端に当たる座標
	 * @param paint
	 * @return 次のChatが表示されるY座標。nakaまたはueなら下端、shitaなら上端の座標
	 */
	public int setY(int y, Paint paint) {
		mVirtualY = y;
		paint.setTextSize(mFontSize);
		switch (mPos) {
		case POS_NAKA:
		case POS_UE:
			mY = y - paint.ascent();
			return mVirtualY + getHeight();
		case POS_SHITA:
			mY = y - paint.descent();
			if (mSplitText != null) {
			    mY -= mHeight * (mSplitText.size() - 1);
			}
            return mVirtualY - getHeight();
		default:
			assert false;
			return y;
		}
	}

	public int getY() {
		return mVirtualY;
	}

	public int getNextY() {
		switch (mPos) {
		case POS_NAKA:
		case POS_UE:
			return mVirtualY + getHeight();
		case POS_SHITA:
		    if (mSplitText == null) {
		        return mVirtualY - getHeight();
		    } else {
		        return mVirtualY - getHeight() * mSplitText.size();
		    }
		default:
			assert false;
			return mVirtualY + getHeight();
		}
	}

	public float getDrawX() {
		return mX;
	}

	public float getDrawY() {
		return mY;
	}

	public int computeShitaNextY(List<MessageChat> chats, Random random) {
		int proposeY = REAL_PLAYER_HEIGHT_PX_4_3;
		for (MessageChat shita : chats) {
			if ((proposeY - getHeight()) >= shita.getY()) {
				break;
			}
			proposeY = shita.getNextY();
		}
		if ((proposeY - getHeight()) < 0) {
			if (getHeight() >= REAL_PLAYER_HEIGHT_PX_4_3) {
				// TODO: 高さを縮める必要（別に必要ない？）
				proposeY = REAL_PLAYER_HEIGHT_PX_4_3;
			} else {
				// ランダム
				proposeY = REAL_PLAYER_HEIGHT_PX_4_3
				- random.nextInt(REAL_PLAYER_HEIGHT_PX_4_3
						- getHeight());
			}
		}
		return proposeY;
	}

	public void addShitaOrder(List<MessageChat> chats) {
		int i = 0;
		for (MessageChat shita : chats) {
			if (getY() < shita.getY()) {
//			if (getNextY() < shita.getNextY()) {
				++i;
			} else {
				break;
			}
		}
		chats.add(i, this);
	}

	public int computeUeNextY(List<MessageChat> chats, Random random) {
		int proposeY = 0;
		for (MessageChat ue : chats) {
			if ((proposeY + getHeight()) <= ue.getY()) {
				break;
			}
			proposeY = ue.getNextY();
		}
		if ((proposeY + getHeight()) > REAL_PLAYER_HEIGHT_PX_4_3) {
			if (getHeight() >= REAL_PLAYER_HEIGHT_PX_4_3) {
				// TODO: 高さを縮める必要（別に必要ない？）
				proposeY = 0;
			} else {
				// ランダム
				proposeY = random.nextInt(REAL_PLAYER_HEIGHT_PX_4_3
						- getHeight());
			}
		}
		return proposeY;
	}

	public void addUeOrder(List<MessageChat> chats) {
		int i = 0;
		for (MessageChat ue : chats) {
			if (getY() > ue.getY()) {
//			if (getNextY() > ue.getNextY()) {
				++i;
			} else {
    			break;
			}
		}
		chats.add(i, this);
	}

	public int computeNakaNextY(int vposCurrent, List<MessageChat> chats, Random random) {
		int proposeY = 0;
		int lastNakaY = proposeY;
		int lastNakaNextY = -1;
		for (MessageChat naka : chats) {
			assert naka.getY() >= lastNakaY;
			assert (naka.getY() == lastNakaY && lastNakaNextY >= 0) ? naka.getNextY() >= lastNakaNextY : true;
			lastNakaY = naka.getY();
			lastNakaNextY = naka.getNextY();

			if ((proposeY + getHeight()) > naka.getY()) {
				if (mDir == DIR_GYAKU) {
					float nakaXLeftStart = naka.computeXRelatively(mVpos);
					float thisXRightStart = computeX(mVpos) + getWidth();
					float nakaXLeftEnd = naka.computeXRelatively(mVpos + DISPLAY_TIME_VPOS_NAKA);
					float thisXRightEnd = computeX(mVpos + DISPLAY_TIME_VPOS_NAKA) + getWidth();
					if (nakaXLeftStart < thisXRightStart
							|| nakaXLeftEnd < thisXRightEnd) {
						// 横方向で衝突
						proposeY = naka.getNextY();
					}
				} else {
					assert mDir == DIR_NORMAL;
//					float nakaXRightStart = naka.computeX(mVpos) + naka.getWidth();
					float nakaXRightStart = naka.computeXRelatively(mVpos) + naka.getWidth();
					float thisXLeftStart = computeX(mVpos);
//					float nakaXRightEnd = naka.computeX(mVpos + DISPLAY_TIME_VPOS_NAKA) + naka.getWidth();
					float nakaXRightEnd = naka.computeXRelatively(mVpos + DISPLAY_TIME_VPOS_NAKA) + naka.getWidth();
					float thisXLeftEnd = computeX(mVpos + DISPLAY_TIME_VPOS_NAKA);
					if (nakaXRightStart > thisXLeftStart
							|| nakaXRightEnd > thisXLeftEnd) {
						// 横方向で衝突
						proposeY = naka.getNextY();
					}
				}
			}
		}
		if ((proposeY + getHeight()) > REAL_PLAYER_HEIGHT_PX_4_3) {
			if (getHeight() >= REAL_PLAYER_HEIGHT_PX_4_3) {
				// TODO: 高さを縮める必要（別に必要ない？）
				proposeY = 0;
			} else {
				// ランダム
				proposeY = random.nextInt(REAL_PLAYER_HEIGHT_PX_4_3
						- getHeight());
			}
		}
		return proposeY;
	}

	public void addNakaOrder(List<MessageChat> chats) {
		int i = 0;
		for (MessageChat naka : chats) {
			if (getY() > naka.getY()) {
				++i;
			} else if (getY() == naka.getY()) {
				if (getNextY() > naka.getNextY()) {
					++i;
				} else {
					break;
				}
			} else {
    			break;
			}
		}
		chats.add(i, this);
	}

	public void setText(String text) {
		mText = text;
		if (text == null) {
			return;
		}
		if (text.length() == 0) {
			mText = null;
			return;
		}

		// テキスト先頭と末尾の改行文字削除
		Matcher matcherRemoveEdgeLF = mMatcherRemoveEdgeLF.getValue();
		boolean createMatcherRemoveEdgeLF = (matcherRemoveEdgeLF == null);
		matcherRemoveEdgeLF = Util.getMatcher(
				matcherRemoveEdgeLF, PATTERN_REMOVE_EDGE_LF, text);
		if (createMatcherRemoveEdgeLF) {
			mMatcherRemoveEdgeLF.setValue(matcherRemoveEdgeLF);
		}
		if (matcherRemoveEdgeLF.find()) {
			text = matcherRemoveEdgeLF.group(0);
		}

		// ３行以上の空の改行を２行に
		Matcher matcherAdjustStraightLF = mMatcherAdjustStraightLF.getValue();
		boolean createMatcherAdjustStraightLF = (matcherAdjustStraightLF == null);
		matcherAdjustStraightLF = Util.getMatcher(
				matcherAdjustStraightLF, PATTERN_ADJUST_STRAIGHT_LF, text);
		if (createMatcherAdjustStraightLF) {
			mMatcherAdjustStraightLF.setValue(matcherAdjustStraightLF);
		}
		// TODO: 原因不明だがreplaceAll内でCrash errosが来たので例外対策
		// →何かの拍子でMessageLoaderのスレッドが２つ同時に動いたとか？
		try {
			text = matcherAdjustStraightLF.replaceAll("\n\n\n");
		} catch (Exception e) {
			Log.e(LOG_TAG, e.toString(), e);
		} finally {
			mText = text;
		}

		setSplitText(mText);
		updateFontSizeBySplitText();
	}

	private void setSplitText(String text) {
		int indexLF = text.indexOf('\n');
		if (indexLF < 0) {
			// 改行文字無し
			return;
		}
//		assert indexLF > 0;

		mSplitText = new ArrayList<String>();
		int indexStart = 0;
//		assert indexLF > indexStart;

		while (true) {
			mSplitText.add(text.substring(indexStart, indexLF));

			indexStart = indexLF + 1;
			indexLF = text.indexOf('\n', indexStart);
			if (indexLF < 0) {
				mSplitText.add(text.substring(indexStart));
				break;
			}
		}
	}

	private void updateFontSizeBySplitText() {
	    if (mSplitText == null) {
	        return;
	    }
		final int lines = mSplitText.size();
		switch (mFontSizeType) {
		    case FONTSIZE_TYPE_BIG:
		        if (lines >= 3) {
		            mFontSize = FONTSIZE_PX_SMALL_BIG;
		            mMarginNext = MARGIN_NEXT_SMALL;
		            mLineHeight = LINEHEIGHT_PX_SMALL_BIG;
		        }
                break;
            case FONTSIZE_TYPE_MEDIUM:
                if (lines >= 5) {
                    mFontSize = FONTSIZE_PX_SMALL_MEDIUM;
                    mMarginNext = MARGIN_NEXT_SMALL;
                    mLineHeight = LINEHEIGHT_PX_SMALL_MEDIUM;
                }
                break;
            case FONTSIZE_TYPE_SMALL:
                if (lines >= 7) {
                    mFontSize = FONTSIZE_PX_SMALL_SMALL;
                    mMarginNext = MARGIN_NEXT_SMALL;
                    mLineHeight = LINEHEIGHT_PX_SMALL_SMALL;
                }
                break;
            default:
                assert false : "mFontSizeType=" + mFontSizeType;
                break;
		}
	}

	/**
	 * コメントのオリジナルのテキストを取得
	 * @return
	 */
	public String getText() {
		return mText;
	}
	/**
	 * 実際に画面に描画されるテキストを取得<br>
	 * 置換等によって{@link MessageChat#getText()}の値とは異なる場合がある
	 * @return
	 */
	public String getDrawnText() {
	    if (mDrawnText == null) {
	        return mText;
	    } else {
	        return mDrawnText;
	    }
	}

	/**
	 * コメントを描画
	 * @param vpos 動画の現在時刻
	 * @param canvas 描画対象
	 * @param paint 描画に使用する{@link android.graphics.Paint}
	 * @param controller コメント管理のコントローラ
	 */
	public void draw(int vpos, Canvas canvas, Paint paint,
			MessageChatController controller) {
	    if (canvas == null) {
	        return;
	    }
	    String text = getDrawnText();
		if (text == null) {
			return;
		}

		paint.setTextSize(getFontSize());
//		mDir = controller.getDir(isFork());
//		float x = computeX(vpos);
		float x = computeXRelatively(vpos);
		float y = getDrawY();

		paint.setShadowLayer(1.5f, 0.0f, 0.0f, getShadowColor());
		paint.setColor(getColor());
		if (mSplitText == null) {
			canvas.drawText(text, x, y, paint);
		} else {
			for (String lineText : mSplitText) {
				canvas.drawText(lineText, x, y, paint);
				y += mHeight;
			}
		}

		if (mMySendComment) {
            drawFrame(canvas, paint, COLOR_YELLOW, x);
		} else if (DEBUG_DRAW) {
            drawFrame(canvas, paint, COLOR_RED, x);
        }
	}

	private void drawFrame(Canvas canvas, Paint paint, int color, float x) {
        int top, bottom;
        if (mPos == POS_SHITA) {
            bottom = mVirtualY - 1;
            top = mVirtualY - getHeight();
        } else {
            top = mVirtualY;
            bottom = mVirtualY + getHeight() - 1;
        }

        paint.setColor(color);
        paint.setShadowLayer(0.0f, 0.0f, 0.0f, 0);
        Paint.Style lastStyle = paint.getStyle();
        paint.setStyle(Paint.Style.STROKE);

        canvas.drawRect(x, top,
                x + mWidth - 1, bottom,
                paint);

        paint.setStyle(lastStyle);
	}

	private int getHeight() {
		assert mHeight > 0;
		if (mSplitText == null) {
			return mHeight + mMarginNext;
		} else {
			return mHeight * mSplitText.size() + mMarginNext;
		}
	}

	public int getSingleLineHeight() {
		return mHeight;
	}

	public int getVpos() {
		return mVpos;
	}
	public void setVpos(int vpos) {
		mVpos = vpos;
		mLastVpos = vpos;
	}

	/**
	 * コメントの有効時間の取得
	 * @return
	 */
	public int getTime() {
	    return mTime;
	}
	/**
	 * コメントの有効時間を外部から設定<br>
	 * {@link #setText}の後に呼ぶこと
	 * @param time
	 */
	public void setTime(int time) {
	    mTime = time;
	}

	public boolean isDisplayedTiming(int vpos) {
		int myVpos;
		switch (mPos) {
		case POS_NAKA:
//            myVpos = mVpos - 100;
			// 16:9考慮して１秒より早めに表示
			myVpos = mVpos - 100 - 100;
			break;
		case POS_UE:
		case POS_SHITA:
            myVpos = mVpos;
			break;
		default:
			assert false;
            myVpos = mVpos - 100 - 100;
			break;
		}
		return vpos >= myVpos;
	}

	public boolean isRemovedTiming(int vpos) {
		boolean ret;
		if (mTime >= 0) {
		    if (mTime == Integer.MAX_VALUE) {
		        ret = false;
		    } else {
		        ret = (vpos > mVpos + mTime * 100);
		    }
		} else {
//			switch (mPos) {
//			case POS_NAKA:
//	//			ret = (vpos > mVpos - 100 + MessageChat.DISPLAY_TIME_VPOS_NAKA);
//	//			// 16:9考慮して１秒余分に表示
//	//			ret = (vpos > mVpos - 100 + 100 + DISPLAY_TIME_VPOS_NAKA);
//				float currentX = computeXRelatively(vpos);
//				ret = (currentX + getWidth() <
//						(REAL_PLAYER_WIDTH_PX_4_3 - REAL_PLAYER_WIDTH_PX_16_9) / 2);
//				break;
//			case POS_UE:
//				ret = (vpos > mVpos + DISPLAY_TIME_VPOS_UE);
//				break;
//			case POS_SHITA:
//				ret = (vpos > mVpos + DISPLAY_TIME_VPOS_SHITA);
//				break;
//			default:
//				assert false;
//				ret = (vpos > mVpos - 100 + 100 + DISPLAY_TIME_VPOS_NAKA);
//				break;
//			}

			if (mPos == POS_UE) {
				ret = (vpos > mVpos + DISPLAY_TIME_VPOS_UE);
			} else if (mPos == POS_SHITA) {
				ret = (vpos > mVpos + DISPLAY_TIME_VPOS_SHITA);
			} else {
				assert mPos == POS_NAKA;
				float currentX = computeXRelatively(vpos);
				if (mDir == DIR_GYAKU) {
					ret = (currentX >
						REAL_PLAYER_WIDTH_PX_16_9 - (REAL_PLAYER_WIDTH_PX_16_9 - REAL_PLAYER_WIDTH_PX_4_3) / 2);
				} else {
	//				ret = (vpos > mVpos - 100 + MessageChat.DISPLAY_TIME_VPOS_NAKA);
	//				// 16:9考慮して１秒余分に表示
	//				ret = (vpos > mVpos - 100 + 100 + DISPLAY_TIME_VPOS_NAKA);
					ret = (currentX + getWidth() <
						(REAL_PLAYER_WIDTH_PX_4_3 - REAL_PLAYER_WIDTH_PX_16_9) / 2);
				}
			}
		}
		return ret;
	}

	/**
	 * ローカルコメント（一時保存）として保存するか判定
	 * @return
	 */
	public boolean isSaveLocal() {
	    return mIsSaveLocal;
	}

	private static final Comparator<MessageChat> sVposComparator =
		new Comparator<MessageChat>() {
			@Override
			public int compare(MessageChat object1, MessageChat object2) {
				int result = object1.mVpos - object2.mVpos;
				if (result == 0) {
				    result = object1.mNo - object2.mNo;
				}
				return result;
			}
		};
	public static Comparator<MessageChat> getVposComparator() {
		return sVposComparator;
	}

	public void setDefaultSetting(MessageChatController controller) {
	    boolean needUpdateFontSize = false;
        if (mIsDefaultColor) {
            mColor = controller.getDefaultColor();
        }
        if (mIsDefaultFontSize) {
            mFontSize = controller.getDefaultFontSize();
            mFontSizeType = controller.getDefaultFontSizeType();
            mLineHeight = controller.getDefaultLineHeight();
            needUpdateFontSize = true;
        }
        if (mIsDefaultPos) {
            mPos = controller.getDefaultPos();
        }

        String text = mText;
        text = controller.applyNgUp(text);

	    NicoScriptReplace replace = controller.getReplaceIfMatch(this);
	    if (replace != null) {
	        // ＠置換の場合は、そちらからコメントの初期設定を引っ張ってくる
            if (mIsDefaultColor) {
                mColor = replace.getColor();
            }
            if (mIsDefaultFontSize) {
                mFontSize = replace.getFontSize();
                mFontSizeType = replace.getFontSizeType();
                mLineHeight = replace.getLineHeight();
                needUpdateFontSize = true;
            }
            if (mIsDefaultPos) {
                mPos = replace.getPos();
            }

            text = replace.replace(text);
	    }

	    if (text != mText) {
	        mDrawnText = text;
            setSplitText(text);
            needUpdateFontSize = true;
	    }

	    if (needUpdateFontSize) {
            updateFontSizeBySplitText();
	    }

		mDir = controller.getDir(isFork());
	}

	/**
	 * コメント表示用コンテナに追加する際の前準備
	 * @param controller コメント管理のコントローラ
	 * @param paintText コメント表示に使用する{@link android.graphics.Paint}
	 * @param vpos 動画の現在時刻
	 */
	public void prepareAdd(MessageChatController controller,
			Paint paintText, int vpos) {
		setDefaultSetting(controller);
		computeWidth(paintText);
		computeSpeed();
		computeLineHeight(paintText);
		initializeX(vpos);
	}

	/**
     * コメント表示用コンテナから除去する際の前準備
	 * @param controller コメント管理のコントローラ
	 */
	public void prepareRemove(MessageChatController controller) {
	}

	/**
	 * コメントのスクロールする方向を設定
	 * @param controller コメント管理のコントローラ
	 */
	public void setDir(MessageChatController controller) {
		mDir = controller.getDir(isFork());
	}

	public void setMySendComment() {
	    mMySendComment = true;
	}

	/**
	 * コメントがニコスクリプトかどうか判定
	 * @return
	 */
	protected boolean isScript() {
		return false;
	}
    /**
     * コメントが投稿者コメントかどうか判定
     * @return
     */
	protected boolean isFork() {
		return false;
	}

	public String getUserId() {
	    return mUserId;
	}

	/**
	 * NGコマンドを反映
	 * @param configureNgClientsCommand
	 */
	public void applyNgCommand(ArrayList<ConfigureNgClient.NgClient> configureNgClientsCommand) {
	    if (configureNgClientsCommand == null) {
	        return;
	    }
	    if (mMail == null) {
	        return;
	    }
	    String mail = mMail;
	    for (ConfigureNgClient.NgClient ngClient : configureNgClientsCommand) {
	        if ("all".equals(ngClient.source)) {
	            mail = "";
	            break;
	        }
	        mail = mail.replace(ngClient.source, "");
	    }
	    mFilteredMail = mail;
	    setMail(mFilteredMail);
	}

	public int getScore() {
	    return mScore;
	}
}