/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.inputmethod.pinyin;

import com.android.inputmethod.pinyin.PinyinIME.DecodingInfo;

import java.util.Vector;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

/**
 * View to show candidate list. There two candidate view instances which are
 * used to show animation when user navigates between pages.
 */
public class CandidateView extends View {
    /**
     * The minimum width to show a item.
     */
    private static final float MIN_ITEM_WIDTH = 22;

    /**
     * Suspension points used to display long items.
     */
    private static final String SUSPENSION_POINTS = "...";

    /**
     * The width to draw candidates.
     */
    private int mContentWidth;

    /**
     * The height to draw candidate content.
     */
    private int mContentHeight;

    /**
     * Whether footnotes are displayed. Footnote is shown when hardware keyboard
     * is available.
     */
    private boolean mShowFootnote = true;

    /**
     * Balloon hint for candidate press/release.
     */
    private BalloonHint mBalloonHint;

    /**
     * Desired position of the balloon to the input view.
     */
    private int mHintPositionToInputView[] = new int[2];

    /**
     * Decoding result to show.
     */
    private DecodingInfo mDecInfo;

    /**
     * Listener used to notify IME that user clicks a candidate, or navigate
     * between them.
     */
    private CandidateViewListener mCvListener;

    /**
     * Used to notify the container to update the status of forward/backward
     * arrows.
     */
    private ArrowUpdater mArrowUpdater;

    /**
     * If true, update the arrow status when drawing candidates.
     */
    private boolean mUpdateArrowStatusWhenDraw = false;

    /**
     * Page number of the page displayed in this view.
     */
    private int mPageNo;

    /**
     * Active candidate position in this page.
     */
    private int mActiveCandInPage;

    /**
     * Used to decided whether the active candidate should be highlighted or
     * not. If user changes focus to composing view (The view to show Pinyin
     * string), the highlight in candidate view should be removed.
     */
    private boolean mEnableActiveHighlight = true;

    /**
     * The page which is just calculated.
     */
    private int mPageNoCalculated = -1;

    /**
     * The Drawable used to display as the background of the high-lighted item.
     */
    private Drawable mActiveCellDrawable;

    /**
     * The Drawable used to display as separators between candidates.
     */
    private Drawable mSeparatorDrawable;

    /**
     * Color to draw normal candidates generated by IME.
     */
    private int mImeCandidateColor;

    /**
     * Color to draw normal candidates Recommended by application.
     */
    private int mRecommendedCandidateColor;

    /**
     * Color to draw the normal(not highlighted) candidates, it can be one of
     * {@link #mImeCandidateColor} or {@link #mRecommendedCandidateColor}.
     */
    private int mNormalCandidateColor;

    /**
     * Color to draw the active(highlighted) candidates, including candidates
     * from IME and candidates from application.
     */
    private int mActiveCandidateColor;

    /**
     * Text size to draw candidates generated by IME.
     */
    private int mImeCandidateTextSize;

    /**
     * Text size to draw candidates recommended by application.
     */
    private int mRecommendedCandidateTextSize;

    /**
     * The current text size to draw candidates. It can be one of
     * {@link #mImeCandidateTextSize} or {@link #mRecommendedCandidateTextSize}.
     */
    private int mCandidateTextSize;

    /**
     * Paint used to draw candidates.
     */
    private Paint mCandidatesPaint;

    /**
     * Used to draw footnote.
     */
    private Paint mFootnotePaint;

    /**
     * The width to show suspension points.
     */
    private float mSuspensionPointsWidth;

    /**
     * Rectangle used to draw the active candidate.
     */
    private RectF mActiveCellRect;

    /**
     * Left and right margins for a candidate. It is specified in xml, and is
     * the minimum margin for a candidate. The actual gap between two candidates
     * is 2 * {@link #mCandidateMargin} + {@link #mSeparatorDrawable}.
     * getIntrinsicWidth(). Because length of candidate is not fixed, there can
     * be some extra space after the last candidate in the current page. In
     * order to achieve best look-and-feel, this extra space will be divided and
     * allocated to each candidates.
     */
    private float mCandidateMargin;

    /**
     * Left and right extra margins for a candidate.
     */
    private float mCandidateMarginExtra;

    /**
     * Rectangles for the candidates in this page.
     **/
    private Vector<RectF> mCandRects;

    /**
     * FontMetricsInt used to measure the size of candidates.
     */
    private FontMetricsInt mFmiCandidates;

    /**
     * FontMetricsInt used to measure the size of footnotes.
     */
    private FontMetricsInt mFmiFootnote;

    private PressTimer mTimer = new PressTimer();

    private GestureDetector mGestureDetector;

    private int mLocationTmp[] = new int[2];

    public CandidateView(Context context, AttributeSet attrs) {
        super(context, attrs);

        Resources r = context.getResources();

        Configuration conf = r.getConfiguration();
        if (conf.keyboard == Configuration.KEYBOARD_NOKEYS
                || conf.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
            mShowFootnote = false;
        }

        mActiveCellDrawable = r.getDrawable(R.drawable.candidate_hl_bg);
        mSeparatorDrawable = r.getDrawable(R.drawable.candidates_vertical_line);
        mCandidateMargin = r.getDimension(R.dimen.candidate_margin_left_right);

        mImeCandidateColor = r.getColor(R.color.candidate_color);
        mRecommendedCandidateColor = r.getColor(R.color.recommended_candidate_color);
        mNormalCandidateColor = mImeCandidateColor;
        mActiveCandidateColor = r.getColor(R.color.active_candidate_color);

        mCandidatesPaint = new Paint();
        mCandidatesPaint.setAntiAlias(true);

        mFootnotePaint = new Paint();
        mFootnotePaint.setAntiAlias(true);
        mFootnotePaint.setColor(r.getColor(R.color.footnote_color));
        mActiveCellRect = new RectF();

        mCandRects = new Vector<RectF>();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int mOldWidth = getMeasuredWidth();

        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),
                widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(),
                heightMeasureSpec));

        if (mOldWidth != getMeasuredWidth()) {
            onSizeChanged();
        }
    }

    public void initialize(ArrowUpdater arrowUpdater, BalloonHint balloonHint,
            GestureDetector gestureDetector, CandidateViewListener cvListener) {
        mArrowUpdater = arrowUpdater;
        mBalloonHint = balloonHint;
        mGestureDetector = gestureDetector;
        mCvListener = cvListener;
    }

    public void setDecodingInfo(DecodingInfo decInfo) {
        if (null == decInfo) return;
        mDecInfo = decInfo;
        mPageNoCalculated = -1;

        if (mDecInfo.candidatesFromApp()) {
            mNormalCandidateColor = mRecommendedCandidateColor;
            mCandidateTextSize = mRecommendedCandidateTextSize;
        } else {
            mNormalCandidateColor = mImeCandidateColor;
            mCandidateTextSize = mImeCandidateTextSize;
        }
        if (mCandidatesPaint.getTextSize() != mCandidateTextSize) {
            mCandidatesPaint.setTextSize(mCandidateTextSize);
            mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
            mSuspensionPointsWidth =
                    mCandidatesPaint.measureText(SUSPENSION_POINTS);
        }

        // Remove any pending timer for the previous list.
        mTimer.removeTimer();
    }

    public int getActiveCandiatePosInPage() {
        return mActiveCandInPage;
    }

    public int getActiveCandiatePosGlobal() {
        return mDecInfo.mPageStart.get(mPageNo) + mActiveCandInPage;
    }

    /**
     * Show a page in the decoding result set previously.
     *
     * @param pageNo Which page to show.
     * @param activeCandInPage Which candidate should be set as active item.
     * @param enableActiveHighlight When false, active item will not be
     *        highlighted.
     */
    public void showPage(int pageNo, int activeCandInPage,
            boolean enableActiveHighlight) {
        if (null == mDecInfo) return;
        mPageNo = pageNo;
        mActiveCandInPage = activeCandInPage;
        if (mEnableActiveHighlight != enableActiveHighlight) {
            mEnableActiveHighlight = enableActiveHighlight;
        }

        if (!calculatePage(mPageNo)) {
            mUpdateArrowStatusWhenDraw = true;
        } else {
            mUpdateArrowStatusWhenDraw = false;
        }

        invalidate();
    }

    public void enableActiveHighlight(boolean enableActiveHighlight) {
        if (enableActiveHighlight == mEnableActiveHighlight) return;

        mEnableActiveHighlight = enableActiveHighlight;
        invalidate();
    }

    public boolean activeCursorForward() {
        if (!mDecInfo.pageReady(mPageNo)) return false;
        int pageSize = mDecInfo.mPageStart.get(mPageNo + 1)
                - mDecInfo.mPageStart.get(mPageNo);
        if (mActiveCandInPage + 1 < pageSize) {
            showPage(mPageNo, mActiveCandInPage + 1, true);
            return true;
        }
        return false;
    }

    public boolean activeCurseBackward() {
        if (mActiveCandInPage > 0) {
            showPage(mPageNo, mActiveCandInPage - 1, true);
            return true;
        }
        return false;
    }

    private void onSizeChanged() {
        mContentWidth = getMeasuredWidth() - mPaddingLeft - mPaddingRight;
        mContentHeight = (int) ((getMeasuredHeight() - mPaddingTop - mPaddingBottom) * 0.95f);
        /**
         * How to decide the font size if the height for display is given?
         * Now it is implemented in a stupid way.
         */
        int textSize = 1;
        mCandidatesPaint.setTextSize(textSize);
        mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
        while (mFmiCandidates.bottom - mFmiCandidates.top < mContentHeight) {
            textSize++;
            mCandidatesPaint.setTextSize(textSize);
            mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
        }

        mImeCandidateTextSize = textSize;
        mRecommendedCandidateTextSize = textSize * 3 / 4;
        if (null == mDecInfo) {
            mCandidateTextSize = mImeCandidateTextSize;
            mCandidatesPaint.setTextSize(mCandidateTextSize);
            mFmiCandidates = mCandidatesPaint.getFontMetricsInt();
            mSuspensionPointsWidth =
                mCandidatesPaint.measureText(SUSPENSION_POINTS);
        } else {
            // Reset the decoding information to update members for painting.
            setDecodingInfo(mDecInfo);
        }

        textSize = 1;
        mFootnotePaint.setTextSize(textSize);
        mFmiFootnote = mFootnotePaint.getFontMetricsInt();
        while (mFmiFootnote.bottom - mFmiFootnote.top < mContentHeight / 2) {
            textSize++;
            mFootnotePaint.setTextSize(textSize);
            mFmiFootnote = mFootnotePaint.getFontMetricsInt();
        }
        textSize--;
        mFootnotePaint.setTextSize(textSize);
        mFmiFootnote = mFootnotePaint.getFontMetricsInt();

        // When the size is changed, the first page will be displayed.
        mPageNo = 0;
        mActiveCandInPage = 0;
    }

    private boolean calculatePage(int pageNo) {
        if (pageNo == mPageNoCalculated) return true;

        mContentWidth = getMeasuredWidth() - mPaddingLeft - mPaddingRight;
        mContentHeight = (int) ((getMeasuredHeight() - mPaddingTop - mPaddingBottom) * 0.95f);

        if (mContentWidth <= 0 || mContentHeight <= 0) return false;

        int candSize = mDecInfo.mCandidatesList.size();

        // If the size of page exists, only calculate the extra margin.
        boolean onlyExtraMargin = false;
        int fromPage = mDecInfo.mPageStart.size() - 1;
        if (mDecInfo.mPageStart.size() > pageNo + 1) {
            onlyExtraMargin = true;
            fromPage = pageNo;
        }

        // If the previous pages have no information, calculate them first.
        for (int p = fromPage; p <= pageNo; p++) {
            int pStart = mDecInfo.mPageStart.get(p);
            int pSize = 0;
            int charNum = 0;
            float lastItemWidth = 0;

            float xPos;
            xPos = 0;
            xPos += mSeparatorDrawable.getIntrinsicWidth();
            while (xPos < mContentWidth && pStart + pSize < candSize) {
                int itemPos = pStart + pSize;
                String itemStr = mDecInfo.mCandidatesList.get(itemPos);
                float itemWidth = mCandidatesPaint.measureText(itemStr);
                if (itemWidth < MIN_ITEM_WIDTH) itemWidth = MIN_ITEM_WIDTH;

                itemWidth += mCandidateMargin * 2;
                itemWidth += mSeparatorDrawable.getIntrinsicWidth();
                if (xPos + itemWidth < mContentWidth || 0 == pSize) {
                    xPos += itemWidth;
                    lastItemWidth = itemWidth;
                    pSize++;
                    charNum += itemStr.length();
                } else {
                    break;
                }
            }
            if (!onlyExtraMargin) {
                mDecInfo.mPageStart.add(pStart + pSize);
                mDecInfo.mCnToPage.add(mDecInfo.mCnToPage.get(p) + charNum);
            }

            float marginExtra = (mContentWidth - xPos) / pSize / 2;

            if (mContentWidth - xPos > lastItemWidth) {
                // Must be the last page, because if there are more items,
                // the next item's width must be less than lastItemWidth.
                // In this case, if the last margin is less than the current
                // one, the last margin can be used, so that the
                // look-and-feeling will be the same as the previous page.
                if (mCandidateMarginExtra <= marginExtra) {
                    marginExtra = mCandidateMarginExtra;
                }
            } else if (pSize == 1) {
                marginExtra = 0;
            }
            mCandidateMarginExtra = marginExtra;
        }
        mPageNoCalculated = pageNo;
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // The invisible candidate view(the one which is not in foreground) can
        // also be called to drawn, but its decoding result and candidate list
        // may be empty.
        if (null == mDecInfo || mDecInfo.isCandidatesListEmpty()) return;

        // Calculate page. If the paging information is ready, the function will
        // return at once.
        calculatePage(mPageNo);

        int pStart = mDecInfo.mPageStart.get(mPageNo);
        int pSize = mDecInfo.mPageStart.get(mPageNo + 1) - pStart;
        float candMargin = mCandidateMargin + mCandidateMarginExtra;
        if (mActiveCandInPage > pSize - 1) {
            mActiveCandInPage = pSize - 1;
        }

        mCandRects.removeAllElements();

        float xPos = mPaddingLeft;
        int yPos = (getMeasuredHeight() -
                (mFmiCandidates.bottom - mFmiCandidates.top)) / 2
                - mFmiCandidates.top;
        xPos += drawVerticalSeparator(canvas, xPos);
        for (int i = 0; i < pSize; i++) {
            float footnoteSize = 0;
            String footnote = null;
            if (mShowFootnote) {
                footnote = Integer.toString(i + 1);
                footnoteSize = mFootnotePaint.measureText(footnote);
                assert (footnoteSize < candMargin);
            }
            String cand = mDecInfo.mCandidatesList.get(pStart + i);
            float candidateWidth = mCandidatesPaint.measureText(cand);
            float centerOffset = 0;
            if (candidateWidth < MIN_ITEM_WIDTH) {
                centerOffset = (MIN_ITEM_WIDTH - candidateWidth) / 2;
                candidateWidth = MIN_ITEM_WIDTH;
            }

            float itemTotalWidth = candidateWidth + 2 * candMargin;

            if (mActiveCandInPage == i && mEnableActiveHighlight) {
                mActiveCellRect.set(xPos, mPaddingTop + 1, xPos
                        + itemTotalWidth, getHeight() - mPaddingBottom - 1);
                mActiveCellDrawable.setBounds((int) mActiveCellRect.left,
                        (int) mActiveCellRect.top, (int) mActiveCellRect.right,
                        (int) mActiveCellRect.bottom);
                mActiveCellDrawable.draw(canvas);
            }

            if (mCandRects.size() < pSize) mCandRects.add(new RectF());
            mCandRects.elementAt(i).set(xPos - 1, yPos + mFmiCandidates.top,
                    xPos + itemTotalWidth + 1, yPos + mFmiCandidates.bottom);

            // Draw footnote
            if (mShowFootnote) {
                canvas.drawText(footnote, xPos + (candMargin - footnoteSize)
                        / 2, yPos, mFootnotePaint);
            }

            // Left margin
            xPos += candMargin;
            if (candidateWidth > mContentWidth - xPos - centerOffset) {
                cand = getLimitedCandidateForDrawing(cand,
                        mContentWidth - xPos - centerOffset);
            }
            if (mActiveCandInPage == i && mEnableActiveHighlight) {
                mCandidatesPaint.setColor(mActiveCandidateColor);
            } else {
                mCandidatesPaint.setColor(mNormalCandidateColor);
            }
            canvas.drawText(cand, xPos + centerOffset, yPos,
                    mCandidatesPaint);

            // Candidate and right margin
            xPos += candidateWidth + candMargin;

            // Draw the separator between candidates.
            xPos += drawVerticalSeparator(canvas, xPos);
        }

        // Update the arrow status of the container.
        if (null != mArrowUpdater && mUpdateArrowStatusWhenDraw) {
            mArrowUpdater.updateArrowStatus();
            mUpdateArrowStatusWhenDraw = false;
        }
    }

    private String getLimitedCandidateForDrawing(String rawCandidate,
            float widthToDraw) {
        int subLen = rawCandidate.length();
        if (subLen <= 1) return rawCandidate;
        do {
            subLen--;
            float width = mCandidatesPaint.measureText(rawCandidate, 0, subLen);
            if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) {
                return rawCandidate.substring(0, subLen) +
                        SUSPENSION_POINTS;
            }
        } while (true);
    }

    private float drawVerticalSeparator(Canvas canvas, float xPos) {
        mSeparatorDrawable.setBounds((int) xPos, mPaddingTop, (int) xPos
                + mSeparatorDrawable.getIntrinsicWidth(), getMeasuredHeight()
                - mPaddingBottom);
        mSeparatorDrawable.draw(canvas);
        return mSeparatorDrawable.getIntrinsicWidth();
    }

    private int mapToItemInPage(int x, int y) {
        // mCandRects.size() == 0 happens when the page is set, but
        // touch events occur before onDraw(). It usually happens with
        // monkey test.
        if (!mDecInfo.pageReady(mPageNo) || mPageNoCalculated != mPageNo
                || mCandRects.size() == 0) {
            return -1;
        }

        int pageStart = mDecInfo.mPageStart.get(mPageNo);
        int pageSize = mDecInfo.mPageStart.get(mPageNo + 1) - pageStart;
        if (mCandRects.size() < pageSize) {
            return -1;
        }

        // If not found, try to find the nearest one.
        float nearestDis = Float.MAX_VALUE;
        int nearest = -1;
        for (int i = 0; i < pageSize; i++) {
            RectF r = mCandRects.elementAt(i);
            if (r.left < x && r.right > x && r.top < y && r.bottom > y) {
                return i;
            }
            float disx = (r.left + r.right) / 2 - x;
            float disy = (r.top + r.bottom) / 2 - y;
            float dis = disx * disx + disy * disy;
            if (dis < nearestDis) {
                nearestDis = dis;
                nearest = i;
            }
        }

        return nearest;
    }

    // Because the candidate view under the current focused one may also get
    // touching events. Here we just bypass the event to the container and let
    // it decide which view should handle the event.
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

    public boolean onTouchEventReal(MotionEvent event) {
        // The page in the background can also be touched.
        if (null == mDecInfo || !mDecInfo.pageReady(mPageNo)
                || mPageNoCalculated != mPageNo) return true;

        int x, y;
        x = (int) event.getX();
        y = (int) event.getY();

        if (mGestureDetector.onTouchEvent(event)) {
            mTimer.removeTimer();
            mBalloonHint.delayedDismiss(0);
            return true;
        }

        int clickedItemInPage = -1;

        switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
            clickedItemInPage = mapToItemInPage(x, y);
            if (clickedItemInPage >= 0) {
                invalidate();
                mCvListener.onClickChoice(clickedItemInPage
                        + mDecInfo.mPageStart.get(mPageNo));
            }
            mBalloonHint.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS);
            break;

        case MotionEvent.ACTION_DOWN:
            clickedItemInPage = mapToItemInPage(x, y);
            if (clickedItemInPage >= 0) {
                showBalloon(clickedItemInPage, true);
                mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo,
                        clickedItemInPage);
            }
            break;

        case MotionEvent.ACTION_CANCEL:
            break;

        case MotionEvent.ACTION_MOVE:
            clickedItemInPage = mapToItemInPage(x, y);
            if (clickedItemInPage >= 0
                    && (clickedItemInPage != mTimer.getActiveCandOfPageToShow() || mPageNo != mTimer
                            .getPageToShow())) {
                showBalloon(clickedItemInPage, true);
                mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo,
                        clickedItemInPage);
            }
        }
        return true;
    }

    private void showBalloon(int candPos, boolean delayedShow) {
        mBalloonHint.removeTimer();

        RectF r = mCandRects.elementAt(candPos);
        int desired_width = (int) (r.right - r.left);
        int desired_height = (int) (r.bottom - r.top);
        mBalloonHint.setBalloonConfig(mDecInfo.mCandidatesList
                .get(mDecInfo.mPageStart.get(mPageNo) + candPos), 44, true,
                mImeCandidateColor, desired_width, desired_height);

        getLocationOnScreen(mLocationTmp);
        mHintPositionToInputView[0] = mLocationTmp[0]
                + (int) (r.left - (mBalloonHint.getWidth() - desired_width) / 2);
        mHintPositionToInputView[1] = -mBalloonHint.getHeight();

        long delay = BalloonHint.TIME_DELAY_SHOW;
        if (!delayedShow) delay = 0;
        mBalloonHint.dismiss();
        if (!mBalloonHint.isShowing()) {
            mBalloonHint.delayedShow(delay, mHintPositionToInputView);
        } else {
            mBalloonHint.delayedUpdate(0, mHintPositionToInputView, -1, -1);
        }
    }

    private class PressTimer extends Handler implements Runnable {
        private boolean mTimerPending = false;
        private int mPageNoToShow;
        private int mActiveCandOfPage;

        public PressTimer() {
            super();
        }

        public void startTimer(long afterMillis, int pageNo, int activeInPage) {
            mTimer.removeTimer();
            postDelayed(this, afterMillis);
            mTimerPending = true;
            mPageNoToShow = pageNo;
            mActiveCandOfPage = activeInPage;
        }

        public int getPageToShow() {
            return mPageNoToShow;
        }

        public int getActiveCandOfPageToShow() {
            return mActiveCandOfPage;
        }

        public boolean removeTimer() {
            if (mTimerPending) {
                mTimerPending = false;
                removeCallbacks(this);
                return true;
            }
            return false;
        }

        public boolean isPending() {
            return mTimerPending;
        }

        public void run() {
            if (mPageNoToShow >= 0 && mActiveCandOfPage >= 0) {
                // Always enable to highlight the clicked one.
                showPage(mPageNoToShow, mActiveCandOfPage, true);
                invalidate();
            }
            mTimerPending = false;
        }
    }
}
