/*
 * Copyright (C) 2012 Google Inc.
 *
 * 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 org.holoeverywhere.widget.datetimepicker;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
import android.support.v4.view.accessibility.AccessibilityRecordCompat;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;

import com.nineoldandroids.view.ViewHelper;

import java.util.LinkedList;
import java.util.List;

public abstract class TouchExplorationHelper<T> extends AccessibilityNodeProviderCompat {
    /**
     * Virtual node identifier value for invalid nodes.
     */
    public static final int INVALID_ID = Integer.MIN_VALUE;
    private final Rect mTempScreenRect = new Rect();
    private final Rect mTempParentRect = new Rect();
    private final Rect mTempVisibleRect = new Rect();
    private final int[] mTempGlobalRect = new int[2];
    private final AccessibilityManager mManager;
    private final AccessibilityDelegateCompat mDelegate = new AccessibilityDelegateCompat() {
        @Override
        public void onInitializeAccessibilityEvent(View view, AccessibilityEvent event) {
            super.onInitializeAccessibilityEvent(view, event);
            event.setClassName(view.getClass().getName());
        }

        @Override
        public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfoCompat info) {
            super.onInitializeAccessibilityNodeInfo(view, info);
            info.setClassName(view.getClass().getName());
        }

        @Override
        public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
            return TouchExplorationHelper.this;
        }
    };
    private View mParentView;
    private int mFocusedItemId = INVALID_ID;
    private T mCurrentItem = null;
    private Object mOnHoverListener;

    /**
     * Constructs a new touch exploration helper.
     *
     * @param context The parent context.
     */
    public TouchExplorationHelper(Context context, View parentView) {
        mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
        mParentView = parentView;
    }

    /**
     * @return The current accessibility focused item, or {@code null} if no
     * item is focused.
     */
    public T getFocusedItem() {
        return getItemForId(mFocusedItemId);
    }

    /**
     * Requests accessibility focus be placed on the specified item.
     *
     * @param item The item to place focus on.
     */
    public void setFocusedItem(T item) {
        final int itemId = getIdForItem(item);
        if (itemId == INVALID_ID) {
            return;
        }

        performAction(itemId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
    }

    /**
     * Clears the current accessibility focused item.
     */
    public void clearFocusedItem() {
        final int itemId = mFocusedItemId;
        if (itemId == INVALID_ID) {
            return;
        }

        performAction(itemId, AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
    }

    /**
     * Invalidates cached information about the parent view.
     * <p>
     * You <b>must</b> call this method after adding or removing items from the
     * parent view.
     * </p>
     */
    public void invalidateParent() {
        mParentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    }

    /**
     * Invalidates cached information for a particular item.
     * <p>
     * You <b>must</b> call this method when any of the properties set in
     * {@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} have
     * changed.
     * </p>
     *
     * @param item
     */
    public void invalidateItem(T item) {
        sendEventForItem(item, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    }

    /**
     * Populates an event of the specified type with information about an item
     * and attempts to send it up through the view hierarchy.
     *
     * @param item      The item for which to send an event.
     * @param eventType The type of event to send.
     * @return {@code true} if the event was sent successfully.
     */
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public boolean sendEventForItem(T item, int eventType) {
        if (!mManager.isEnabled() || Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            return false;
        }

        final AccessibilityEvent event = getEventForItem(item, eventType);
        final ViewGroup group = (ViewGroup) mParentView.getParent();

        return group.requestSendAccessibilityEvent(mParentView, event);
    }

    @Override
    public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
        if (virtualViewId == View.NO_ID) {
            return getNodeForParent();
        }

        final T item = getItemForId(virtualViewId);
        if (item == null) {
            return null;
        }

        final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
        populateNodeForItemInternal(item, node);
        return node;
    }

    @Override
    public boolean performAction(int virtualViewId, int action, Bundle arguments) {
        if (virtualViewId == View.NO_ID) {
            return ViewCompat.performAccessibilityAction(mParentView, action, arguments);
        }

        final T item = getItemForId(virtualViewId);
        if (item == null) {
            return false;
        }

        boolean handled = false;

        switch (action) {
            case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
                if (mFocusedItemId != virtualViewId) {
                    mFocusedItemId = virtualViewId;
                    sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                    handled = true;
                }
                break;
            case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
                if (mFocusedItemId == virtualViewId) {
                    mFocusedItemId = INVALID_ID;
                    sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
                    handled = true;
                }
                break;
        }

        handled |= performActionForItem(item, action, arguments);

        return handled;
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public View.OnHoverListener getOnHoverListener() {
        if (mOnHoverListener == null) {
            mOnHoverListener = new View.OnHoverListener() {
                @Override
                public boolean onHover(View view, MotionEvent event) {
                    if (!mManager.isTouchExplorationEnabled()) {
                        return false;
                    }

                    switch (event.getAction()) {
                        case MotionEvent.ACTION_HOVER_ENTER:
                        case MotionEvent.ACTION_HOVER_MOVE:
                            final T item = getItemAt(event.getX(), event.getY());
                            setCurrentItem(item);
                            return true;
                        case MotionEvent.ACTION_HOVER_EXIT:
                            setCurrentItem(null);
                            return true;
                    }

                    return false;
                }
            };
        }
        return (View.OnHoverListener) mOnHoverListener;
    }

    private void setCurrentItem(T item) {
        if (mCurrentItem == item) {
            return;
        }

        if (mCurrentItem != null) {
            sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
        }

        mCurrentItem = item;

        if (mCurrentItem != null) {
            sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
        }
    }

    private AccessibilityEvent getEventForItem(T item, int eventType) {
        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
        final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
        final int virtualDescendantId = getIdForItem(item);

        // Ensure the client has good defaults.
        event.setEnabled(true);

        // Allow the client to populate the event.
        populateEventForItem(item, event);

        if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) {
            throw new RuntimeException(
                    "You must add text or a content description in populateEventForItem()");
        }

        // Don't allow the client to override these properties.
        event.setClassName(item.getClass().getName());
        event.setPackageName(mParentView.getContext().getPackageName());
        record.setSource(mParentView, virtualDescendantId);

        return event;
    }

    private AccessibilityNodeInfoCompat getNodeForParent() {
        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mParentView);
        ViewCompat.onInitializeAccessibilityNodeInfo(mParentView, info);

        final LinkedList<T> items = new LinkedList<T>();
        getVisibleItems(items);

        for (T item : items) {
            final int virtualDescendantId = getIdForItem(item);
            info.addChild(mParentView, virtualDescendantId);
        }

        return info;
    }

    private AccessibilityNodeInfoCompat populateNodeForItemInternal(
            T item, AccessibilityNodeInfoCompat node) {
        final int virtualDescendantId = getIdForItem(item);

        // Ensure the client has good defaults.
        node.setEnabled(true);

        // Allow the client to populate the node.
        populateNodeForItem(item, node);

        if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription())) {
            throw new RuntimeException(
                    "You must add text or a content description in populateNodeForItem()");
        }

        // Don't allow the client to override these properties.
        node.setPackageName(mParentView.getContext().getPackageName());
        node.setClassName(item.getClass().getName());
        node.setParent(mParentView);
        node.setSource(mParentView, virtualDescendantId);

        if (mFocusedItemId == virtualDescendantId) {
            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
        } else {
            node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
        }

        node.getBoundsInParent(mTempParentRect);
        if (mTempParentRect.isEmpty()) {
            throw new RuntimeException("You must set parent bounds in populateNodeForItem()");
        }

        // Set the visibility based on the parent bound.
        if (intersectVisibleToUser(mTempParentRect)) {
            node.setVisibleToUser(true);
            node.setBoundsInParent(mTempParentRect);
        }

        // Calculate screen-relative bound.
        mParentView.getLocationOnScreen(mTempGlobalRect);
        final int offsetX = mTempGlobalRect[0];
        final int offsetY = mTempGlobalRect[1];
        mTempScreenRect.set(mTempParentRect);
        mTempScreenRect.offset(offsetX, offsetY);
        node.setBoundsInScreen(mTempScreenRect);

        return node;
    }

    /**
     * Computes whether the specified {@link Rect} intersects with the visible
     * portion of its parent {@link View}. Modifies {@code localRect} to
     * contain only the visible portion.
     *
     * @param localRect A rectangle in local (parent) coordinates.
     * @return Whether the specified {@link Rect} is visible on the screen.
     */
    private boolean intersectVisibleToUser(Rect localRect) {
        // Missing or empty bounds mean this view is not visible.
        if ((localRect == null) || localRect.isEmpty()) {
            return false;
        }

        // Attached to invisible window means this view is not visible.
        if (mParentView.getWindowVisibility() != View.VISIBLE) {
            return false;
        }

        // An invisible predecessor or one with alpha zero means
        // that this view is not visible to the user.
        Object current = this;
        while (current instanceof View) {
            final View view = (View) current;
            // We have attach info so this view is attached and there is no
            // need to check whether we reach to ViewRootImpl on the way up.

            if ((ViewHelper.getAlpha(view) <= 0) || (view.getVisibility() != View.VISIBLE)) {
                return false;
            }
            current = view.getParent();
        }

        // If no portion of the parent is visible, this view is not visible.
        if (!mParentView.getLocalVisibleRect(mTempVisibleRect)) {
            return false;
        }

        // Check if the view intersects the visible portion of the parent.
        return localRect.intersect(mTempVisibleRect);
    }

    public AccessibilityDelegateCompat getAccessibilityDelegate() {
        return mDelegate;
    }

    /**
     * Performs an accessibility action on the specified item. See
     * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)}.
     * <p>
     * The helper class automatically handles focus management resulting from
     * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} and
     * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}, so
     * typically a developer only needs to handle actions added manually in the
     * {{@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)}
     * method.
     * </p>
     *
     * @param item      The item on which to perform the action.
     * @param action    The accessibility action to perform.
     * @param arguments Arguments for the action, or optionally {@code null}.
     * @return {@code true} if the action was performed successfully.
     */
    protected abstract boolean performActionForItem(T item, int action, Bundle arguments);

    /**
     * Populates an event with information about the specified item.
     * <p>
     * At a minimum, a developer must populate the event text by doing one of
     * the following:
     * <ul>
     * <li>appending text to {@link AccessibilityEvent#getText()}</li>
     * <li>populating a description with
     * {@link AccessibilityEvent#setContentDescription(CharSequence)}</li>
     * </ul>
     * </p>
     *
     * @param item  The item for which to populate the event.
     * @param event The event to populate.
     */
    protected abstract void populateEventForItem(T item, AccessibilityEvent event);

    /**
     * Populates a node with information about the specified item.
     * <p/>
     * At a minimum, a developer must:
     * <ul>
     * <li>populate the event text using
     * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or
     * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)}
     * </li>
     * <li>set the item's parent-relative bounds using
     * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)}
     * </ul>
     *
     * @param item The item for which to populate the node.
     * @param node The node to populate.
     */
    protected abstract void populateNodeForItem(T item, AccessibilityNodeInfoCompat node);

    /**
     * Populates a list with the parent view's visible items.
     * <p>
     * The result of this method is cached until the developer calls
     * {@link #invalidateParent()}.
     * </p>
     *
     * @param items The list to populate with visible items.
     */
    protected abstract void getVisibleItems(List<T> items);

    /**
     * Returns the item under the specified parent-relative coordinates.
     *
     * @param x The parent-relative x coordinate.
     * @param y The parent-relative y coordinate.
     * @return The item under coordinates (x,y).
     */
    protected abstract T getItemAt(float x, float y);

    /**
     * Returns the unique identifier for an item. If the specified item does not
     * exist, returns {@link #INVALID_ID}.
     * <p>
     * This result of this method must be consistent with
     * {@link #getItemForId(int)}.
     * </p>
     *
     * @param item The item whose identifier to return.
     * @return A unique identifier, or {@link #INVALID_ID}.
     */
    protected abstract int getIdForItem(T item);

    /**
     * Returns the item for a unique identifier. If the specified item does not
     * exist, returns {@code null}.
     *
     * @param id The identifier for the item to return.
     * @return An item, or {@code null}.
     */
    protected abstract T getItemForId(int id);
}
