/*
 * 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.email.activity;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.Layout.Alignment;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.format.DateUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;

import com.android.email.R;
import com.android.emailcommon.utility.TextUtilities;
import com.google.common.base.Objects;

/**
 * This custom View is the list item for the MessageList activity, and serves two purposes:
 * 1.  It's a container to store message metadata (e.g. the ids of the message, mailbox, & account)
 * 2.  It handles internal clicks such as the checkbox or the favorite star
 */
public class MessageListItem extends View {
    // Note: messagesAdapter directly fiddles with these fields.
    /* package */ long mMessageId;
    /* package */ long mMailboxId;
    /* package */ long mAccountId;

    private ThreePaneLayout mLayout;
    private MessagesAdapter mAdapter;
    private MessageListItemCoordinates mCoordinates;
    private Context mContext;
    private boolean mIsSearchResult = false;

    private boolean mDownEvent;

    public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL =
        "com.android.email.MESSAGE_LIST_ITEMS";

    public MessageListItem(Context context) {
        super(context);
        init(context);
    }

    public MessageListItem(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public MessageListItem(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    // Wide mode shows sender, snippet, time, and favorite spread out across the screen
    private static final int MODE_WIDE = MessageListItemCoordinates.WIDE_MODE;
    // Sentinel indicating that the view needs layout
    public static final int NEEDS_LAYOUT = -1;

    private static boolean sInit = false;
    private static final TextPaint sDefaultPaint = new TextPaint();
    private static final TextPaint sBoldPaint = new TextPaint();
    private static final TextPaint sDatePaint = new TextPaint();
    private static Bitmap sAttachmentIcon;
    private static Bitmap sInviteIcon;
    private static int sBadgeMargin;
    private static Bitmap sFavoriteIconOff;
    private static Bitmap sFavoriteIconOn;
    private static Bitmap sSelectedIconOn;
    private static Bitmap sSelectedIconOff;
    private static Bitmap sStateReplied;
    private static Bitmap sStateForwarded;
    private static Bitmap sStateRepliedAndForwarded;
    private static String sSubjectSnippetDivider;
    private static String sSubjectDescription;
    private static String sSubjectEmptyDescription;

    // Static colors.
    private static int DEFAULT_TEXT_COLOR;
    private static int ACTIVATED_TEXT_COLOR;
    private static int LIGHT_TEXT_COLOR;
    private static int DRAFT_TEXT_COLOR;
    private static int SUBJECT_TEXT_COLOR_READ;
    private static int SUBJECT_TEXT_COLOR_UNREAD;
    private static int SNIPPET_TEXT_COLOR_READ;
    private static int SNIPPET_TEXT_COLOR_UNREAD;
    private static int SENDERS_TEXT_COLOR_READ;
    private static int SENDERS_TEXT_COLOR_UNREAD;
    private static int DATE_TEXT_COLOR_READ;
    private static int DATE_TEXT_COLOR_UNREAD;

    public String mSender;
    public SpannableStringBuilder mText;
    public CharSequence mSnippet;
    private String mSubject;
    private StaticLayout mSubjectLayout;
    public boolean mRead;
    public boolean mHasAttachment = false;
    public boolean mHasInvite = true;
    public boolean mIsFavorite = false;
    public boolean mHasBeenRepliedTo = false;
    public boolean mHasBeenForwarded = false;
    /** {@link Paint} for account color chips.  null if no chips should be drawn.  */
    public Paint mColorChipPaint;

    private int mMode = -1;

    private int mViewWidth = 0;
    private int mViewHeight = 0;

    private static int sItemHeightWide;
    private static int sItemHeightNormal;

    // Note: these cannot be shared Drawables because they are selectors which have state.
    private Drawable mReadSelector;
    private Drawable mUnreadSelector;
    private Drawable mWideReadSelector;
    private Drawable mWideUnreadSelector;

    private CharSequence mFormattedSender;
    // We must initialize this to something, in case the timestamp of the message is zero (which
    // should be very rare); this is otherwise set in setTimestamp
    private CharSequence mFormattedDate = "";

    private void init(Context context) {
        mContext = context;
        if (!sInit) {
            Resources r = context.getResources();
            sSubjectDescription = r.getString(R.string.message_subject_description).concat(", ");
            sSubjectEmptyDescription = r.getString(R.string.message_is_empty_description);
            sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider);
            sItemHeightWide =
                r.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
            sItemHeightNormal =
                r.getDimensionPixelSize(R.dimen.message_list_item_height_normal);

            sDefaultPaint.setTypeface(Typeface.DEFAULT);
            sDefaultPaint.setAntiAlias(true);
            sDatePaint.setTypeface(Typeface.DEFAULT);
            sDatePaint.setAntiAlias(true);
            sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
            sBoldPaint.setAntiAlias(true);

            sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment);
            sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_invite_holo_light);
            sBadgeMargin = r.getDimensionPixelSize(R.dimen.message_list_badge_margin);
            sFavoriteIconOff =
                BitmapFactory.decodeResource(r, R.drawable.btn_star_off_normal_email_holo_light);
            sFavoriteIconOn =
                BitmapFactory.decodeResource(r, R.drawable.btn_star_on_normal_email_holo_light);
            sSelectedIconOff =
                BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light);
            sSelectedIconOn =
                BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);

            sStateReplied =
                BitmapFactory.decodeResource(r, R.drawable.ic_badge_reply_holo_light);
            sStateForwarded =
                BitmapFactory.decodeResource(r, R.drawable.ic_badge_forward_holo_light);
            sStateRepliedAndForwarded =
                BitmapFactory.decodeResource(r, R.drawable.ic_badge_reply_forward_holo_light);

            DEFAULT_TEXT_COLOR = r.getColor(R.color.default_text_color);
            ACTIVATED_TEXT_COLOR = r.getColor(android.R.color.white);
            SUBJECT_TEXT_COLOR_READ = r.getColor(R.color.subject_text_color_read);
            SUBJECT_TEXT_COLOR_UNREAD = r.getColor(R.color.subject_text_color_unread);
            SNIPPET_TEXT_COLOR_READ = r.getColor(R.color.snippet_text_color_read);
            SNIPPET_TEXT_COLOR_UNREAD = r.getColor(R.color.snippet_text_color_unread);
            SENDERS_TEXT_COLOR_READ = r.getColor(R.color.senders_text_color_read);
            SENDERS_TEXT_COLOR_UNREAD = r.getColor(R.color.senders_text_color_unread);
            DATE_TEXT_COLOR_READ = r.getColor(R.color.date_text_color_read);
            DATE_TEXT_COLOR_UNREAD = r.getColor(R.color.date_text_color_unread);

            sInit = true;
        }
    }

    /**
     * Invalidate all drawing caches associated with drawing message list items.
     * This is an expensive operation, and should be done rarely, such as when system font size
     * changes occurs.
     */
    public static void resetDrawingCaches() {
        MessageListItemCoordinates.resetCaches();
        sInit = false;
    }

    /**
     * Sets message subject and snippet safely, ensuring the cache is invalidated.
     */
    public void setText(String subject, String snippet, boolean forceUpdate) {
        boolean changed = false;
        if (!Objects.equal(mSubject, subject)) {
            mSubject = subject;
            changed = true;
            populateContentDescription();
        }

        if (!Objects.equal(mSnippet, snippet)) {
            mSnippet = snippet;
            changed = true;
        }

        if (forceUpdate || changed || (mSubject == null && mSnippet == null) /* first time */) {
            SpannableStringBuilder ssb = new SpannableStringBuilder();
            boolean hasSubject = false;
            if (!TextUtils.isEmpty(mSubject)) {
                SpannableString ss = new SpannableString(mSubject);
                ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                ssb.append(ss);
                hasSubject = true;
            }
            if (!TextUtils.isEmpty(mSnippet)) {
                if (hasSubject) {
                    ssb.append(sSubjectSnippetDivider);
                }
                ssb.append(mSnippet);
            }
            mText = ssb;
            requestLayout();
        }
    }

    long mTimeFormatted = 0;

    public void setTimestamp(long timestamp) {
        if (mTimeFormatted != timestamp) {
            mFormattedDate = DateUtils.getRelativeTimeSpanString(mContext, timestamp).toString();
            mTimeFormatted = timestamp;
        }
    }

    /**
     * Determine the mode of this view (WIDE or NORMAL)
     *
     * @param width The width of the view
     * @return The mode of the view
     */
    private int getViewMode(int width) {
        return MessageListItemCoordinates.getMode(mContext, width, mIsSearchResult);
    }

    private Drawable mCurentBackground = null; // Only used by updateBackground()

    private void updateBackground() {
        final Drawable newBackground;
        boolean isMultiPane = MessageListItemCoordinates.isMultiPane(mContext);
        if (mRead) {
            if (isMultiPane && mLayout.isLeftPaneVisible()) {
                if (mWideReadSelector == null) {
                    mWideReadSelector = getContext().getResources()
                            .getDrawable(R.drawable.conversation_wide_read_selector);
                }
                newBackground = mWideReadSelector;
            } else {
                if (mReadSelector == null) {
                    mReadSelector = getContext().getResources()
                            .getDrawable(R.drawable.conversation_read_selector);
                }
                newBackground = mReadSelector;
            }
        } else {
            if (isMultiPane && mLayout.isLeftPaneVisible()) {
                if (mWideUnreadSelector == null) {
                    mWideUnreadSelector = getContext().getResources().getDrawable(
                            R.drawable.conversation_wide_unread_selector);
                }
                newBackground = mWideUnreadSelector;
            } else {
                if (mUnreadSelector == null) {
                    mUnreadSelector = getContext().getResources()
                            .getDrawable(R.drawable.conversation_unread_selector);
                }
                newBackground = mUnreadSelector;
            }
        }
        if (newBackground != mCurentBackground) {
            // setBackgroundDrawable is a heavy operation.  Only call it when really needed.
            setBackgroundDrawable(newBackground);
            mCurentBackground = newBackground;
        }
    }

    private void calculateSubjectText() {
        if (mText == null || mText.length() == 0) {
            return;
        }
        boolean hasSubject = false;
        int snippetStart = 0;
        if (!TextUtils.isEmpty(mSubject)) {
            int subjectColor = getFontColor(mRead ? SUBJECT_TEXT_COLOR_READ
                    : SUBJECT_TEXT_COLOR_UNREAD);
            mText.setSpan(new ForegroundColorSpan(subjectColor), 0, mSubject.length(),
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            snippetStart = mSubject.length() + 1;
        }
        if (!TextUtils.isEmpty(mSnippet)) {
            int snippetColor = getFontColor(mRead ? SNIPPET_TEXT_COLOR_READ
                    : SNIPPET_TEXT_COLOR_UNREAD);
            mText.setSpan(new ForegroundColorSpan(snippetColor), snippetStart, mText.length(),
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }

    private void calculateDrawingData() {
        sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
        calculateSubjectText();
        mSubjectLayout = new StaticLayout(mText, sDefaultPaint,
                mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, false /* includePad */);
        if (mCoordinates.subjectLineCount < mSubjectLayout.getLineCount()) {
            // TODO: ellipsize.
            int end = mSubjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
            mSubjectLayout = new StaticLayout(mText.subSequence(0, end),
                    sDefaultPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
        }

        // Now, format the sender for its width
        TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
        // And get the ellipsized string for the calculated width
        if (TextUtils.isEmpty(mSender)) {
            mFormattedSender = "";
        } else {
            int senderWidth = mCoordinates.sendersWidth;
            senderPaint.setTextSize(mCoordinates.sendersFontSize);
            senderPaint.setColor(getFontColor(mRead ? SENDERS_TEXT_COLOR_READ
                    : SENDERS_TEXT_COLOR_UNREAD));
            mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth,
                    TruncateAt.END);
        }
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (widthMeasureSpec != 0 || mViewWidth == 0) {
            mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
            int mode = getViewMode(mViewWidth);
            if (mode != mMode) {
                mMode = mode;
            }
            mViewHeight = measureHeight(heightMeasureSpec, mMode);
        }
        setMeasuredDimension(mViewWidth, mViewHeight);
    }

    /**
     * Determine the height of this view
     *
     * @param measureSpec A measureSpec packed into an int
     * @param mode The current mode of this view
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec, int mode) {
        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 (mMode == MODE_WIDE) {
                result = sItemHeightWide;
            } else {
                result = sItemHeightNormal;
            }
            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;
    }

    @Override
    public void draw(Canvas canvas) {
        // Update the background, before View.draw() draws it.
        setSelected(mAdapter.isSelected(this));
        updateBackground();
        super.draw(canvas);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        mCoordinates = MessageListItemCoordinates.forWidth(mContext, mViewWidth, mIsSearchResult);
        calculateDrawingData();
    }

    private int getFontColor(int defaultColor) {
        return isActivated() && MessageListItemCoordinates.isMultiPane(mContext) ?
                ACTIVATED_TEXT_COLOR : defaultColor;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // Draw the color chip indicating the mailbox this belongs to
        if (mColorChipPaint != null) {
            canvas.drawRect(
                    mCoordinates.chipX, mCoordinates.chipY,
                    mCoordinates.chipX + mCoordinates.chipWidth,
                    mCoordinates.chipY + mCoordinates.chipHeight,
                    mColorChipPaint);
        }

        // Draw the checkbox
        canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
                mCoordinates.checkmarkX, mCoordinates.checkmarkY, null);

        // Draw the sender name
        Paint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
        senderPaint.setColor(getFontColor(mRead ? SENDERS_TEXT_COLOR_READ
                : SENDERS_TEXT_COLOR_UNREAD));
        senderPaint.setTextSize(mCoordinates.sendersFontSize);
        canvas.drawText(mFormattedSender, 0, mFormattedSender.length(),
                mCoordinates.sendersX, mCoordinates.sendersY - mCoordinates.sendersAscent,
                senderPaint);

        // Draw the reply state. Draw nothing if neither replied nor forwarded.
        if (mHasBeenRepliedTo && mHasBeenForwarded) {
            canvas.drawBitmap(sStateRepliedAndForwarded,
                    mCoordinates.stateX, mCoordinates.stateY, null);
        } else if (mHasBeenRepliedTo) {
            canvas.drawBitmap(sStateReplied,
                    mCoordinates.stateX, mCoordinates.stateY, null);
        } else if (mHasBeenForwarded) {
            canvas.drawBitmap(sStateForwarded,
                    mCoordinates.stateX, mCoordinates.stateY, null);
        }

        // Subject and snippet.
        sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
        canvas.save();
        canvas.translate(
                mCoordinates.subjectX,
                mCoordinates.subjectY);
        mSubjectLayout.draw(canvas);
        canvas.restore();

        // Draw the date
        sDatePaint.setTextSize(mCoordinates.dateFontSize);
        sDatePaint.setColor(mRead ? DATE_TEXT_COLOR_READ : DATE_TEXT_COLOR_UNREAD);
        int dateX = mCoordinates.dateXEnd
                - (int) sDatePaint.measureText(mFormattedDate, 0, mFormattedDate.length());

        canvas.drawText(mFormattedDate, 0, mFormattedDate.length(),
                dateX, mCoordinates.dateY - mCoordinates.dateAscent, sDatePaint);

        // Draw the favorite icon
        canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff,
                mCoordinates.starX, mCoordinates.starY, null);

        // TODO: deal with the icon layouts better from the coordinate class so that this logic
        // doesn't have to exist.
        // Draw the attachment and invite icons, if necessary.
        int iconsLeft = dateX - sBadgeMargin;
        if (mHasAttachment) {
            iconsLeft = iconsLeft - sAttachmentIcon.getWidth();
            canvas.drawBitmap(sAttachmentIcon, iconsLeft, mCoordinates.paperclipY, null);
        }
        if (mHasInvite) {
            iconsLeft -= sInviteIcon.getWidth();
            canvas.drawBitmap(sInviteIcon, iconsLeft, mCoordinates.paperclipY, null);
        }

    }

    /**
     * Called by the adapter at bindView() time
     *
     * @param adapter the adapter that creates this view
     * @param layout If this is a three pane implementation, the
     *            ThreePaneLayout. Otherwise, null.
     */
    public void bindViewInit(MessagesAdapter adapter, ThreePaneLayout layout,
            boolean isSearchResult) {
        mLayout = layout;
        mAdapter = adapter;
        mIsSearchResult = isSearchResult;
        requestLayout();
    }

    private static final int TOUCH_SLOP = 24;
    private static int sScaledTouchSlop = -1;

    private void initializeSlop(Context context) {
        if (sScaledTouchSlop == -1) {
            final Resources res = context.getResources();
            final Configuration config = res.getConfiguration();
            final float density = res.getDisplayMetrics().density;
            final float sizeAndDensity;
            if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) {
                sizeAndDensity = density * 1.5f;
            } else {
                sizeAndDensity = density;
            }
            sScaledTouchSlop = (int) (sizeAndDensity * TOUCH_SLOP + 0.5f);
        }
    }

    /**
     * Overriding this method allows us to "catch" clicks in the checkbox or star
     * and process them accordingly.
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        initializeSlop(getContext());

        boolean handled = false;
        int touchX = (int) event.getX();
        int checkRight = mCoordinates.checkmarkX
                + mCoordinates.checkmarkWidthIncludingMargins + sScaledTouchSlop;
        int starLeft = mCoordinates.starX - sScaledTouchSlop;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (touchX < checkRight || touchX > starLeft) {
                    mDownEvent = true;
                    if ((touchX < checkRight) || (touchX > starLeft)) {
                        handled = true;
                    }
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                mDownEvent = false;
                break;

            case MotionEvent.ACTION_UP:
                if (mDownEvent) {
                    if (touchX < checkRight) {
                        mAdapter.toggleSelected(this);
                        handled = true;
                    } else if (touchX > starLeft) {
                        mIsFavorite = !mIsFavorite;
                        mAdapter.updateFavorite(this, mIsFavorite);
                        handled = true;
                    }
                }
                break;
        }

        if (handled) {
            invalidate();
        } else {
            handled = super.onTouchEvent(event);
        }

        return handled;
    }

    @Override
    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
        event.setClassName(getClass().getName());
        event.setPackageName(getContext().getPackageName());
        event.setEnabled(true);
        event.setContentDescription(getContentDescription());
        return true;
    }

    /**
     * Sets the content description for this item, used for accessibility.
     */
    private void populateContentDescription() {
        if (!TextUtils.isEmpty(mSubject)) {
            setContentDescription(sSubjectDescription + mSubject);
        } else {
            setContentDescription(sSubjectEmptyDescription);
        }
    }
}
