/*
 * MIT License
 *
 * Copyright (c) 2016 Alibaba Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.alibaba.android.vlayout.layout;

import com.alibaba.android.vlayout.LayoutHelper;
import com.alibaba.android.vlayout.LayoutManagerHelper;
import com.alibaba.android.vlayout.OrientationHelperEx;
import com.alibaba.android.vlayout.R;
import com.alibaba.android.vlayout.VirtualLayoutManager;
import com.alibaba.android.vlayout.VirtualLayoutManager.LayoutStateWrapper;

import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;

/**
 * {@link com.alibaba.android.vlayout.LayoutHelper} that provides basic methods
 */
public abstract class BaseLayoutHelper extends MarginLayoutHelper {

    private static final String TAG = "BaseLayoutHelper";

    public static boolean DEBUG = false;

    protected Rect mLayoutRegion = new Rect();

    View mLayoutView;

    int mBgColor;

    float mAspectRatio = Float.NaN;

    public BaseLayoutHelper() {

    }

    @Override
    public boolean isFixLayout() {
        return false;
    }

    public int getBgColor() {
        return this.mBgColor;
    }

    /**
     * Set backgroundColor for LayoutView
     *
     * @param bgColor
     */
    public void setBgColor(int bgColor) {
        this.mBgColor = bgColor;
    }

    public void setAspectRatio(float aspectRatio) {
        this.mAspectRatio = aspectRatio;
    }

    public float getAspectRatio() {
        return mAspectRatio;
    }

    private int mItemCount = 0;

    /**
     * The number of items in current layout
     *
     * @return the number of child views
     */
    @Override
    public int getItemCount() {
        return mItemCount;
    }

    @Override
    public void setItemCount(int itemCount) {
        this.mItemCount = itemCount;
    }

    /**
     * Retrieve next view and add it into layout, this is to make sure that view are added by order
     *
     * @param recycler    recycler generate views
     * @param layoutState current layout state
     * @param helper      helper to add views
     * @param result      chunk result to tell layoutManager whether layout process goes end
     * @return next view to render, null if no more view available
     */
    @Nullable
    public final View nextView(RecyclerView.Recycler recycler, LayoutStateWrapper layoutState, LayoutManagerHelper helper, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            if (DEBUG && !layoutState.hasScrapList()) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if there is no more views can be retrieved, this layout process is finished
            result.mFinished = true;
            return null;
        }

        helper.addChildView(layoutState, view);
        return view;
    }


    @Override
    public void beforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
                             LayoutManagerHelper helper) {
        if (DEBUG) {
            Log.d(TAG, "call beforeLayout() on " + this.getClass().getSimpleName());
        }


        if (requireLayoutView()) {
            if (mLayoutView != null) {
                // TODO: recycle LayoutView
                // helper.detachChildView(mLayoutView);
            }
        } else {
            // if no layoutView is required, remove it
            if (mLayoutView != null) {
                if (mLayoutViewUnBindListener != null) {
                    mLayoutViewUnBindListener.onUnbind(mLayoutView, this);
                }
                helper.removeChildView(mLayoutView);
                mLayoutView = null;
            }
        }
    }

    /**
     * Tell whether the scrolled value is valid, if not, means it's a layout processing without scrolling
     *
     * @param scrolled value of how many pixels does scrolled
     * @return true means during a scrolling process, false means during a layout process.
     */
    protected boolean isValidScrolled(int scrolled) {
        return scrolled != Integer.MAX_VALUE && scrolled != Integer.MIN_VALUE;
    }


    @Override
    public void afterLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
                            int startPosition, int endPosition, int scrolled,
                            LayoutManagerHelper helper) {
        if (DEBUG) {
            Log.d(TAG, "call afterLayout() on " + this.getClass().getSimpleName());
        }


        if (requireLayoutView()) {
            if (isValidScrolled(scrolled) && mLayoutView != null) {
                // initial layout do reset
                mLayoutRegion.union(mLayoutView.getLeft(), mLayoutView.getTop(), mLayoutView.getRight(), mLayoutView.getBottom());
            }


            if (!mLayoutRegion.isEmpty()) {
                if (isValidScrolled(scrolled)) {
                    if (helper.getOrientation() == VirtualLayoutManager.VERTICAL) {
                        mLayoutRegion.offset(0, -scrolled);
                    } else {
                        mLayoutRegion.offset(-scrolled, 0);
                    }
                }
                int contentWidth = helper.getContentWidth();
                int contentHeight = helper.getContentHeight();
                if (helper.getOrientation() == VirtualLayoutManager.VERTICAL ?
                        mLayoutRegion.intersects(0, -contentHeight / 4, contentWidth, contentHeight + contentHeight / 4) :
                        mLayoutRegion.intersects(-contentWidth / 4, 0, contentWidth + contentWidth / 4, contentHeight)) {

                    if (mLayoutView == null) {
                        mLayoutView = helper.generateLayoutView();
                        helper.addOffFlowView(mLayoutView, true);
                    }
                    //finally fix layoutRegion's height and with here to avoid visual blank
                    if (helper.getOrientation() == VirtualLayoutManager.VERTICAL) {
                        mLayoutRegion.left = helper.getPaddingLeft() + mMarginLeft;
                        mLayoutRegion.right = helper.getContentWidth() - helper.getPaddingRight() - mMarginRight;
                    } else {
                        mLayoutRegion.top = helper.getPaddingTop() + mMarginTop;
                        mLayoutRegion.bottom = helper.getContentHeight() - helper.getPaddingBottom() - mMarginBottom;
                    }

                    bindLayoutView(mLayoutView);
                    return;
                } else {
                    mLayoutRegion.set(0, 0, 0, 0);
                    if (mLayoutView != null) {
                        mLayoutView.layout(0, 0, 0, 0);
                    }
                }
            }
        }

        if (mLayoutView != null) {
            if (mLayoutViewUnBindListener != null) {
                mLayoutViewUnBindListener.onUnbind(mLayoutView, this);
            }
            helper.removeChildView(mLayoutView);
            mLayoutView = null;
        }

    }

    @Override
    public void adjustLayout(int startPosition, int endPosition, LayoutManagerHelper helper) {
        if (requireLayoutView()) {
            View refer = null;
            Rect tempRect = new Rect();
            final OrientationHelperEx orientationHelper = helper.getMainOrientationHelper();
            for (int i = 0; i < helper.getChildCount(); i++) {
                refer = helper.getChildAt(i);
                int anchorPos = helper.getPosition(refer);
                if (getRange().contains(anchorPos)) {
                    if (refer.getVisibility() == View.GONE) {
                        tempRect.setEmpty();
                    } else {
                        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                            refer.getLayoutParams();
                        if (helper.getOrientation() == VirtualLayoutManager.VERTICAL) {
                            tempRect.union(helper.getDecoratedLeft(refer) - params.leftMargin,
                                orientationHelper.getDecoratedStart(refer),
                                helper.getDecoratedRight(refer) + params.rightMargin,
                                orientationHelper.getDecoratedEnd(refer));
                        } else {
                            tempRect.union(orientationHelper.getDecoratedStart(refer),
                                helper.getDecoratedTop(refer) - params.topMargin, orientationHelper.getDecoratedEnd(refer),
                                helper.getDecoratedBottom(refer) + params.bottomMargin);
                        }
                    }
                }
            }
            if (!tempRect.isEmpty()) {
                mLayoutRegion.set(tempRect.left - mPaddingLeft, tempRect.top - mPaddingTop,
                    tempRect.right + mPaddingRight, tempRect.bottom + mPaddingBottom);
            } else {
                mLayoutRegion.setEmpty();
            }
            if (mLayoutView != null) {
                mLayoutView.layout(mLayoutRegion.left, mLayoutRegion.top, mLayoutRegion.right, mLayoutRegion.bottom);
            }
        }
    }

    /**
     * Called when {@link com.alibaba.android.vlayout.LayoutHelper} get dropped
     * Do default clean jobs defined by framework
     *
     * @param helper LayoutManagerHelper
     */
    @Override
    public final void clear(LayoutManagerHelper helper) {
        // remove LayoutViews if there is one
        if (mLayoutView != null) {
            if (mLayoutViewUnBindListener != null) {
                mLayoutViewUnBindListener.onUnbind(mLayoutView, this);
            }
            helper.removeChildView(mLayoutView);
            mLayoutView = null;
        }

        // call user defined
        onClear(helper);
    }

    /**
     * Called when {@link com.alibaba.android.vlayout.LayoutHelper} get dropped, do clean custom jobs
     *
     * @param helper
     */
    protected void onClear(LayoutManagerHelper helper) {

    }

    /**
     * @return
     */
    @Override
    public boolean requireLayoutView() {
        return mBgColor != 0 || mLayoutViewBindListener != null;
    }

    public abstract void layoutViews(RecyclerView.Recycler recycler, RecyclerView.State state,
                                     LayoutStateWrapper layoutState, LayoutChunkResult result,
                                     LayoutManagerHelper helper);


    @Override
    public void doLayout(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutStateWrapper layoutState, LayoutChunkResult result, LayoutManagerHelper helper) {
        layoutViews(recycler, state, layoutState, result, helper);
    }

    /**
     * Helper function which do layout children and also update layoutRegion
     * but it won't consider margin in layout, so you need take care of margin if you apply margin to your layoutView
     *
     * @param child  child that will be laid
     * @param left   left position
     * @param top    top position
     * @param right  right position
     * @param bottom bottom position
     * @param helper layoutManagerHelper, help to lay child
     */
    protected void layoutChildWithMargin(final View child, int left, int top, int right, int bottom, @NonNull LayoutManagerHelper helper) {
        layoutChildWithMargin(child, left, top, right, bottom, helper, false);
    }

    protected void layoutChildWithMargin(final View child, int left, int top, int right, int bottom, @NonNull LayoutManagerHelper helper, boolean addLayoutRegionWithMargin) {
        helper.layoutChildWithMargins(child, left, top, right, bottom);
        if (requireLayoutView()) {
            if (addLayoutRegionWithMargin) {
                mLayoutRegion
                        .union(left - mPaddingLeft - mMarginLeft, top - mPaddingTop - mMarginTop,
                                right + mPaddingRight + mMarginRight,
                                bottom + mPaddingBottom + mMarginBottom);
            } else {
                mLayoutRegion.union(left - mPaddingLeft, top - mPaddingTop, right + mPaddingRight, bottom + mPaddingBottom);
            }
        }

    }

    /**
     * Helper function which do layout children and also update layoutRegion
     *
     * @param child  child that will be laid
     * @param left   left position
     * @param top    top position
     * @param right  right position
     * @param bottom bottom position
     * @param helper layoutManagerHelper, help to lay child
     */
    protected void layoutChild(final View child, int left, int top, int right, int bottom, @NonNull LayoutManagerHelper helper) {
        layoutChild(child, left, top, right, bottom, helper, false);
    }

    protected void layoutChild(final View child, int left, int top, int right, int bottom, @NonNull LayoutManagerHelper helper, boolean addLayoutRegionWithMargin) {
        helper.layoutChild(child, left, top, right, bottom);
        if (requireLayoutView()) {
            if (addLayoutRegionWithMargin) {
                mLayoutRegion
                        .union(left - mPaddingLeft - mMarginLeft, top - mPaddingTop - mMarginTop,
                                right + mPaddingRight + mMarginRight,
                                bottom + mPaddingBottom + mMarginBottom);
            } else {
                mLayoutRegion.union(left - mPaddingLeft, top - mPaddingTop, right + mPaddingRight, bottom + mPaddingBottom);
            }
        }

    }

    /**
     * Listener to handle LayoutViews, like bgImage
     */
    public interface LayoutViewBindListener {
        void onBind(View layoutView, BaseLayoutHelper baseLayoutHelper);
    }

    /**
     * Listener to handle LayoutViews, like bgImage
     */
    public interface LayoutViewUnBindListener {
        void onUnbind(View layoutView, BaseLayoutHelper baseLayoutHelper);
    }


    public interface LayoutViewHelper {

        /**
         * Implement it by maintaining a map between layoutView and image url or setting a unique tag to view. It's up to your choice.
         * @param layoutView view ready to be binded with an image
*        * @param id layoutView's identifier
         */
        void onBindViewSuccess(View layoutView, String id);
    }

    private LayoutViewUnBindListener mLayoutViewUnBindListener;

    private LayoutViewBindListener mLayoutViewBindListener;

    /**
     * Helper to decide whether call {@link LayoutViewBindListener#onBind(View, BaseLayoutHelper)}.
     * Here is a performance issue: {@link LayoutViewBindListener#onBind(View, BaseLayoutHelper)} is called during layout phase,
     * when binding image to it would cause view tree to relayout, then the same  {@link LayoutViewBindListener#onBind(View, BaseLayoutHelper)} would be called.
     * User should provide enough information to tell LayoutHelper whether image has been bind success.
     * If image has been successfully binded , no more dead loop happens.
     *
     * Of course you can handle this logic by yourself and ignore this helper.
     */
    public static class DefaultLayoutViewHelper implements LayoutViewBindListener, LayoutViewUnBindListener, LayoutViewHelper {

        private final LayoutViewBindListener mLayoutViewBindListener;

        private final LayoutViewUnBindListener mLayoutViewUnBindListener;

        public DefaultLayoutViewHelper(
            LayoutViewBindListener layoutViewBindListener,
            LayoutViewUnBindListener layoutViewUnBindListener) {
            mLayoutViewBindListener = layoutViewBindListener;
            mLayoutViewUnBindListener = layoutViewUnBindListener;
        }

        @Override
        public void onBindViewSuccess(View layoutView, String id) {
            layoutView.setTag(R.id.tag_layout_helper_bg, id);
        }

        @Override
        public void onBind(View layoutView, BaseLayoutHelper baseLayoutHelper) {
            if (layoutView.getTag(R.id.tag_layout_helper_bg) == null) {
                if (mLayoutViewBindListener != null) {
                    mLayoutViewBindListener.onBind(layoutView, baseLayoutHelper);
                }
            }
        }

        @Override
        public void onUnbind(View layoutView, BaseLayoutHelper baseLayoutHelper) {
            if (mLayoutViewUnBindListener != null) {
                mLayoutViewUnBindListener.onUnbind(layoutView, baseLayoutHelper);
            }
            layoutView.setTag(R.id.tag_layout_helper_bg, null);
        }
    }

    public void setLayoutViewHelper(DefaultLayoutViewHelper layoutViewHelper) {
        mLayoutViewBindListener = layoutViewHelper;
        mLayoutViewUnBindListener = layoutViewHelper;
    }

    /**
     * Better to use {@link #setLayoutViewHelper(DefaultLayoutViewHelper)}
     * @param bindListener
     */
    public void setLayoutViewBindListener(LayoutViewBindListener bindListener) {
        mLayoutViewBindListener = bindListener;
    }

    /**
     * Better to use {@link #setLayoutViewHelper(DefaultLayoutViewHelper)}
     * @param layoutViewUnBindListener
     */
    public void setLayoutViewUnBindListener(
            LayoutViewUnBindListener layoutViewUnBindListener) {
        mLayoutViewUnBindListener = layoutViewUnBindListener;
    }

    @Override
    public void bindLayoutView(@NonNull final View layoutView) {
        layoutView.measure(View.MeasureSpec.makeMeasureSpec(mLayoutRegion.width(), View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(mLayoutRegion.height(), View.MeasureSpec.EXACTLY));
        layoutView.layout(mLayoutRegion.left, mLayoutRegion.top, mLayoutRegion.right, mLayoutRegion.bottom);
        layoutView.setBackgroundColor(mBgColor);

        if (mLayoutViewBindListener != null) {
            mLayoutViewBindListener.onBind(layoutView, this);
        }

        // reset region rectangle
        mLayoutRegion.set(0, 0, 0, 0);
    }

    protected void handleStateOnResult(LayoutChunkResult result, View view) {
        if (view == null) {
            return;
        }

        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();

        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }

        // used when search a focusable view
        result.mFocusable = result.mFocusable || view.isFocusable();

    }

    /**
     * Helper methods to handle focus states for views
     * @param result
     * @param views
     */
    protected void handleStateOnResult(LayoutChunkResult result, View[] views) {
        if (views == null) return;

        for (int i = 0; i < views.length; i++) {
            View view = views[i];
            if (view == null) {
                continue;
            }
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();

            // Consume the available space if the view is not removed OR changed
            if (params.isItemRemoved() || params.isItemChanged()) {
                result.mIgnoreConsumed = true;
            }

            // used when search a focusable view
            result.mFocusable = result.mFocusable || view.isFocusable();

            if (result.mFocusable && result.mIgnoreConsumed) {
                break;
            }
        }
    }

    protected int computeStartSpace(LayoutManagerHelper helper, boolean layoutInVertical, boolean isLayoutEnd, boolean isOverLapMargin) {
        int startSpace = 0;
        LayoutHelper lastLayoutHelper = null;
        if (helper instanceof VirtualLayoutManager) {
            lastLayoutHelper = ((VirtualLayoutManager) helper).findNeighbourNonfixLayoutHelper(this, isLayoutEnd);
        }
        MarginLayoutHelper lastMarginLayoutHelper = null;

        if (lastLayoutHelper != null && lastLayoutHelper instanceof MarginLayoutHelper) {
            lastMarginLayoutHelper = (MarginLayoutHelper) lastLayoutHelper;
        }
        if (lastLayoutHelper == this)
            return 0;

        if (!isOverLapMargin) {
            startSpace = layoutInVertical ? mMarginTop + mPaddingTop : mMarginLeft + mPaddingLeft;
        } else {
            int offset = 0;

            if (lastMarginLayoutHelper == null) {
                offset = layoutInVertical ? mMarginTop + mPaddingTop : mMarginLeft + mPaddingLeft;
            } else {
                offset = layoutInVertical
                        ? (isLayoutEnd ? calGap(lastMarginLayoutHelper.mMarginBottom, mMarginTop) : calGap(lastMarginLayoutHelper.mMarginTop, mMarginBottom))
                        : (isLayoutEnd ? calGap(lastMarginLayoutHelper.mMarginRight, mMarginLeft) : calGap(lastMarginLayoutHelper.mMarginLeft, mMarginRight));
            }
            //Log.e("huang", "computeStartSpace offset: " + offset + ", isLayoutEnd: " + isLayoutEnd + ", " + this);
            startSpace += layoutInVertical
                    ? (isLayoutEnd ? mPaddingTop : mPaddingBottom)
                    : (isLayoutEnd ? mPaddingLeft : mPaddingRight);

            startSpace += offset;
        }
        return startSpace;
    }

    protected int computeEndSpace(LayoutManagerHelper helper, boolean layoutInVertical, boolean isLayoutEnd, boolean isOverLapMargin) {
        int endSpace = layoutInVertical
                ? mMarginBottom + mPaddingBottom : mMarginLeft + mPaddingLeft;
        //Log.e("huang", "computeEndSpace offset: " + endSpace + ", isLayoutEnd: " + isLayoutEnd + ", " + this);
        //Log.e("huang", "===================\n\n");
        return endSpace;
    }

    private int calGap(int gap, int currGap) {
        if (gap < currGap) {
            return currGap - gap;
        } else {
            return 0;
        }
    }
}