/**
 * Copyright 2010-present Facebook.
 *
 * 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.facebook.widget;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.facebook.android.R;
import com.facebook.internal.*;

/**
 * This class provides the UI for displaying the Facebook Like button and its associated components.
 */
public class LikeView extends FrameLayout {

    // ***
    // Keep all the enum values in sync with attrs.xml
    // ***

    /**
     * Encapsulates the valid values for the facebook:style attribute for a LikeView
     */
    public enum Style {
        /**
         * Setting the attribute to this value will display the button and a sentence near it that describes the
         * social sentence for the associated object.
         *
         * This is the default value
         */
        STANDARD("standard", 0),

        /**
         * Setting the attribute to this value will display the button by itself, with no other components
         */
        BUTTON("button", 1),

        /**
         * Setting the attribute to this value will display the button and a box near it with the number of likes
         * for the associated object
         */
        BOX_COUNT("box_count", 2);

        static Style DEFAULT = STANDARD;

        static Style fromInt(int enumValue) {
            for (Style style : values()) {
                if (style.getValue() == enumValue) {
                    return style;
                }
            }

            return null;
        }

        private String stringValue;
        private int intValue;
        private Style(String stringValue, int value) {
            this.stringValue = stringValue;
            this.intValue = value;
        }

        @Override
        public String toString() {
            return stringValue;
        }

        private int getValue() {
            return intValue;
        }
    }

    /**
     * Encapsulates the valid values for the facebook:horizontal_alignment attribute for a LikeView.
     */
    public enum HorizontalAlignment {
        /**
         * Setting the attribute to this value will center the button and auxiliary view in the parent view.
         *
         * This is the default value
         */
        CENTER("center", 0),

        /**
         * Setting the attribute to this value will left-justify the button and auxiliary view in the parent view.
         */
        LEFT("left", 1),

        /**
         * Setting the attribute to this value will right-justify the button and auxiliary view in the parent view.
         * If the facebook:auxiliary_view_position is set to INLINE, then the auxiliary view will be on the
         * left of the button
         */
        RIGHT("right", 2);

        static HorizontalAlignment DEFAULT = CENTER;

        static HorizontalAlignment fromInt(int enumValue) {
            for (HorizontalAlignment horizontalAlignment : values()) {
                if (horizontalAlignment.getValue() == enumValue) {
                    return horizontalAlignment;
                }
            }

            return null;
        }

        private String stringValue;
        private int intValue;
        private HorizontalAlignment(String stringValue, int value) {
            this.stringValue = stringValue;
            this.intValue = value;
        }

        @Override
        public String toString() {
            return stringValue;
        }

        private int getValue() {
            return intValue;
        }
    }

    /**
     * Encapsulates the valid values for the facebook:auxiliary_view_position attribute for a LikeView.
     */
    public enum AuxiliaryViewPosition {
        /**
         * Setting the attribute to this value will put the social-sentence or box-count below the like button.
         * If the facebook:style is set to BUTTON, then this has no effect.
         *
         * This is the default value
         */
        BOTTOM("bottom", 0),

        /**
         * Setting the attribute to this value will put the social-sentence or box-count inline with the like button.
         * The auxiliary view will be to the left of the button if the facebook:horizontal_alignment is set to RIGHT.
         * In all other cases, it will be to the right of the button.
         * If the facebook:style is set to BUTTON, then this has no effect.
         */
        INLINE("inline", 1),

        /**
         * Setting the attribute to this value will put the social-sentence or box-count above the like button.
         * If the facebook:style is set to BUTTON, then this has no effect.
         */
        TOP("top", 2);

        static AuxiliaryViewPosition DEFAULT = BOTTOM;

        static AuxiliaryViewPosition fromInt(int enumValue) {
            for (AuxiliaryViewPosition auxViewPosition : values()) {
                if (auxViewPosition.getValue() == enumValue) {
                    return auxViewPosition;
                }
            }

            return null;
        }

        private String stringValue;
        private int intValue;
        private AuxiliaryViewPosition(String stringValue, int value) {
            this.stringValue = stringValue;
            this.intValue = value;
        }

        @Override
        public String toString() {
            return stringValue;
        }

        private int getValue() {
            return intValue;
        }
    }

    private static final int NO_FOREGROUND_COLOR = -1;

    private String objectId;
    private LinearLayout containerView;
    private LikeButton likeButton;
    private LikeBoxCountView likeBoxCountView;
    private TextView socialSentenceView;
    private LikeActionController likeActionController;
    private OnErrorListener onErrorListener;
    private BroadcastReceiver broadcastReceiver;
    private LikeActionControllerCreationCallback creationCallback;

    private Style likeViewStyle = Style.DEFAULT;
    private HorizontalAlignment horizontalAlignment = HorizontalAlignment.DEFAULT;
    private AuxiliaryViewPosition auxiliaryViewPosition = AuxiliaryViewPosition.DEFAULT;
    private int foregroundColor = NO_FOREGROUND_COLOR;

    private int edgePadding;
    private int internalPadding;

    /**
     * If your app does not use UiLifeCycleHelper, then you must call this method in the calling activity's
     * onActivityResult method, to process any pending like actions, where tapping the button had resulted in
     * the Like dialog being shown in the Facebook application.
     *
     * @param context Hosting context
     * @param requestCode From the originating call to onActivityResult
     * @param resultCode From the originating call to onActivityResult
     * @param data From the originating call to onActivityResult
     * @return Indication of whether the Intent was handled
     */
    public static boolean handleOnActivityResult(Context context,
                                                 int requestCode,
                                                 int resultCode,
                                                 Intent data) {
        return LikeActionController.handleOnActivityResult(context, requestCode, resultCode, data);
    }

    /**
     * Constructor
     *
     * @param context Context for this View
     */
    public LikeView(Context context) {
        super(context);
        initialize(context);
    }

    /**
     * Constructor
     *
     * @param context Context for this View
     * @param attrs   AttributeSet for this View.
     */
    public LikeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        parseAttributes(attrs);
        initialize(context);
    }

    /**
     * Sets the associated object for this LikeView. Can be changed during runtime.
     * @param objectId Object Id
     */
    public void setObjectId(String objectId) {
        objectId = Utility.coerceValueIfNullOrEmpty(objectId, null);
        if (!Utility.areObjectsEqual(objectId, this.objectId)) {
            setObjectIdForced(objectId);

            updateLikeStateAndLayout();
        }
    }

    /**
     * Sets the facebook:style for this LikeView. Can be changed during runtime.
     * @param likeViewStyle Should be either LikeView.STANDARD, LikeView.BUTTON or LikeView.BOX_COUNT
     */
    public void setLikeViewStyle(Style likeViewStyle) {
        likeViewStyle = likeViewStyle != null ? likeViewStyle : Style.DEFAULT;
        if (this.likeViewStyle != likeViewStyle) {
            this.likeViewStyle = likeViewStyle;

            updateLayout();
        }
    }

    /**
     * Sets the facebook:auxiliary_view_position for this LikeView. Can be changed during runtime.
     * @param auxiliaryViewPosition Should be either LikeView.TOP, LikeView.INLINE or LikeView.BOTTOM
     */
    public void setAuxiliaryViewPosition(AuxiliaryViewPosition auxiliaryViewPosition) {
        auxiliaryViewPosition = auxiliaryViewPosition != null ? auxiliaryViewPosition : AuxiliaryViewPosition.DEFAULT;
        if (this.auxiliaryViewPosition != auxiliaryViewPosition) {
            this.auxiliaryViewPosition = auxiliaryViewPosition;

            updateLayout();
        }
    }

    /**
     * Sets the facebook:horizontal_alignment for this LikeView. Can be changed during runtime.
     * @param horizontalAlignment Should be either LikeView.LEFT, LikeView.CENTER or LikeView.RIGHT
     */
    public void setHorizontalAlignment(HorizontalAlignment horizontalAlignment) {
        horizontalAlignment = horizontalAlignment != null ? horizontalAlignment : HorizontalAlignment.DEFAULT;
        if (this.horizontalAlignment != horizontalAlignment) {
            this.horizontalAlignment = horizontalAlignment;

            updateLayout();
        }
    }

    /**
     * Sets the facebook:foreground_color for this LikeView. Can be changed during runtime.
     * The color is only used for the social sentence text.
     * @param foregroundColor And valid android.graphics.Color value.
     */
    public void setForegroundColor(int foregroundColor) {
        if (this.foregroundColor != foregroundColor) {
            socialSentenceView.setTextColor(foregroundColor);
        }
    }

    /**
     * Sets an OnErrorListener for this instance of LikeView to call into when
     * certain exceptions occur.
     *
     * @param onErrorListener The listener object to set
     */
    public void setOnErrorListener(OnErrorListener onErrorListener) {
        this.onErrorListener = onErrorListener;
    }

    /**
     * Returns the current OnErrorListener for this instance of LikeView.
     *
     * @return The OnErrorListener
     */
    public OnErrorListener getOnErrorListener() {
        return onErrorListener;
    }

    @Override
    protected void onDetachedFromWindow() {
        // Disassociate from the object
        setObjectId(null);

        super.onDetachedFromWindow();
    }

    private void parseAttributes(AttributeSet attrs) {
        if (attrs == null || getContext() == null) {
            return;
        }

        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.com_facebook_like_view);
        if (a == null) {
            return;
        }

        objectId = Utility.coerceValueIfNullOrEmpty(a.getString(R.styleable.com_facebook_like_view_object_id), null);
        likeViewStyle = Style.fromInt(
                a.getInt(R.styleable.com_facebook_like_view_style,
                        Style.DEFAULT.getValue()));
        if (likeViewStyle == null) {
            throw new IllegalArgumentException("Unsupported value for LikeView 'style'");
        }

        auxiliaryViewPosition = AuxiliaryViewPosition.fromInt(
                a.getInt(R.styleable.com_facebook_like_view_auxiliary_view_position,
                        AuxiliaryViewPosition.DEFAULT.getValue()));
        if (auxiliaryViewPosition == null) {
            throw new IllegalArgumentException("Unsupported value for LikeView 'auxiliary_view_position'");
        }

        horizontalAlignment = HorizontalAlignment.fromInt(
                a.getInt(R.styleable.com_facebook_like_view_horizontal_alignment,
                        HorizontalAlignment.DEFAULT.getValue()));
        if (horizontalAlignment == null) {
            throw new IllegalArgumentException("Unsupported value for LikeView 'horizontal_alignment'");
        }

        foregroundColor = a.getColor(R.styleable.com_facebook_like_view_foreground_color, NO_FOREGROUND_COLOR);

        a.recycle();
    }

    // If attributes were present, parseAttributes MUST be called before initialize() to ensure proper behavior
    private void initialize(Context context) {
        edgePadding = getResources().getDimensionPixelSize(R.dimen.com_facebook_likeview_edge_padding);
        internalPadding = getResources().getDimensionPixelSize(R.dimen.com_facebook_likeview_internal_padding);
        if (foregroundColor == NO_FOREGROUND_COLOR) {
            foregroundColor = getResources().getColor(R.color.com_facebook_likeview_text_color);
        }

        setBackgroundColor(Color.TRANSPARENT);

        containerView = new LinearLayout(context);
        LayoutParams containerViewLayoutParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);
        containerView.setLayoutParams(containerViewLayoutParams);

        initializeLikeButton(context);
        initializeSocialSentenceView(context);
        initializeLikeCountView(context);

        containerView.addView(likeButton);
        containerView.addView(socialSentenceView);
        containerView.addView(likeBoxCountView);

        addView(containerView);

        setObjectIdForced(this.objectId);
        updateLikeStateAndLayout();
    }

    private void initializeLikeButton(Context context) {
        likeButton = new LikeButton(
                context,
                likeActionController != null ? likeActionController.isObjectLiked() : false);
        likeButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                toggleLike();
            }
        });

        LinearLayout.LayoutParams buttonLayout = new LinearLayout.LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);

        likeButton.setLayoutParams(buttonLayout);
    }

    private void initializeSocialSentenceView(Context context) {
        socialSentenceView = new TextView(context);
        socialSentenceView.setTextSize(
                TypedValue.COMPLEX_UNIT_PX,
                getResources().getDimension(R.dimen.com_facebook_likeview_text_size));
        socialSentenceView.setMaxLines(2);
        socialSentenceView.setTextColor(foregroundColor);
        socialSentenceView.setGravity(Gravity.CENTER);

        LinearLayout.LayoutParams socialSentenceViewLayout = new LinearLayout.LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        socialSentenceView.setLayoutParams(socialSentenceViewLayout);
    }

    private void initializeLikeCountView(Context context) {
        likeBoxCountView = new LikeBoxCountView(context);

        LinearLayout.LayoutParams likeCountViewLayout = new LinearLayout.LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
        likeBoxCountView.setLayoutParams(likeCountViewLayout);
    }

    private void toggleLike() {
        if (likeActionController != null) {
            Activity activity = (Activity)getContext();
            likeActionController.toggleLike(activity, getAnalyticsParameters());
        }
    }

    private Bundle getAnalyticsParameters() {
        Bundle params = new Bundle();
        params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_STYLE, likeViewStyle.toString());
        params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_AUXILIARY_POSITION, auxiliaryViewPosition.toString());
        params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_HORIZONTAL_ALIGNMENT, horizontalAlignment.toString());
        params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_OBJECT_ID, Utility.coerceValueIfNullOrEmpty(objectId, ""));
        return params;
    }

    private void setObjectIdForced(String newObjectId) {
        tearDownObjectAssociations();

        objectId = newObjectId;
        if (Utility.isNullOrEmpty(newObjectId)) {
            return;
        }

        creationCallback = new LikeActionControllerCreationCallback();
        LikeActionController.getControllerForObjectId(
                getContext(),
                newObjectId,
                creationCallback);
    }

    private void associateWithLikeActionController(LikeActionController likeActionController) {
        this.likeActionController = likeActionController;

        this.broadcastReceiver = new LikeControllerBroadcastReceiver();
        LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getContext());

        // add the broadcast receiver
        IntentFilter filter = new IntentFilter();
        filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_UPDATED);
        filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR);
        filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_RESET);

        localBroadcastManager.registerReceiver(broadcastReceiver, filter);
    }

    private void tearDownObjectAssociations() {
        if (broadcastReceiver != null) {
            LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getContext());
            localBroadcastManager.unregisterReceiver(broadcastReceiver);

            broadcastReceiver = null;
        }

        // If we were already waiting on a controller to be given back, make sure we aren't waiting anymore.
        // Otherwise when that controller is given back to the callback, it will go and register a broadcast receiver
        // for it.
        if (creationCallback != null) {
            creationCallback.cancel();

            creationCallback = null;
        }

        likeActionController = null;
    }

    private void updateLikeStateAndLayout() {
        if (likeActionController == null) {
            likeButton.setLikeState(false);
            socialSentenceView.setText(null);
            likeBoxCountView.setText(null);
        } else {
            likeButton.setLikeState(likeActionController.isObjectLiked());
            socialSentenceView.setText(likeActionController.getSocialSentence());
            likeBoxCountView.setText(likeActionController.getLikeCountString());
        }

        updateLayout();
    }

    private void updateLayout() {
        // Make sure the container is horizontally aligned according to specifications.
        LayoutParams containerViewLayoutParams = (LayoutParams)containerView.getLayoutParams();
        LinearLayout.LayoutParams buttonLayoutParams = (LinearLayout.LayoutParams)likeButton.getLayoutParams();
        int viewGravity =
                horizontalAlignment == HorizontalAlignment.LEFT ? Gravity.LEFT :
                        horizontalAlignment == HorizontalAlignment.CENTER ? Gravity.CENTER_HORIZONTAL : Gravity.RIGHT;

        containerViewLayoutParams.gravity = viewGravity | Gravity.TOP;
        buttonLayoutParams.gravity = viewGravity;

        // Choose the right auxiliary view to make visible.
        socialSentenceView.setVisibility(GONE);
        likeBoxCountView.setVisibility(GONE);

        View auxView;
        if (likeViewStyle == Style.STANDARD &&
                likeActionController != null &&
                !Utility.isNullOrEmpty(likeActionController.getSocialSentence())) {
            auxView = socialSentenceView;
        } else if (likeViewStyle == Style.BOX_COUNT &&
                likeActionController != null &&
                !Utility.isNullOrEmpty(likeActionController.getLikeCountString())) {
            updateBoxCountCaretPosition();
            auxView = likeBoxCountView;
        } else {
            // No more work to be done.
            return;
        }
        auxView.setVisibility(VISIBLE);

        // Now position the auxiliary view properly
        LinearLayout.LayoutParams auxViewLayoutParams = (LinearLayout.LayoutParams)auxView.getLayoutParams();
        auxViewLayoutParams.gravity = viewGravity;

        containerView.setOrientation(
                auxiliaryViewPosition == AuxiliaryViewPosition.INLINE ?
                        LinearLayout.HORIZONTAL :
                        LinearLayout.VERTICAL);

        if (auxiliaryViewPosition == AuxiliaryViewPosition.TOP ||
                (auxiliaryViewPosition == AuxiliaryViewPosition.INLINE &&
                        horizontalAlignment == HorizontalAlignment.RIGHT)) {
            // Button comes after the auxiliary view. Make sure it is at the end
            containerView.removeView(likeButton);
            containerView.addView(likeButton);
        } else {
            // In all other cases, the button comes first
            containerView.removeView(auxView);
            containerView.addView(auxView);
        }

        switch (auxiliaryViewPosition) {
            case TOP:
                auxView.setPadding(edgePadding, edgePadding, edgePadding, internalPadding);
                break;
            case BOTTOM:
                auxView.setPadding(edgePadding, internalPadding, edgePadding, edgePadding);
                break;
            case INLINE:
                if (horizontalAlignment == HorizontalAlignment.RIGHT) {
                    auxView.setPadding(edgePadding, edgePadding, internalPadding, edgePadding);
                } else {
                    auxView.setPadding(internalPadding, edgePadding, edgePadding, edgePadding);
                }
                break;
        }
    }

    private void updateBoxCountCaretPosition() {
        switch (auxiliaryViewPosition) {
            case TOP:
                likeBoxCountView.setCaretPosition(LikeBoxCountView.LikeBoxCountViewCaretPosition.BOTTOM);
                break;
            case BOTTOM:
                likeBoxCountView.setCaretPosition(LikeBoxCountView.LikeBoxCountViewCaretPosition.TOP);
                break;
            case INLINE:
                likeBoxCountView.setCaretPosition(
                        horizontalAlignment == HorizontalAlignment.RIGHT ?
                                LikeBoxCountView.LikeBoxCountViewCaretPosition.RIGHT :
                                LikeBoxCountView.LikeBoxCountViewCaretPosition.LEFT);
                break;
        }
    }

    /**
     * Callback interface that will be called when a network or other error is encountered
     * while logging in.
     */
    public interface OnErrorListener {
        /**
         * Called when a network or other error is encountered.
         * @param errorBundle     a FacebookException representing the error that was encountered.
         */
        void onError(Bundle errorBundle);
    }

    private class LikeControllerBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String intentAction = intent.getAction();
            Bundle extras = intent.getExtras();
            boolean shouldRespond = true;
            if (extras != null) {
                // See if an Id was set in the broadcast Intent. If it was, treat it as a filter.
                String broadcastObjectId = extras.getString(LikeActionController.ACTION_OBJECT_ID_KEY);
                shouldRespond = Utility.isNullOrEmpty(broadcastObjectId) ||
                        Utility.areObjectsEqual(objectId, broadcastObjectId);
            }

            if (!shouldRespond) {
                return;
            }

            if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_UPDATED.equals(intentAction)) {
                updateLikeStateAndLayout();
            } else if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR.equals(intentAction)) {
                if (onErrorListener != null) {
                    onErrorListener.onError(extras);
                }
            } else if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_RESET.equals(intentAction)) {
                // This will recreate the controller and associated objects
                setObjectIdForced(objectId);
                updateLikeStateAndLayout();
            }
        }
    }

    private class LikeActionControllerCreationCallback implements LikeActionController.CreationCallback {
        private boolean isCancelled;

        public void cancel() {
            isCancelled = true;
        }

        @Override
        public void onComplete(LikeActionController likeActionController) {
            if (isCancelled) {
                return;
            }

            associateWithLikeActionController(likeActionController);
            updateLikeStateAndLayout();

            LikeView.this.creationCallback = null;
        }
    }
}
