/*
 * Copyright 2012 - 2014 Benjamin Weiss
 *
 * 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.gitonway.fineday.widget.crouton;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;

import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * Manages the lifecycle of {@link Crouton}s.
 */
final class Manager extends Handler {
    private static final class Messages {
        private Messages() { /* no-op */
        }

        public static final int DISPLAY_CROUTON = 0xc2007;
        public static final int ADD_CROUTON_TO_VIEW = 0xc20074dd;
        public static final int REMOVE_CROUTON = 0xc2007de1;
    }

    private static Manager INSTANCE;

    private final Queue<Crouton> croutonQueue;

    private Manager() {
        croutonQueue = new LinkedBlockingQueue<Crouton>();
    }

    /**
     * @return The currently used instance of the {@link Manager}.
     */
    static synchronized Manager getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Manager();
        }

        return INSTANCE;
    }

    /**
     * Inserts a {@link Crouton} to be displayed.
     * 
     * @param crouton The {@link Crouton} to be displayed.
     */
    void add(Crouton crouton) {
        if (croutonQueue.size() < 1) {
            croutonQueue.add(crouton);
            displayCrouton();
        }

    }

    /**
     * Displays the next {@link Crouton} within the queue.
     */
    private void displayCrouton() {
        if (croutonQueue.isEmpty()) {
            return;
        }

        // First peek whether the Crouton has an activity.
        final Crouton currentCrouton = croutonQueue.peek();

        // If the activity is null we poll the Crouton off the queue.
        if (null == currentCrouton.getActivity()) {
            croutonQueue.poll();
        }

        if (!currentCrouton.isShowing()) {
            // Display the Crouton
            sendMessage(currentCrouton, Messages.ADD_CROUTON_TO_VIEW);
            if (null != currentCrouton.getLifecycleCallback()) {
                currentCrouton.getLifecycleCallback().onDisplayed();
            }
        } else {
            sendMessageDelayed(currentCrouton, Messages.DISPLAY_CROUTON,
                    calculateCroutonDuration(currentCrouton));
        }
    }

    private long calculateCroutonDuration(Crouton crouton) {
        long croutonDuration = crouton.getConfiguration().durationInMilliseconds;
        croutonDuration += crouton.getInAnimation().getDuration();
        croutonDuration += crouton.getOutAnimation().getDuration();
        return croutonDuration;
    }

    /**
     * Sends a {@link Crouton} within a {@link Message}.
     * 
     * @param crouton The {@link Crouton} that should be sent.
     * @param messageId The {@link Message} id.
     */
    private void sendMessage(Crouton crouton, final int messageId) {
        final Message message = obtainMessage(messageId);
        message.obj = crouton;
        sendMessage(message);
    }

    /**
     * Sends a {@link Crouton} within a delayed {@link Message}.
     * 
     * @param crouton The {@link Crouton} that should be sent.
     * @param messageId The {@link Message} id.
     * @param delay The delay in milliseconds.
     */
    private void sendMessageDelayed(Crouton crouton, final int messageId, final long delay) {
        Message message = obtainMessage(messageId);
        message.obj = crouton;
        sendMessageDelayed(message, delay);
    }

    /*
     * (non-Javadoc)
     * @see android.os.Handler#handleMessage(android.os.Message)
     */
    @Override
    public void handleMessage(Message message) {
        final Crouton crouton = (Crouton) message.obj;
        if (null == crouton) {
            return;
        }
        switch (message.what) {
            case Messages.DISPLAY_CROUTON: {
                displayCrouton();
                break;
            }

            case Messages.ADD_CROUTON_TO_VIEW: {
                addCroutonToView(crouton);
                break;
            }

            case Messages.REMOVE_CROUTON: {
                removeCrouton(crouton);
                if (null != crouton.getLifecycleCallback()) {
                    crouton.getLifecycleCallback().onRemoved();
                }
                break;
            }

            default: {
                super.handleMessage(message);
                break;
            }
        }
    }

    /**
     * Adds a {@link Crouton} to the {@link ViewParent} of it's {@link Activity}
     * .
     * 
     * @param crouton The {@link Crouton} that should be added.
     */
    private void addCroutonToView(final Crouton crouton) {
        // don't add if it is already showing
        if (crouton.isShowing()) {
            return;
        }

        final View croutonView = crouton.getView();
        if (null == croutonView.getParent()) {
            ViewGroup.LayoutParams params = croutonView.getLayoutParams();
            if (null == params) {
                params = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT);
            }
            // display Crouton in ViewGroup is it has been supplied
            if (null != crouton.getViewGroup()) {
                // TODO implement add to last position feature (need to align
                // with how this will be requested for activity)
                if (crouton.getViewGroup() instanceof FrameLayout) {
                    crouton.getViewGroup().addView(croutonView, params);
                } else {
                    crouton.getViewGroup().addView(croutonView, 0, params);
                }
            } else {
                Activity activity = crouton.getActivity();
                if (null == activity || activity.isFinishing()) {
                    return;
                }
                handleTranslucentActionBar((ViewGroup.MarginLayoutParams) params, activity);

                activity.addContentView(croutonView, params);
            }
        }

        croutonView.requestLayout(); // This is needed so the animation can use
                                     // the measured with/height
        ViewTreeObserver observer = croutonView.getViewTreeObserver();
        if (null != observer) {
            observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                @TargetApi(16)
                public void onGlobalLayout() {
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                        croutonView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    } else {
                        croutonView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    }

                    croutonView.startAnimation(crouton.getInAnimation());
                    announceForAccessibilityCompat(crouton.getActivity(), crouton.getText());
                    if (Configuration.DURATION_INFINITE != crouton.getConfiguration().durationInMilliseconds) {
                        sendMessageDelayed(crouton, Messages.REMOVE_CROUTON,
                                crouton.getConfiguration().durationInMilliseconds
                                        + crouton.getInAnimation().getDuration());
                    }
                }
            });
        }
    }

    @TargetApi(19)
    private void handleTranslucentActionBar(ViewGroup.MarginLayoutParams params, Activity activity) {
        // Translucent status is only available as of Android 4.4 Kit Kat.
        // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.) {
        // final int flags = activity.getWindow().getAttributes().flags;
        // final int translucentStatusFlag =
        // WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
        // /* Checks whether translucent status is enabled for this window.
        // * If true, sets the top margin to show the crouton just below the
        // action bar. */
        // if ((flags & translucentStatusFlag) == translucentStatusFlag) {
        // final int actionBarContainerId =
        // Resources.getSystem().getIdentifier("action_bar_container", "id",
        // "android");
        // final View actionBarContainer =
        // activity.findViewById(actionBarContainerId);
        // // The action bar is present: the app is using a Holo theme.
        // if (actionBarContainer != null) {
        // final ViewGroup.MarginLayoutParams marginParams =
        // (ViewGroup.MarginLayoutParams) params;
        // marginParams.topMargin = actionBarContainer.getBottom();
        // }
        // }
        // }
    }

    /**
     * Removes the {@link Crouton}'s view after it's display
     * durationInMilliseconds.
     * 
     * @param crouton The {@link Crouton} added to a {@link ViewGroup} and
     *            should be removed.
     */
    protected void removeCrouton(Crouton crouton) {
        View croutonView = crouton.getView();
        ViewGroup croutonParentView = (ViewGroup) croutonView.getParent();

        if (null != croutonParentView) {
            croutonView.startAnimation(crouton.getOutAnimation());

            // Remove the Crouton from the queue.
            Crouton removed = croutonQueue.poll();

            // Remove the crouton from the view's parent.
            croutonParentView.removeView(croutonView);
            if (null != removed) {
                removed.detachActivity();
                removed.detachViewGroup();
                if (null != removed.getLifecycleCallback()) {
                    removed.getLifecycleCallback().onRemoved();
                }
                removed.detachLifecycleCallback();
            }

            // Send a message to display the next crouton but delay it by the
            // out
            // animation duration to make sure it finishes
            sendMessageDelayed(crouton, Messages.DISPLAY_CROUTON, crouton.getOutAnimation()
                    .getDuration());
        }
    }

    /**
     * Removes a {@link Crouton} immediately, even when it's currently being
     * displayed.
     * 
     * @param crouton The {@link Crouton} that should be removed.
     */
    void removeCroutonImmediately(Crouton crouton) {
        // if Crouton has already been displayed then it may not be in the queue
        // (because it was popped).
        // This ensures the displayed Crouton is removed from its parent
        // immediately, whether another instance
        // of it exists in the queue or not.
        // Note: crouton.isShowing() is false here even if it really is showing,
        // as croutonView object in
        // Crouton seems to be out of sync with reality!
        if (null != crouton.getActivity() && null != crouton.getView()
                && null != crouton.getView().getParent()) {
            ((ViewGroup) crouton.getView().getParent()).removeView(crouton.getView());

            // remove any messages pending for the crouton
            removeAllMessagesForCrouton(crouton);
        }
        // remove any matching croutons from queue
        final Iterator<Crouton> croutonIterator = croutonQueue.iterator();
        while (croutonIterator.hasNext()) {
            final Crouton c = croutonIterator.next();
            if (c.equals(crouton) && (null != c.getActivity())) {
                // remove the crouton from the content view
                removeCroutonFromViewParent(crouton);

                // remove any messages pending for the crouton
                removeAllMessagesForCrouton(c);

                // remove the crouton from the queue
                croutonIterator.remove();

                // we have found our crouton so just break
                break;
            }
        }
    }

    /**
     * Removes all {@link Crouton}s from the queue.
     */
    void clearCroutonQueue() {
        removeAllMessages();

        // remove any views that may already have been added to the activity's
        // content view
        for (Crouton crouton : croutonQueue) {
            removeCroutonFromViewParent(crouton);
        }
        croutonQueue.clear();
    }

    /**
     * Removes all {@link Crouton}s for the provided activity. This will remove
     * crouton from {@link Activity}s content view immediately.
     */
    void clearCroutonsForActivity(Activity activity) {
        Iterator<Crouton> croutonIterator = croutonQueue.iterator();
        while (croutonIterator.hasNext()) {
            Crouton crouton = croutonIterator.next();
            if ((null != crouton.getActivity()) && crouton.getActivity().equals(activity)) {
                // remove the crouton from the content view
                removeCroutonFromViewParent(crouton);

                removeAllMessagesForCrouton(crouton);

                // remove the crouton from the queue
                croutonIterator.remove();
            }
        }
    }

    private void removeCroutonFromViewParent(Crouton crouton) {
        if (crouton.isShowing()) {
            ViewGroup parent = (ViewGroup) crouton.getView().getParent();
            if (null != parent) {
                parent.removeView(crouton.getView());
            }
        }
    }

    private void removeAllMessages() {
        removeMessages(Messages.ADD_CROUTON_TO_VIEW);
        removeMessages(Messages.DISPLAY_CROUTON);
        removeMessages(Messages.REMOVE_CROUTON);
    }

    private void removeAllMessagesForCrouton(Crouton crouton) {
        removeMessages(Messages.ADD_CROUTON_TO_VIEW, crouton);
        removeMessages(Messages.DISPLAY_CROUTON, crouton);
        removeMessages(Messages.REMOVE_CROUTON, crouton);

    }

    /**
     * Generates and dispatches an SDK-specific spoken announcement.
     * <p>
     * For backwards compatibility, we're constructing an event from scratch
     * using the appropriate event type. If your application only targets SDK
     * 16+, you can just call View.announceForAccessibility(CharSequence).
     * </p>
     * <p/>
     * note: AccessibilityManager is only available from API lvl 4.
     * <p/>
     * Adapted from https://http://eyes-free.googlecode.com/files/
     * accessibility_codelab_demos_v2_src.zip via
     * https://github.com/coreform/android-formidable-validation
     * 
     * @param context Used to get {@link AccessibilityManager}
     * @param text The text to announce.
     */
    public static void announceForAccessibilityCompat(Context context, CharSequence text) {
        if (Build.VERSION.SDK_INT >= 4) {
            AccessibilityManager accessibilityManager = (AccessibilityManager) context
                    .getSystemService(
                    Context.ACCESSIBILITY_SERVICE);
            if (!accessibilityManager.isEnabled()) {
                return;
            }

            // Prior to SDK 16, announcements could only be made through FOCUSED
            // events. Jelly Bean (SDK 16) added support for speaking text
            // verbatim
            // using the ANNOUNCEMENT event type.
            final int eventType;
            if (Build.VERSION.SDK_INT < 16) {
                eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED;
            } else {
                eventType = AccessibilityEventCompat.TYPE_ANNOUNCEMENT;
            }

            // Construct an accessibility event with the minimum recommended
            // attributes. An event without a class name or package may be
            // dropped.
            final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
            event.getText().add(text);
            event.setClassName(Manager.class.getName());
            event.setPackageName(context.getPackageName());

            // Sends the event directly through the accessibility manager. If
            // your
            // application only targets SDK 14+, you should just call
            // getParent().requestSendAccessibilityEvent(this, event);
            accessibilityManager.sendAccessibilityEvent(event);
        }
    }

    @Override
    public String toString() {
        return "Manager{" +
                "croutonQueue=" + croutonQueue +
                '}';
    }
}
