/*
 * Copyright (c) 2018. Evren Coşkun
 *
 *  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.evrencoskun.tableview.layoutmanager;

import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.evrencoskun.tableview.ITableView;
import com.evrencoskun.tableview.adapter.recyclerview.CellRecyclerView;
import com.evrencoskun.tableview.adapter.recyclerview.holder.AbstractViewHolder;
import com.evrencoskun.tableview.listener.scroll.HorizontalRecyclerViewListener;
import com.evrencoskun.tableview.util.TableViewUtils;

import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

/**
 * Created by evrencoskun on 24/06/2017.
 */

public class CellLayoutManager extends LinearLayoutManager {
    private static final String LOG_TAG = CellLayoutManager.class.getSimpleName();
    private static final int IGNORE_LEFT = -99999;

    @NonNull
    private ColumnHeaderLayoutManager mColumnHeaderLayoutManager;

    @NonNull
    private CellRecyclerView mRowHeaderRecyclerView;

    private HorizontalRecyclerViewListener mHorizontalListener;
    @NonNull
    private ITableView mTableView;

    @NonNull
    private final SparseArray<SparseIntArray> mCachedWidthList = new SparseArray<>();
    //TODO: Store a single instance for both cell and column cache width values.

    private int mLastDy = 0;
    private boolean mNeedSetLeft;
    private boolean mNeedFit;

    public CellLayoutManager(@NonNull Context context, @NonNull ITableView tableView) {
        super(context);
        this.mTableView = tableView;
        this.mColumnHeaderLayoutManager = tableView.getColumnHeaderLayoutManager();
        this.mRowHeaderRecyclerView = tableView.getRowHeaderRecyclerView();

        initialize();
    }

    private void initialize() {
        this.setOrientation(VERTICAL);
        // Add new one
    }

    @Override
    public void onAttachedToWindow(RecyclerView view) {
        super.onAttachedToWindow(view);

        // initialize the instances
        if (mHorizontalListener == null) {
            mHorizontalListener = mTableView.getHorizontalRecyclerViewListener();
        }
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State
            state) {
        if (mRowHeaderRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE &&
                !mRowHeaderRecyclerView.isScrollOthers()) {
            // CellRecyclerViews should be scrolled after the RowHeaderRecyclerView.
            // Because it is one of the main compared criterion to make each columns fit.
            mRowHeaderRecyclerView.scrollBy(0, dy);
        }

        int scroll = super.scrollVerticallyBy(dy, recycler, state);

        // It is important to determine right position to fit all columns which are the same y pos.
        mLastDy = dy;
        return scroll;
    }

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        if (state == RecyclerView.SCROLL_STATE_IDLE) {
            // It is important to set it 0 to be able to know which direction is being scrolled
            mLastDy = 0;
        }
    }

    /**
     * This method helps to fit all columns which are displayed on screen.
     * Especially it will be called when TableView is scrolled on vertically.
     */
    public void fitWidthSize(boolean scrollingUp) {
        int left = mColumnHeaderLayoutManager.getFirstItemLeft();
        for (int i = mColumnHeaderLayoutManager.findFirstVisibleItemPosition(); i <
                mColumnHeaderLayoutManager.findLastVisibleItemPosition() + 1; i++) {
            left = fitSize(i, left, scrollingUp);
        }

        mNeedSetLeft = false;
    }

    /**
     * This method helps to fit a column. it will be called when TableView is scrolled on
     * horizontally.
     */
    public void fitWidthSize(int position, boolean scrollingLeft) {
        fitSize(position, IGNORE_LEFT, false);

        if (mNeedSetLeft & scrollingLeft) {
            // Works just like invoke later of swing utils.
            Handler handler = new Handler();
            handler.post(() -> fitWidthSize2(true));
        }
    }

    private int fitSize(int position, int left, boolean scrollingUp) {
        int cellRight = -1;

        int columnCacheWidth = mColumnHeaderLayoutManager.getCacheWidth(position);
        View column = mColumnHeaderLayoutManager.findViewByPosition(position);

        if (column != null) {
            // Determine default right
            cellRight = column.getLeft() + columnCacheWidth + 1;

            if (scrollingUp) {
                // Loop reverse order
                for (int i = findLastVisibleItemPosition(); i >= findFirstVisibleItemPosition();
                     i--) {
                    cellRight = fit(position, i, left, cellRight, columnCacheWidth);
                }
            } else {
                // Loop for all rows which are visible.
                for (int j = findFirstVisibleItemPosition(); j < findLastVisibleItemPosition() +
                        1; j++) {
                    cellRight = fit(position, j, left, cellRight, columnCacheWidth);
                }
            }
        } else {
            Log.e(LOG_TAG, "Warning: column couldn't found for " + position);
        }
        return cellRight;
    }

    private int fit(int xPosition, int yPosition, int left, int right, int columnCachedWidth) {
        CellRecyclerView child = (CellRecyclerView) findViewByPosition(yPosition);

        if (child != null) {
            ColumnLayoutManager childLayoutManager = (ColumnLayoutManager) child.getLayoutManager();

            int cellCacheWidth = getCacheWidth(yPosition, xPosition);
            View cell = childLayoutManager.findViewByPosition(xPosition);

            // Control whether the cell needs to be fitted by column header or not.
            if (cell != null) {

                if (cellCacheWidth != columnCachedWidth || mNeedSetLeft) {

                    // This is just for setting width value
                    if (cellCacheWidth != columnCachedWidth) {
                        cellCacheWidth = columnCachedWidth;
                        TableViewUtils.setWidth(cell, cellCacheWidth);

                        setCacheWidth(yPosition, xPosition, cellCacheWidth);
                    }

                    // Even if the cached values are same, the left & right value wouldn't change.
                    // mNeedSetLeft & the below lines for it.
                    if (left != IGNORE_LEFT && cell.getLeft() != left) {

                        // Calculate scroll distance
                        int scrollX = Math.max(cell.getLeft(), left) - Math.min(cell.getLeft(),
                                left);

                        // Update its left
                        cell.setLeft(left);

                        int offset = mHorizontalListener.getScrollPositionOffset();

                        // It shouldn't be scroll horizontally and the problem is gotten just for
                        // first visible item.
                        if (offset > 0 && xPosition == childLayoutManager
                                .findFirstVisibleItemPosition() && getCellRecyclerViewScrollState() != RecyclerView.SCROLL_STATE_IDLE) {

                            int scrollPosition = mHorizontalListener.getScrollPosition();
                            offset = mHorizontalListener.getScrollPositionOffset() + scrollX;

                            // Update scroll position offset value
                            mHorizontalListener.setScrollPositionOffset(offset);
                            // Scroll considering to the desired value.
                            childLayoutManager.scrollToPositionWithOffset(scrollPosition, offset);
                        }
                    }

                    if (cell.getWidth() != cellCacheWidth) {
                        if (left != IGNORE_LEFT) {
                            // TODO: + 1 is for decoration item. It should be gotten from a
                            // generic method  of layoutManager
                            // Set right
                            right = cell.getLeft() + cellCacheWidth + 1;
                            cell.setRight(right);

                            childLayoutManager.layoutDecoratedWithMargins(cell, cell.getLeft(),
                                    cell.getTop(), cell.getRight(), cell.getBottom());
                        }

                        mNeedSetLeft = true;
                    }
                }
            }
        }
        return right;
    }

    /**
     * Alternative method of fitWidthSize().
     * The main difference is this method works after main thread draw the ui components.
     */
    public void fitWidthSize2(boolean scrollingLeft) {
        // The below line helps to change left & right value of the each column
        // header views
        // without using requestLayout().
        mColumnHeaderLayoutManager.customRequestLayout();

        // Get the right scroll position information from Column header RecyclerView
        int columnHeaderScrollPosition = mTableView.getColumnHeaderRecyclerView().getScrolledX();
        int columnHeaderOffset = mColumnHeaderLayoutManager.getFirstItemLeft();
        int columnHeaderFirstItem = mColumnHeaderLayoutManager.findFirstVisibleItemPosition();

        // Fit all visible columns widths
        for (int i = mColumnHeaderLayoutManager.findFirstVisibleItemPosition(); i <
                mColumnHeaderLayoutManager.findLastVisibleItemPosition() + 1; i++) {

            fitSize2(i, scrollingLeft, columnHeaderScrollPosition, columnHeaderOffset,
                    columnHeaderFirstItem);
        }

        mNeedSetLeft = false;
    }

    /**
     * Alternative method of fitWidthSize().
     * The main difference is this method works after main thread draw the ui components.
     */
    public void fitWidthSize2(int position, boolean scrollingLeft) {
        // The below line helps to change left & right value of the each column
        // header views
        // without using requestLayout().
        mColumnHeaderLayoutManager.customRequestLayout();

        // Get the right scroll position information from Column header RecyclerView
        int columnHeaderScrollPosition = mTableView.getColumnHeaderRecyclerView().getScrolledX();
        int columnHeaderOffset = mColumnHeaderLayoutManager.getFirstItemLeft();
        int columnHeaderFirstItem = mColumnHeaderLayoutManager.findFirstVisibleItemPosition();

        // Fit all visible columns widths
        fitSize2(position, scrollingLeft, columnHeaderScrollPosition, columnHeaderOffset,
                columnHeaderFirstItem);


        mNeedSetLeft = false;
    }

    private void fitSize2(int position, boolean scrollingLeft, int columnHeaderScrollPosition,
                          int columnHeaderOffset, int columnHeaderFirstItem) {
        int columnCacheWidth = mColumnHeaderLayoutManager.getCacheWidth(position);
        View column = mColumnHeaderLayoutManager.findViewByPosition(position);

        if (column != null) {
            // Loop for all rows which are visible.
            for (int j = findFirstVisibleItemPosition(); j < findLastVisibleItemPosition() + 1;
                 j++) {

                // Get CellRowRecyclerView
                CellRecyclerView child = (CellRecyclerView) findViewByPosition(j);

                if (child != null) {
                    ColumnLayoutManager childLayoutManager = (ColumnLayoutManager) child
                            .getLayoutManager();

                    // Checking Scroll position is necessary. Because, even if they have same width
                    // values, their scroll positions can be different.
                    if (!scrollingLeft && columnHeaderScrollPosition != child.getScrolledX()) {

                        // Column Header RecyclerView has the right scroll position. So,
                        // considering it
                        childLayoutManager.scrollToPositionWithOffset(columnHeaderFirstItem,
                                columnHeaderOffset);
                    }

                    if (childLayoutManager != null) {
                        fit2(position, j, columnCacheWidth, column, childLayoutManager);
                    }
                }
            }
        }
    }

    private void fit2(int xPosition, int yPosition, int columnCachedWidth, @NonNull View column,
                      @NonNull ColumnLayoutManager childLayoutManager) {
        int cellCacheWidth = getCacheWidth(yPosition, xPosition);
        View cell = childLayoutManager.findViewByPosition(xPosition);

        // Control whether the cell needs to be fitted by column header or not.
        if (cell != null) {

            if (cellCacheWidth != columnCachedWidth || mNeedSetLeft) {

                // This is just for setting width value
                if (cellCacheWidth != columnCachedWidth) {
                    cellCacheWidth = columnCachedWidth;
                    TableViewUtils.setWidth(cell, cellCacheWidth);

                    setCacheWidth(yPosition, xPosition, cellCacheWidth);
                }

                // The left & right values of Column header can be considered. Because this
                // method will be worked
                // after drawing process of main thread.
                if (column.getLeft() != cell.getLeft() || column.getRight() != cell.getRight()) {
                    // TODO: + 1 is for decoration item. It should be gotten from a generic
                    // method  of layoutManager
                    // Set right & left values
                    cell.setLeft(column.getLeft());
                    cell.setRight(column.getRight() + 1);
                    childLayoutManager.layoutDecoratedWithMargins(cell, cell.getLeft(), cell
                            .getTop(), cell.getRight(), cell.getBottom());

                    mNeedSetLeft = true;
                }
            }
        }
    }

    public boolean shouldFitColumns(int yPosition) {

        // Scrolling horizontally
        if (getCellRecyclerViewScrollState() == RecyclerView.SCROLL_STATE_IDLE) {

            int lastVisiblePosition = findLastVisibleItemPosition();
            CellRecyclerView lastCellRecyclerView = (CellRecyclerView) findViewByPosition
                    (lastVisiblePosition);

            if (lastCellRecyclerView != null) {
                if (yPosition == lastVisiblePosition) {
                    return true;
                } else if (lastCellRecyclerView.isScrollOthers() && yPosition ==
                        lastVisiblePosition - 1) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
        super.measureChildWithMargins(child, widthUsed, heightUsed);

        // If has fixed width is true, than calculation of the column width is not necessary.
        if (mTableView.hasFixedWidth()) {
            return;
        }

        int position = getPosition(child);

        ColumnLayoutManager childLayoutManager = (ColumnLayoutManager) ((CellRecyclerView) child)
                .getLayoutManager();

        // the below codes should be worked when it is scrolling vertically
        if (getCellRecyclerViewScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
            if (childLayoutManager.isNeedFit()) {

                // Scrolling up
                if (mLastDy < 0) {
                    Log.e(LOG_TAG, position + " fitWidthSize all vertically up");
                    fitWidthSize(true);
                } else {
                    // Scrolling down
                    Log.e(LOG_TAG, position + " fitWidthSize all vertically down");
                    fitWidthSize(false);
                }
                // all columns have been fitted.
                childLayoutManager.clearNeedFit();
            }

            // Set the right initialPrefetch size to improve performance
            childLayoutManager.setInitialPrefetchItemCount(childLayoutManager.getChildCount());

            // That means,populating for the first time like fetching all data to display.
            // It shouldn't be worked when it is scrolling horizontally ."getLastDx() == 0"
            // control for it.
        } else if (childLayoutManager.getLastDx() == 0 && getCellRecyclerViewScrollState() ==
                RecyclerView.SCROLL_STATE_IDLE) {

            if (childLayoutManager.isNeedFit()) {
                mNeedFit = true;

                // all columns have been fitted.
                childLayoutManager.clearNeedFit();
            }

            if (mNeedFit) {
                // for the first time to populate adapter
                if (mTableView.getRowHeaderLayoutManager().findLastVisibleItemPosition() == position) {

                    fitWidthSize2(false);
                    Log.e(LOG_TAG, position + " fitWidthSize populating data for the first time");

                    mNeedFit = false;
                }
            }
        }
    }

    @NonNull
    public AbstractViewHolder[] getVisibleCellViewsByColumnPosition(int xPosition) {
        int visibleChildCount = findLastVisibleItemPosition() - findFirstVisibleItemPosition() + 1;
        int index = 0;
        AbstractViewHolder[] viewHolders = new AbstractViewHolder[visibleChildCount];
        for (int i = findFirstVisibleItemPosition(); i < findLastVisibleItemPosition() + 1; i++) {
            CellRecyclerView cellRowRecyclerView = (CellRecyclerView) findViewByPosition(i);

            AbstractViewHolder holder = (AbstractViewHolder) cellRowRecyclerView
                    .findViewHolderForAdapterPosition(xPosition);

            viewHolders[index] = holder;

            index++;
        }
        return viewHolders;
    }

    @Nullable
    public AbstractViewHolder getCellViewHolder(int xPosition, int yPosition) {
        CellRecyclerView cellRowRecyclerView = (CellRecyclerView) findViewByPosition(yPosition);

        if (cellRowRecyclerView != null) {
            return (AbstractViewHolder) cellRowRecyclerView.findViewHolderForAdapterPosition
                    (xPosition);
        }
        return null;
    }

    public void remeasureAllChild() {
        // TODO: the below code causes requestLayout() improperly called by com.evrencoskun.tableview.adapter

        for (int j = 0; j < getChildCount(); j++) {
            CellRecyclerView recyclerView = (CellRecyclerView) getChildAt(j);

            recyclerView.getLayoutParams().width = WRAP_CONTENT;
            recyclerView.requestLayout();
        }
    }

    /**
     * Allows to set cache width value for single cell item.
     */
    public void setCacheWidth(int row, int column, int width) {
        SparseIntArray cellRowCache = mCachedWidthList.get(row);
        if (cellRowCache == null) {
            cellRowCache = new SparseIntArray();
        }

        cellRowCache.put(column, width);
        mCachedWidthList.put(row, cellRowCache);
    }

    /**
     * Allows to set cache width value for all cell items that is located on column position.
     */
    public void setCacheWidth(int column, int width) {
        for (int i = 0; i < mRowHeaderRecyclerView.getAdapter().getItemCount(); i++) {
            // set cache width for single cell item.
            setCacheWidth(i, column, width);
        }
    }

    public int getCacheWidth(int row, int column) {
        SparseIntArray cellRowCaches = mCachedWidthList.get(row);
        if (cellRowCaches != null) {
            return cellRowCaches.get(column, -1);
        }
        return -1;
    }

    /**
     * Clears the widths which have been calculated and reused.
     */
    public void clearCachedWidths() {
        mCachedWidthList.clear();
    }

    @NonNull
    public CellRecyclerView[] getVisibleCellRowRecyclerViews() {
        int length = findLastVisibleItemPosition() - findFirstVisibleItemPosition() + 1;
        CellRecyclerView[] recyclerViews = new CellRecyclerView[length];

        int index = 0;
        for (int i = findFirstVisibleItemPosition(); i < findLastVisibleItemPosition() + 1; i++) {
            recyclerViews[index] = (CellRecyclerView) findViewByPosition(i);
            index++;
        }

        return recyclerViews;
    }

    private int getCellRecyclerViewScrollState() {
        return mTableView.getCellRecyclerView().getScrollState();
    }
}