import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { createNamespace } from '../../tools/namingTools';
import { scrollToTop, scrollToTopAccessible } from '../../tools/interactivity/scroll';
import { useResponsive } from '../../tools/widths/useResponsive';
import { GRID_VIEW } from '../../constants/views';
import { compareProductsStore } from '../../store/compareProducts';
import { productListControlsStore } from '../../store/productListControls';
import { productListDataStore } from '../../store/productListData';
import Icon from '../../atoms/icon/Icon';
import Grid from '../../atoms/grid/Grid';
import withListener from '../withListener';
import ProductListingCard, { productListingCardForm, productListingSettingsShape } from '../../atoms/product-listing-card/ProductListingCard';
import Paging from '../../atoms/paging/Paging';
import { handlePageSelect } from '../../tools/paginationHelper';
import { TYPE_PREVIOUS, TYPE_NEXT, TYPE_PAGE } from '../../constants/components/paging';
import { buildListingPageApiQuery, getListingApiEndpoint, updatePageUrl } from '../../tools/urlTools';
import { useContainerWidth } from '../../tools/widths/useContainerWidth';
import { useDebounceResize } from '../../tools/dimensions/useDebounceResize';
import { DEFAULT_LOCALE_CODE } from '../../constants/localization';
import { MAX_COMPARE_ITEMS_LENGTH } from '../../constants/components/compare';
import { paginationPropTypes } from '../../constants/prop-types/listingProducts/pagination';
import { itemsPropTypes } from '../../constants/prop-types/listingProducts/items';
import { i18nPropTypes } from '../../constants/prop-types/listingProducts/i18n';
import { updateFilterDetails } from '../../tools/components/productListing';
// IMPORTANT: (s)css should be imported after other atoms/components
import './styles/ProductListing.scss';

const n = createNamespace('ProductListing');

export const N_COLS_LIST = 1;
export const N_COLS_GRID = 4;

const LARGE_CONTAINER = 1292;
const MEDIUM_CONTAINER = 932;

const LOADING_ITEMS_LENGTH = 12;

const getNumberOfColumns = gridWidth => {
    if (gridWidth < MEDIUM_CONTAINER) {
        return 2;
    }
    if (gridWidth < LARGE_CONTAINER) {
        return 3;
    }
    return 4;
};

export const ProductListingComponent = ({
    data: {
        items: itemsInitialValue,
        showAsGrid,
        pagination: paginationInitialValue,
        locale,
        i18n,
        i18nRating
    },
    settings
}) => {
    const i18nWithDefaults = { ...ProductListingComponent.defaultProps.data.i18n, ...i18n };

    // on load, list is received from props. Need to set it in state then.
    // after that, it will be refetched by ajax calls
    const [poductListItems, setProductListItems] = useState(itemsInitialValue);
    const [pagination, setPagination] = useState(paginationInitialValue);

    const { isRatingsVisible = true } = settings;

    const { totalCount, itemsPerPage, currentPage } = pagination;

    const {
        defaultView
    } = { ...ProductListingComponent.defaultProps.settings, ...settings };

    const { viewType, setViewType, listingSidebarTitle } = productListControlsStore();

    useEffect(() => {
        setViewType(defaultView);
    }, [defaultView]);

    // if view type is set in store, use that value, otherwise use param value
    const shouldShowAsGrid = viewType ? viewType === GRID_VIEW : showAsGrid;

    const [shouldShowLoader, setShouldShowLoader] = useState(false);

    const [gridRef, gridWidth] = useContainerWidth();
    const defaultColumns = shouldShowAsGrid ? (getNumberOfColumns(gridWidth) || N_COLS_GRID) : N_COLS_LIST;
    const [columns, setColumns] = useState(defaultColumns);

    const { mediumDown } = useResponsive();
    const isMobile = mediumDown;

    const { list: compareProductsList, toggle: toggleCompare } = compareProductsStore();
    const { listingPage, setListingPage, isNavigateFromSearchBar, setIsNavigateFromSearchBar,
        setListingLastPage, filtersDetails, setFiltersDetails, setIsRatingsVisible,
        sortBy, filtersQuery, setSearchResultCount, listingLastPage } = productListDataStore();

    const shouldShowPagingNav = Boolean(pagination && totalCount);
    const lastPage = Math.ceil(totalCount / itemsPerPage);

    const renderPagingCount = useMemo(() => (
        <div {...n('results', isMobile && 'mobile').props}>
            {`${totalCount} ${i18nWithDefaults.results}`}
        </div>
    ), [isMobile, totalCount, i18nWithDefaults.results]);

    const renderPagingArrows = () => {
        const pagingMobile = pagination.pagesList.reduce((accumulator, currentValue) => {
            if (currentValue.type === TYPE_PREVIOUS) {
                accumulator.previous = currentValue;
            } else if (currentValue.type === TYPE_NEXT) {
                accumulator.next = currentValue;
            }

            return accumulator;
        }, {});

        const renderArrow = ({ type, status, nb }, label) => {
            const pageSelection = type === TYPE_PAGE ? nb : type;
            return (
                <div {...n('paging-arrow', type).props}>
                    <a
                        {...n('btn-paging-arrow', [status, type]).props}
                        disabled={status === 'disabled'}
                        onClick={e => handlePageSelect(e, pageSelection, listingPage, setListingPage, listingLastPage)}
                        onKeyDown={() => {}}
                    >
                        <span {...n('btn-paging-arrow-text').props}>{label}</span>
                        <Icon type={type === TYPE_PREVIOUS ? 'caretLeft' : 'caretRight'} {...n('btn-paging-arrow-icon', type).props} />
                    </a>
                </div>
            );
        };

        return (
            <div {...n('paging-arrows').props}>
                {pagingMobile.previous && renderArrow(pagingMobile.previous, i18n.previous)}
                {pagingMobile.next && renderArrow(pagingMobile.next, i18n.next)}
            </div>
        );
    };

    const renderSidebarTitle = () => {
        if (!listingSidebarTitle) {
            return null;
        }
        return <h1 {...n('title', isMobile && 'mobile').props}>{listingSidebarTitle}</h1>;
    };

    const renderLoadingItems = useCallback(() => Array.from(Array(LOADING_ITEMS_LENGTH).keys()).map(index => (
        <ProductListingCard
            key={index}
            showAsGrid={shouldShowAsGrid}
            cardIndex={index}
            isLoading
        />
    )), [shouldShowAsGrid]);

    const renderCardItems = useCallback(() => poductListItems.map((item, index) => {
        const isCompareChecked = compareProductsList && compareProductsList.includes(item.id);
        const isCompareMaxed = compareProductsList
            && compareProductsList.length >= MAX_COMPARE_ITEMS_LENGTH;
        const isDisabled = !isCompareChecked && isCompareMaxed;

        const onCheckCompare = e => (isDisabled ? null : toggleCompare(e));
        return (
            <ProductListingCard
                key={item.id || index}
                data={item}
                settings={settings}
                i18n={i18nWithDefaults}
                i18nRating={i18nRating}
                showAsGrid={shouldShowAsGrid}
                cardIndex={index}
                onCheckCompare={onCheckCompare}
                isCompareChecked={isCompareChecked}
                isDisabled={isDisabled}
            />
        );
    }), [poductListItems, compareProductsList, shouldShowAsGrid, settings]);

    const getCardItems = useCallback(() => {
        if (shouldShowLoader) {
            return renderLoadingItems();
        }
        return renderCardItems();
    }, [renderLoadingItems, renderCardItems, shouldShowLoader]);

    const updateNumberOfColumns = (containerWidth, isGridView) => {
        const updatedColumns = isGridView ? getNumberOfColumns(containerWidth) : N_COLS_LIST;
        setColumns(updatedColumns);
    };
    // set global state from prop values when component loads
    useEffect(() => {
        setSearchResultCount(totalCount);
        setIsRatingsVisible(isRatingsVisible);
        setListingLastPage(lastPage);
        setListingPage(currentPage);
    }, []);

    useEffect(() => {
        if (!gridRef.current) {
            return;
        }
        updateNumberOfColumns(gridRef.current.offsetWidth, shouldShowAsGrid);
    }, [shouldShowAsGrid, updateNumberOfColumns]);

    const gridOffsetWidth = gridRef.current ? gridRef.current.offsetWidth : 0;
    useDebounceResize(() => updateNumberOfColumns(gridOffsetWidth, shouldShowAsGrid));

    const [listingQuery, setListingQuery] = useState(null);

    const currentFetchRef = useRef({ controller: null, url: null });

    useEffect(() => {
        // sort & filters are null on initialisation, and their value will eventually be set by controls
        if (sortBy !== null && filtersQuery !== null) {
            // builds query matching these parameters
            const localeCode = locale && locale.code ? locale.code : DEFAULT_LOCALE_CODE;
            const parsedQuery = buildListingPageApiQuery(filtersQuery, sortBy, listingPage, localeCode);
            // when we don't have a value yet for the current query, it means we are at initialization.
            // this initial value is thus the prop's list query. But if we had a query, and the new value is different,
            // it means the filters/sort were updated, so we need to fetch a new list
            if (listingQuery !== null && listingQuery !== parsedQuery) {
                setShouldShowLoader(true);
                scrollToTop(false);
                const listingEndpoint = getListingApiEndpoint(parsedQuery);

                // abort previous fetch
                if (currentFetchRef.current?.url && currentFetchRef.current?.controller) {
                    currentFetchRef.current.controller.abort();
                }

                // store the current controller and URL
                const abortController = new AbortController();
                currentFetchRef.current = { controller: abortController, url: listingEndpoint };

                fetch(listingEndpoint, { signal: abortController.signal })
                    .then(res => res.json())
                    .then(
                        result => {
                            updateFilterDetails(result?.listingFilters?.filters, filtersDetails, setFiltersDetails);
                            // push state for url in address bar to reflect loaded list
                            updatePageUrl(parsedQuery, isNavigateFromSearchBar, setIsNavigateFromSearchBar);
                            // update internal product list to display them
                            if (result?.listingProducts?.items) {
                                setProductListItems(result.listingProducts.items);
                            }
                            setPagination(result.listingProducts.pagination);
                            setSearchResultCount(result.listingProducts.pagination.totalCount);
                            setShouldShowLoader(false);
                        }
                    ).catch(error => {
                        if (error?.name === 'AbortError') {
                            // FIXME: keep log for debugging in TEST
                            console.log('ProductListing: Previous fetch aborted for ', listingEndpoint);
                        } else {
                            console.log('ProductListing error: ', error);
                        }
                    })
                    .finally(() => {
                        // reset fetch ref
                        currentFetchRef.current = { controller: null, url: null };
                    });
            }
            setListingQuery(parsedQuery);
        }
    }, [sortBy, filtersQuery, poductListItems, listingQuery, listingPage]);

    if (!poductListItems) {
        return null;
    }

    const backToTopBtnClasses = [
        'button_item',
        'button_item--primary',
        'button_item--small'
    ].join(' ');

    const onBackToTopClick = e => {
        e.preventDefault();
        scrollToTop();
    };

    const onKeyboardSelection = e => {
        if (e.key === ' ' || e.key === 'Enter') {
            e.preventDefault();
            scrollToTopAccessible();
        }
    };

    return (
        <div {...n('container').withTestId().props}>
            {isMobile && renderSidebarTitle()}
            {isMobile && renderPagingCount}

            <Grid
                maxCol={columns}
                gridRef={gridRef}
                itemClass={n('gridItem').props.className}
                rowClass={n('gridRow').props.className}
            >
                {getCardItems()}
            </Grid>

            <div {...n('footer').props}>
                {shouldShowPagingNav && renderPagingArrows()}
                <div {...n('paging-desktop').props}>
                    {renderPagingCount}
                    {shouldShowPagingNav && (
                        <div {...n('paging').props}>
                            <div {...n('page-list').props}>
                                <Paging data={pagination} size="small" i18n={i18n} />
                            </div>
                            <div {...n('footerButton').withE2EId().props}>
                                <button
                                    type="button"
                                    {...n('backToTop').withClass(backToTopBtnClasses).props}
                                    onClick={onBackToTopClick}
                                    onKeyDown={onKeyboardSelection}
                                >
                                    <span className="button_text">{i18nWithDefaults.backToTop}</span>
                                </button>
                            </div>
                        </div>
                    )}
                </div>
            </div>
        </div>
    );
};

ProductListingComponent.schema = () => ({
    name: 'ProductListing',
    title: 'uieditor.sfbComponent.productListing.title',
    iconName: 'app',
    form: {
        ...productListingCardForm
    },
    marketplaceData: {
        items: 'listingProducts.items',
        pagination: 'listingProducts.pagination',
        i18n: 'listingProducts.i18n',
        i18nRating: 'i18n.rating',
        locale: 'config.locale'
    },
    requiredDataFunc: data => (!!data?.items?.length)
});

ProductListingComponent.propTypes = {
    data: PropTypes.shape({
        showAsGrid: PropTypes.bool,
        items: PropTypes.arrayOf(
            PropTypes.shape(itemsPropTypes)
        ),
        pagination: PropTypes.shape(paginationPropTypes),
        locale: PropTypes.object,
        i18n: PropTypes.shape(i18nPropTypes),
        i18nRating: PropTypes.shape({
            label: PropTypes.string
        })
    }),
    settings: PropTypes.shape({
        ...productListingSettingsShape,
        isRatingsVisible: PropTypes.bool,
        sortBy: PropTypes.string
    })
};

ProductListingComponent.defaultProps = {
    data: {
        showAsGrid: false,
        items: [],
        pagination: {},
        locale: {},
        i18n: {
            backToTop: 'Back to Top',
            compare: 'Compare',
            grid: 'Grid',
            hideFilters: 'Hide Filters',
            list: 'List',
            results: 'Results',
            showFilters: 'Show Filters',
            sortBy: 'Sorted By',
            viewMode: 'View Mode',
            viewMore: 'View More',
            sort: 'Sort',
            previous: 'Previous',
            next: 'Next'
        },
        i18nRating: null
    },
    settings: {}
};

export default withListener(ProductListingComponent);
