package jp.sourceforge.nicoro;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;

import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Iterator;

/**
 * 単純なテキスト表示View
 *
 * SDKのApiDemosのLabelViewクラスを参考に改造
 *
 */
public class VariableLabelView extends View {
    private static final int MSG_ID_DRAW_SCROLL = 0;
    private static final long INTERVAL_DRAW_SCROLL = 33L;

	private Paint mTextPaint;
	private StringBuilder mTextBuilder;
	private String mText;
    private float mTextAscent;
    private float mTextWidth;
    private float mTextHeight;

    private boolean mScroll;
    private float mScrollSpeed;
    private int mScrollDelay;
    private long mScrollBeginTime = 0L;
    private float mScrollSpace;

    private boolean mScrollImpl;

    private float mShadowRadius;
    private float mShadowDx;
    private float mShadowDy;

    private static float sDensity = 0.0f;

//    private Handler mDrawScroll = new Handler() {
//		@Override
//		public void handleMessage(Message msg) {
//			switch (msg.what) {
//			case MSG_ID_DRAW_SCROLL:
//		        invalidate();
//				sendEmptyMessageDelayed(
//						MSG_ID_DRAW_SCROLL, INTERVAL_DRAW_SCROLL);
//				break;
//			default:
//				assert false : msg.what;
//				break;
//			}
//		}
//    };

	public VariableLabelView(Context context) {
		this(context, null);
	}
	public VariableLabelView(Context context, AttributeSet attrs) {
		super(context, attrs);

		mTextPaint = new Paint();

		TypedArray a = context.obtainStyledAttributes(attrs,
				R.styleable.VariableLabelView);

//		String source =
//			a.getString(R.styleable.VariableLabelView_text);
//		int textColor =
//			a.getColor(R.styleable.VariableLabelView_textColor, Color.BLACK);
//		int textSize =
//			a.getDimensionPixelOffset(R.styleable.VariableLabelView_textSize, 16);
//		boolean antialias =
//			a.getBoolean(R.styleable.VariableLabelView_antialias, false);
//		int defaultCapacity =
//			a.getInt(R.styleable.VariableLabelView_defaultCapacity, 256);

		String source = null;
		int textColor = Color.BLACK;
		int textSize = 16;
		boolean antialias = false;
		boolean underlineText = false;
		int defaultCapacity = 256;
		boolean scroll = false;
		float scrollSpeed = 30.0f;
		int scrollDelay = 1000;
		int shadowColor = 0;
		float shadowDx = 0.0f;
        float shadowDy = 0.0f;
        float shadowRadius = 0.0f;

		final int indexCount = a.getIndexCount();
		for (int i = 0; i < indexCount; i++) {
			int attr = a.getIndex(i);
			switch (attr) {
			case R.styleable.VariableLabelView_text:
				source =
					a.getString(R.styleable.VariableLabelView_text);
				break;
			case R.styleable.VariableLabelView_textColor:
				textColor =
					a.getColor(R.styleable.VariableLabelView_textColor, Color.BLACK);
				break;
			case R.styleable.VariableLabelView_textSize:
				textSize =
					a.getDimensionPixelOffset(R.styleable.VariableLabelView_textSize, 16);
				break;
			case R.styleable.VariableLabelView_antialias:
				antialias =
					a.getBoolean(R.styleable.VariableLabelView_antialias, false);
				break;
			case R.styleable.VariableLabelView_underlineText:
				underlineText =
					a.getBoolean(R.styleable.VariableLabelView_underlineText, false);
				break;
			case R.styleable.VariableLabelView_scroll:
				scroll =
					a.getBoolean(R.styleable.VariableLabelView_scroll, false);
				break;
			case R.styleable.VariableLabelView_scrollSpeed:
				scrollSpeed =
					a.getFloat(R.styleable.VariableLabelView_scrollSpeed, 20.0f);
				break;
			case R.styleable.VariableLabelView_scrollDelay:
				scrollDelay =
					a.getInt(R.styleable.VariableLabelView_scrollDelay, 1000);
				break;
            case R.styleable.VariableLabelView_shadowColor:
                shadowColor = a.getColor(R.styleable.VariableLabelView_shadowColor, 0);
                break;
            case R.styleable.VariableLabelView_shadowDx:
                shadowDx = a.getFloat(R.styleable.VariableLabelView_shadowDx, 0.0f);
                break;
            case R.styleable.VariableLabelView_shadowDy:
                shadowDy = a.getFloat(R.styleable.VariableLabelView_shadowDy, 0.0f);
                break;
            case R.styleable.VariableLabelView_shadowRadius:
                shadowRadius = a.getFloat(R.styleable.VariableLabelView_shadowRadius, 0.0f);
                break;
			default:
				defaultCapacity =
					a.getInt(R.styleable.VariableLabelView_defaultCapacity, 256);
				break;
			}
		}

		mTextPaint.setColor(textColor);
		mTextPaint.setAntiAlias(antialias);
		mTextPaint.setUnderlineText(underlineText);
		setTextSizeImpl(textSize);
		if (source == null) {
			mTextBuilder = new StringBuilder(defaultCapacity);
			mText = null;
			mTextWidth = 0.0f;
		} else {
			mTextBuilder = new StringBuilder(source);
			setTextImpl(source);
		}

		mScroll = scroll;
		mScrollSpeed = -scrollSpeed;
		mScrollDelay = scrollDelay;

		mScrollImpl = mScroll;

        if (shadowColor != 0) {
            setShadowLayer(shadowRadius, shadowDx, shadowDy, shadowColor);
        }

		a.recycle();

		if (sDensity == 0.0f) {
    		DisplayMetrics metrics = new DisplayMetrics();
    		((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
    		    .getDefaultDisplay().getMetrics(metrics);
    		sDensity = metrics.density;
		}
	}

	public StringBuilder getTextBuilder() {
		mText = null;
		return mTextBuilder;
	}
	public StringBuilder getTextBuilderWithClear() {
		StringBuilder builder = getTextBuilder();
		builder.setLength(0);
		return builder;
	}

	public String getText() {
		// 設定済みであればmTextはnullではない
		return mText;
	}

	public void notifyUpdateText() {
		setTextImpl(mTextBuilder.toString());
        requestLayout();
        invalidate();
	}

    public void setText(String text) {
        if (text == null) {
            // 外部データから直接持ってくるとたまにnullのことがあり得る
            text = "";
        }
    	mTextBuilder.setLength(0);
        mTextBuilder.append(text);
		setTextImpl(text);
        requestLayout();
        invalidate();
    }

    public void setTextSize(float size) {
    	setTextSizeImpl(size);
        if (mText != null) {
        	mTextWidth = mTextPaint.measureText(mText);
        }
        requestLayout();
        invalidate();
    }

    public void setTextColor(int color) {
        mTextPaint.setColor(color);
        invalidate();
    }

    public void setScrollEnabled(boolean enabled) {
    	mScroll = enabled;
    	mScrollImpl = mScroll;
    }

    public void setScrollSpeed(float speed) {
    	mScrollSpeed = -speed;
    }

    public void setShadowLayer(float radius, float dx, float dy, int color) {
        mTextPaint.setShadowLayer(radius, dx, dy, color);

        mShadowRadius = radius;
        mShadowDx = dx;
        mShadowDy = dy;

        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
     * Determines the width of this view
     * @param measureSpec A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
        	if (mText == null) {
        		setTextImpl(mTextBuilder.toString());
        	}
            result = (int) mTextWidth + getPaddingLeft()
        		+ getPaddingRight();

            result = Math.max(result, getSuggestedMinimumWidth());

            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        return result;
    }

    /**
     * Determines the height of this view
     * @param measureSpec A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (mTextHeight + 0.5f) + getPaddingTop()
                    + getPaddingBottom();

            result = Math.max(result, getSuggestedMinimumHeight());

            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    /**
     * Render the text
     *
     * @see android.view.View#onDraw(android.graphics.Canvas)
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mText != null) {
            canvas.save(Canvas.CLIP_SAVE_FLAG);

            int width = getWidth();
            int paddingLeft = getPaddingLeft();
            int paddingTop = getPaddingTop();
            int paddingRight = getPaddingRight();
            int scrollX = getScrollX();
            int scrollY = getScrollY();

            int clipLeft = paddingLeft + scrollX;
            int clipTop = paddingTop + scrollY;
            int clipRight = width - paddingRight + scrollX;
            int clipBottom = getHeight() - getPaddingBottom() + scrollY;
            if (mShadowRadius != 0.0f) {
                clipLeft += Math.min(0.0f, mShadowDx - mShadowRadius);
                clipTop += Math.min(0.0f, mShadowDy - mShadowRadius);
                clipRight += Math.max(0.0f, mShadowDx + mShadowRadius);
                clipBottom += Math.max(0.0f, mShadowDy + mShadowRadius);
            }
            canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);

//        	if (canvas.getWidth() >=
            if (width >=
                mTextWidth + paddingLeft + paddingRight) {
        		mScrollImpl = false;
        		mScrollBeginTime = 0;
//        		mDrawScroll.removeMessages(MSG_ID_DRAW_SCROLL);
        		ScrollManager.getInstance().detachView(this);
        	} else {
        		mScrollImpl = mScroll;
        	}

//        	if (mScroll) {
        	if (mScrollImpl) {
        		final long current = SystemClock.elapsedRealtime();
        		if (mScrollBeginTime == 0L) {
        			mScrollBeginTime = current;
//        			if (!mDrawScroll.hasMessages(MSG_ID_DRAW_SCROLL)) {
//	    				mDrawScroll.sendEmptyMessageDelayed(
//	    						MSG_ID_DRAW_SCROLL, INTERVAL_DRAW_SCROLL);
//        			}
                    ScrollManager.getInstance().attachView(this);
        		}

        		long timeForPos = current - mScrollBeginTime - mScrollDelay;
        		final float scrollWidth = mTextWidth + mScrollSpace;
        		float pos1st;
        		if (timeForPos >= 0) {
            		pos1st = timeForPos * mScrollSpeed * sDensity / 1000.0f;
            		if (pos1st < -scrollWidth) {
            			pos1st = 0.0f;
            			mScrollBeginTime = current;
            		}
        		} else {
        			pos1st = 0.0f;
        		}
        		float pos2nd = pos1st + scrollWidth;

	        	canvas.drawText(mText,
                        paddingLeft + pos1st,
                        paddingTop - mTextAscent,
	        			mTextPaint);
	        	canvas.drawText(mText,
                        paddingLeft + pos2nd,
                        paddingTop - mTextAscent,
	        			mTextPaint);
        	} else {
	        	canvas.drawText(mText,
                        paddingLeft,
                        paddingTop - mTextAscent,
	        			mTextPaint);
        	}

        	canvas.restore();
        }
    }

    private void setTextImpl(String text) {
    	mText = text;
    	measureTextWidth();
    }

    private void setTextSizeImpl(float textSize) {
    	final float lastTextSize = mTextPaint.getTextSize();
		mTextPaint.setTextSize(textSize);
		// 共通化のためのメソッドのため、ascent等は必ず取り直す
        mTextAscent = mTextPaint.ascent();
        mTextHeight = mTextPaint.getFontMetrics(null);
        mScrollSpace = textSize * 2.0f;
        if (lastTextSize != textSize && mText != null) {
        	measureTextWidth();
        }
    }

    private void measureTextWidth() {
    	assert mText != null;
    	final float lastTextWidth = mTextWidth;
		mTextWidth = mTextPaint.measureText(mText);
		assert mScrollImpl ? true : mScrollBeginTime == 0;
    	if (mScroll && lastTextWidth != mTextWidth) {
//    	if (mScrollImpl && lastTextWidth != mTextWidth) {
    		// scroll初期化
    		if (mScrollBeginTime != 0) {
    			mScrollBeginTime = SystemClock.elapsedRealtime();
//    			if (!mDrawScroll.hasMessages(MSG_ID_DRAW_SCROLL)) {
//    				mDrawScroll.sendEmptyMessageDelayed(
//    						MSG_ID_DRAW_SCROLL, INTERVAL_DRAW_SCROLL);
//    			}
                ScrollManager.getInstance().attachView(this);
    		}
    	}
    }

    // ScrollManagerにattachするかどうかはonAttachedToWindow()ではなく
    // scrollが必要になったタイミングで行う

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        ScrollManager.getInstance().detachView(this);
    }

    @Override
    protected boolean isPaddingOffsetRequired() {
        return mShadowRadius != 0.0f;
    }
    @Override
    protected int getLeftPaddingOffset() {
        return (int) Math.min(0.0f, mShadowDx - mShadowRadius);
    }
    @Override
    protected int getRightPaddingOffset() {
        return (int) Math.max(0.0f, mShadowDx + mShadowRadius);
    }
    @Override
    protected int getTopPaddingOffset() {
        return (int) Math.min(0.0f, mShadowDy - mShadowRadius);
    }
    @Override
    protected int getBottomPaddingOffset() {
        return (int) Math.max(0.0f, mShadowDy + mShadowRadius);
    }

    // FIXME ViewPagerのFragment復帰でどうしてもクラッシュすることがあるので、いったん無効化
//    @Override
//    protected Parcelable onSaveInstanceState() {
//        Parcelable superState = super.onSaveInstanceState();
//
//        SavedState ss = new SavedState(superState);
//        ss.mText = mText;
//        return ss;
//    }
//
//    @Override
//    protected void onRestoreInstanceState(Parcelable state) {
//        SavedState ss = (SavedState) state;
//
//        super.onRestoreInstanceState(ss.getSuperState());
//        if (ss.mText != null) {
//            mText = ss.mText;
//        }
//    }

    public static class SavedState extends BaseSavedState {
        String mText;

        SavedState(Parcelable superState) {
            super(superState);
            mText = null;
        }

        private SavedState(Parcel in) {
            super(in);
            mText = in.readString();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeString(mText);
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel source) {
                return new SavedState(source);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }


    private static class WeakRef extends WeakReference<VariableLabelView> {
        public WeakRef(VariableLabelView r) {
            super(r);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o instanceof WeakRef) {
                o = ((WeakRef) o).get();
            }
            return get() == o;
        }

        @Override
        public int hashCode() {
            VariableLabelView thiz = get();
            if (thiz == null) {
                return super.hashCode();
            } else {
                return thiz.hashCode();
            }
        }
    }

    /**
     *
     * すべてのメソッドはMainスレッドから使用すること
     */
    private static class ScrollManager extends Handler {
        private HashSet<WeakRef> mSet = new HashSet<WeakRef>();

        @Override
        public void handleMessage(Message msg) {
            for (Iterator<WeakRef> it = mSet.iterator(); it.hasNext(); ) {
                WeakRef ref = it.next();
                VariableLabelView view = ref.get();
                if (view == null) {
                    it.remove();
                } else if (view.getWindowToken() == null) {
                    // 念のためチェックして、既にdetachされていたらremove
                    it.remove();
                } else {
                    view.invalidate();
                }
            }
            if (mSet.size() > 0) {
                sendEmptyMessageDelayed(
                        MSG_ID_DRAW_SCROLL, INTERVAL_DRAW_SCROLL);
            }
        }

        void attachView(VariableLabelView view) {
            int lastSize = mSet.size();
            mSet.add(new WeakRef(view));
            if (lastSize == 0) {
                if (!hasMessages(MSG_ID_DRAW_SCROLL)) {
                    sendEmptyMessage(MSG_ID_DRAW_SCROLL);
                }
            }
        }
        void detachView(VariableLabelView view) {
            if (mSet.remove(view)) {
                if (mSet.size() == 0) {
                    removeMessages(MSG_ID_DRAW_SCROLL);
                }
            }
        }

        private static ScrollManager sInstance;
        static ScrollManager getInstance() {
            if (sInstance == null) {
                sInstance = new ScrollManager();
            }
            return sInstance;
        }
        private ScrollManager() {
            super();
        }
    }
}
