/*
 * SOS
 * Copyright (C) 2016  zDuo (Adithya J, Vazbloke)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

package com.onegravity.contactpicker.picture;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources.Theme;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.graphics.drawable.shapes.PathShape;
import android.graphics.drawable.shapes.RectShape;
import android.graphics.drawable.shapes.Shape;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.QuickContact;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import com.onegravity.contactpicker.Helper;
import com.onegravity.contactpicker.R;

import static com.onegravity.contactpicker.picture.Constants.EMAIL_LOOKUP_PROJECTION;
import static com.onegravity.contactpicker.picture.Constants.PHONE_LOOKUP_PROJECTION;
import static com.onegravity.contactpicker.picture.Constants.TOKEN_EMAIL_LOOKUP;
import static com.onegravity.contactpicker.picture.Constants.TOKEN_PHONE_LOOKUP;

/**
 * The ContactBadge.
 * <p>
 * Derived from {@link android.widget.QuickContactBadge}.
 */
public class ContactBadge extends View implements OnClickListener {
    /**
     * Resize the pictures to the following value (device-independent pixels).
     */
    public static final float STANDARD_PICTURE_SIZE = 40f;
    private final int mSizeInPx;
    protected String[] mExcludeMimes = null;
    private Uri mContactUri;
    private String mContactEmail;
    private String mContactPhone;
    ContactQueryHandler.ContactQueryHandlerCallback mContactQueryHandlerCallback =
            new ContactQueryHandler.ContactQueryHandlerCallback() {
                @Override
                public void onQueryComplete(int token, Uri uri, Bundle extras, boolean trigger, Uri createUri) {
                    assignContactUri(uri);
                    if (trigger && uri != null) {
                        // Found contact, so trigger QuickContact
                        ContactsContract.QuickContact.showQuickContact(getContext(),
                                ContactBadge.this, uri, ContactsContract.QuickContact.MODE_LARGE, mExcludeMimes);
                    } else if (createUri != null) {
                        // Prompt user to add this person to contacts
                        final Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, createUri);
                        if (extras != null) {
                            extras.remove(Constants.EXTRA_URI_CONTENT);
                            intent.putExtras(extras);
                        }
                        getContext().startActivity(intent);
                    }

                }
            };
    private ContactQueryHandler mQueryHandler;
    private Bundle mExtras = null;
    // Bitmap
    private BitmapDrawable mDrawable;
    // Character
    private String mChar;
    private Paint mTextPaint;
    private Rect mRect;
    // circle (round contact picture)
    private Paint mBackground;
    // Chip/Triangle (square contact picture)
    private ShapeDrawable mTriangle;
    private Paint mLinePaint;
    private float mOffset;
    // pressed overlay
    private boolean mIsPressed;
    private ShapeDrawable mPressedOverlay;
    private boolean mRoundContactPictures = true;
    private String mKey;
    private float mDensity;

    public ContactBadge(Context context) {
        this(context, null);
    }

    public ContactBadge(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ContactBadge(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        if (!isInEditMode()) {
            mQueryHandler = new ContactQueryHandler(context, mExcludeMimes);
        }
        setOnClickListener(this);

        mDensity = Helper.getDisplayMetrics(context).density;
        mSizeInPx = Math.round(STANDARD_PICTURE_SIZE * mDensity);

        initBadge(context, mRoundContactPictures);
    }

    private void initBadge(Context context, boolean roundContactPictures) {
        if (mTextPaint == null) {
            // Character
            mTextPaint = new Paint();
            mTextPaint.setAntiAlias(true);
            mTextPaint.setStyle(Paint.Style.FILL);
            mTextPaint.setARGB(255, 255, 255, 255);
            mTextPaint.setTextSize(mSizeInPx * 0.7f); // just scale this down a bit
            mRect = new Rect();
        }

        if (roundContactPictures) {
            initRound(context);
        } else {
            initSquare(context);
        }
    }

    private void initRound(Context context) {
        if (mBackground == null) {
            // background (if there's no bitmap)
            mBackground = new Paint();
            mBackground.setStyle(Paint.Style.FILL);
            mBackground.setAntiAlias(true);
        }

        initOverlay(context, new OvalShape());
    }

    private void initSquare(Context context) {
        if (mTriangle == null) {
            TypedValue typedValue = new TypedValue();
            Theme theme = context.getTheme();

            // triangle
            Path chipPath = new Path();
            chipPath.moveTo(500f, 0f);
            chipPath.lineTo(500f, 500f);
            chipPath.lineTo(0f, 500f);
            chipPath.close();
            mTriangle = new ShapeDrawable(new PathShape(chipPath, 500f, 500f));
            mTriangle.setDither(true);
            int triangleColor = Color.parseColor("#cc1f1f1f");
            if (theme.resolveAttribute(R.attr.cp_badgeTriangleColor, typedValue, true)) {
                triangleColor = typedValue.data;
            }
            mTriangle.getPaint().setColor(triangleColor);

            // line
            mLinePaint = new Paint();
            int lineColor = Color.parseColor("#ffffffff");
            if (theme.resolveAttribute(R.attr.cp_badgeLineColor, typedValue, true)) {
                lineColor = typedValue.data;
            }
            mLinePaint.setColor(lineColor);
            mOffset = 1.5f * mDensity;
            mLinePaint.setStrokeWidth(mOffset);
        }

        initOverlay(context, new RectShape());
    }

    private void initOverlay(Context context, Shape shape) {
        // pressed state
        TypedValue typedValue = new TypedValue();
        Theme theme = context.getTheme();

        mPressedOverlay = new ShapeDrawable(shape);
        int overlayColor = Color.parseColor("#aa888888");
        if (theme.resolveAttribute(R.attr.cp_badgeOverlayColor, typedValue, true)) {
            overlayColor = typedValue.data;
        }
        Paint paint = mPressedOverlay.getPaint();
        paint.setColor(overlayColor);
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
    }

    // ****************************************** Contact Picture Methods *******************************************

    public synchronized void onDestroy() {
        mDrawable = null;
        if (mQueryHandler != null) {
            mQueryHandler.cancelOperation();
        }
        mQueryHandler = null;
    }

    public void setBadgeType(ContactPictureType contactPictureType) {
        mRoundContactPictures = contactPictureType == ContactPictureType.ROUND;
        initBadge(getContext(), mRoundContactPictures);
    }

    /**
     * The "key" is set by the ContactPictureManager to identify the ContactPictureLoader associated
     * with this ContactBadge.
     * Because ContactBadges may be re-used in a layout (list view, grid view etc.) a new loader
     * might be started with a different key while the current loader is still running. To prevent
     * multiple concurrent loaders from overriding each others result, the keys need to match.
     */
    synchronized String getKey() {
        return mKey;
    }

    synchronized void setKey(String key) {
        mKey = key;
    }

    public void setCharacter(Character c, int color) {
        mChar = Character.toString(c);
        mDrawable = null;
        if (mRoundContactPictures) {
            mBackground.setColor(color);
        } else {
            setBackgroundColor(color);
        }
        invalidate();
    }

    // ****************************************** View Methods *******************************************

    public void setBitmap(Bitmap bitmap) {
        mChar = null;
        if (mDrawable == null || mDrawable.getBitmap() != bitmap) {
            mDrawable = new BitmapDrawable(getContext().getResources(), bitmap);
            mKey = null;
            invalidate();
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();

        boolean isPressed = false;
        for (int state : getDrawableState()) {
            if (state == android.R.attr.state_pressed || state == android.R.attr.state_focused) {
                isPressed = true;
                break;
            }
        }

        if (isPressed != mIsPressed) {
            mIsPressed = isPressed;
            invalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int w = getWidth();
        int h = getHeight();

        if (mRoundContactPictures) {
            onDrawCircle(canvas, w, h);
        } else {
            onDrawSquare(canvas, w, h);
        }

        if (mIsPressed) {
            mPressedOverlay.setBounds(0, 0, w, h);
            mPressedOverlay.draw(canvas);
        }
    }

    private void onDrawCircle(Canvas canvas, int w, int h) {
        if (mDrawable != null) {
            // picture
            mDrawable.setBounds(0, 0, w, h);
            mDrawable.draw(canvas);
        } else if (!TextUtils.isEmpty(mChar)) {
            // circle
            float radius = Math.min(w, h) / 2f;
            canvas.drawCircle(radius, radius, radius, mBackground);

            // letter
            mTextPaint.getTextBounds(mChar, 0, 1, mRect);
            float width = mTextPaint.measureText(mChar);
            canvas.drawText(mChar, (mSizeInPx - width) / 2f, (mSizeInPx + mRect.height()) / 2f, mTextPaint);
        }
    }

    private void onDrawSquare(Canvas canvas, int w, int h) {
        if (mDrawable != null) {
            // picture
            mDrawable.setBounds(0, 0, w, h);
            mDrawable.draw(canvas);
        } else if (!TextUtils.isEmpty(mChar)) {
            // letter
            mTextPaint.getTextBounds(mChar, 0, 1, mRect);
            float width = mTextPaint.measureText(mChar);
            canvas.drawText(mChar, (mSizeInPx - width) / 2f, (mSizeInPx + mRect.height()) / 2f, mTextPaint);
        }

        // chip / triangle
        if (isEnabled() && mTriangle != null) {
            // triangle
            int size = Math.round(mSizeInPx / 3f - mOffset / 2f);
            mTriangle.setBounds(w - size, h - size, w, h);
            mTriangle.draw(canvas);

            // line
            size = Math.round(mSizeInPx / 3f);
            canvas.drawLine(w - size - mOffset, h + mOffset, w + mOffset, h - size - mOffset, mLinePaint);
        }
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        event.setClassName(ContactBadge.class.getName());
    }

    // ****************************************** Assign Methods *******************************************

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setClassName(ContactBadge.class.getName());
    }

    /**
     * True if a contact, an email address or a phone number has been assigned
     */
    private boolean isAssigned() {
        return mContactUri != null || mContactEmail != null || mContactPhone != null;
    }

    /**
     * Assign the contact uri that this QuickContactBadge should be associated
     * with. Note that this is only used for displaying the QuickContact window and
     * won't bind the contact's photo for you.
     *
     * @param contactUri Either a {@link Contacts#CONTENT_URI} or {@link Contacts#CONTENT_LOOKUP_URI} style URI.
     */
    public void assignContactUri(Uri contactUri) {
        mContactUri = contactUri;
        mContactEmail = null;
        mContactPhone = null;
        onContactUriChanged();
    }

    /**
     * Assign a contact based on an email address. This should only be used when
     * the contact's URI is not available, as an extra query will have to be
     * performed to lookup the URI based on the email.
     *
     * @param emailAddress The email address of the contact.
     * @param lazyLookup   If this is true, the lookup query will not be performed
     *                     until this view is clicked.
     */
    public void assignContactFromEmail(String emailAddress, boolean lazyLookup) {
        assignContactFromEmail(emailAddress, lazyLookup, null);
    }

    /**
     * Assign a contact based on an email address. This should only be used when
     * the contact's URI is not available, as an extra query will have to be
     * performed to lookup the URI based on the email.
     *
     * @param emailAddress The email address of the contact.
     * @param lazyLookup   If this is true, the lookup query will not be performed until this view is
     *                     clicked.
     * @param extras       A bundle of extras to populate the contact edit page with if the contact is not
     *                     found and the user chooses to add the email address to an existing contact or
     *                     create a new contact. Uses the same string constants as those found in
     *                     {@link android.provider.ContactsContract.Intents.Insert}
     */
    public void assignContactFromEmail(String emailAddress, boolean lazyLookup, Bundle extras) {
        mContactEmail = emailAddress;
        mExtras = extras;
        if (!lazyLookup && mQueryHandler != null) {
            mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, null,
                    Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
                    EMAIL_LOOKUP_PROJECTION, null, null, null, mContactQueryHandlerCallback);
        } else {
            mContactUri = null;
            onContactUriChanged();
        }
    }

    /**
     * Assign a contact based on a phone number. This should only be used when the contact's URI is
     * not available, as an extra query will have to be performed to lookup the URI based on the
     * phone number.
     *
     * @param phoneNumber The phone number of the contact.
     * @param lazyLookup  If this is true, the lookup query will not be performed  until this view is
     *                    clicked.
     */
    public void assignContactFromPhone(String phoneNumber, boolean lazyLookup) {
        assignContactFromPhone(phoneNumber, lazyLookup, new Bundle());
    }

    /**
     * Assign a contact based on a phone number. This should only be used when the contact's URI is
     * not available, as an extra query will have to be performed to lookup the URI based on the
     * phone number.
     *
     * @param phoneNumber The phone number of the contact.
     * @param lazyLookup  If this is true, the lookup query will not be performed  until this view is
     *                    clicked.
     * @param extras      A bundle of extras to populate the contact edit page with if the contact is not
     *                    found and the user chooses to add the phone number to an existing contact or
     *                    create a new contact. Uses the same string constants as those found in
     *                    {@link android.provider.ContactsContract.Intents.Insert}
     */
    public void assignContactFromPhone(String phoneNumber, boolean lazyLookup, Bundle extras) {
        mContactPhone = phoneNumber;
        mExtras = extras;
        if (!lazyLookup && mQueryHandler != null) {
            mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, null,
                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
                    PHONE_LOOKUP_PROJECTION, null, null, null, mContactQueryHandlerCallback);
        } else {
            mContactUri = null;
            onContactUriChanged();
        }
    }

    private void onContactUriChanged() {
        setEnabled(isAssigned());
    }

    @Override
    public void onClick(View v) {
        // If contact has been assigned, mExtras should no longer be null, but do a null check
        // anyway just in case assignContactFromPhone or Email was called with a null bundle or
        // wasn't assigned previously.
        final Bundle extras = (mExtras == null) ? new Bundle() : mExtras;
        if (mContactUri != null) {
            QuickContact.showQuickContact(getContext(), ContactBadge.this, mContactUri, QuickContact.MODE_LARGE, mExcludeMimes);
        } else if (mContactEmail != null && mQueryHandler != null) {
            extras.putString(Constants.EXTRA_URI_CONTENT, mContactEmail);
            mQueryHandler.startQuery(Constants.TOKEN_EMAIL_LOOKUP_AND_TRIGGER, extras,
                    Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
                    EMAIL_LOOKUP_PROJECTION, null, null, null, mContactQueryHandlerCallback);
        } else if (mContactPhone != null && mQueryHandler != null) {
            extras.putString(Constants.EXTRA_URI_CONTENT, mContactPhone);
            mQueryHandler.startQuery(Constants.TOKEN_PHONE_LOOKUP_AND_TRIGGER, extras,
                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
                    PHONE_LOOKUP_PROJECTION, null, null, null, mContactQueryHandlerCallback);
        } else {
            // If a contact hasn't been assigned, don't react to click.
            return;
        }
    }

    /**
     * Set a list of specific MIME-types to exclude and not display. For
     * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE}
     * profile icon.
     */
    public void setExcludeMimes(String[] excludeMimes) {
        mExcludeMimes = excludeMimes;
    }

}