import ClassNames from 'classnames';
import {
    debounce,
    get,
    isNil,
} from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import ScrollBar from 'react-custom-scrollbars';
import {
    DEBOUNCE_WAIT_TIME,
    TH_RESIZABLE_MAX_WIDTH,
} from './tableConstants';
import Utils from '../../utils/utils';
import TableBody from './tableBody';
import TableCell from './tableCell';
import TableHeader from './tableHeader';
import TableRow from './tableRow';
import TableHeaderCell from './tableHeaderCell';
import withStyles from '../../styles/withStyles';

const propTypes = {
    /**
     * Basic (default) styling.
     */
    basic: PropTypes.bool,
    /**
     * If `true`, all columns will be divided.
     */
    celled: PropTypes.bool,
    /**
     * The content of the Table.
     */
    children: PropTypes.node,
    /**
     * Override or extend the styles applied to the Table.
     */
    classes: PropTypes.shape({
        root: PropTypes.string,
        tableSticky: PropTypes.string,
        tableStickyColumns: PropTypes.string,
    }),
    /**
     * Add additional classes to the Table.
     */
    className: PropTypes.string,
    /**
     * If `true`, applies `width: auto` to the Table.
     */
    collapsing: PropTypes.bool,
    /**
     * If `true`, the far left column is divided from the rest of the columns.
     */
    definition: PropTypes.bool,
    /**
     * If `true`, columns will be evenly spaced.
     */
    fixed: PropTypes.bool,
    /**
     * Change the font size of text in the Table.
     */
    fontSize: PropTypes.oneOf(Utils.sizeEnums()),
    /**
     * Deprecated prop.
     */
    fullWidth: PropTypes.bool,
    /**
     * Add an id to the Table.
     */
    id: PropTypes.string,
    /**
     * Table rows and cells can be highlighted as if they have been selected.
     */
    selectable: PropTypes.bool,
    /**
     * Truncate copy within cells in the Table
     */
    singleLine: PropTypes.bool,
    /**
     * Change the cells vertical size in the Table.
     */
    size: PropTypes.oneOf(['l', 'large', 'm', 'medium', 's', 'small']),
    /**
     * Deprecated prop.
     */
    stackable: PropTypes.bool,
    /**
     * A table can have one or more columns defined as 'sticky' so that they stay fixed while the
     * user horizontally scrolls to see the remaining columns.
     */
    stickyColumnCount: PropTypes.number,
    /**
     * Resizable cell default percentage width related to the Table container size.
     */
    resizableColumnWidthPercentage: (props, propName, componentName) => {
        const {
            [propName]: propValue,
        } = props;

        const isValid = propValue ?
            propValue > 0 && propValue <= parseInt(TH_RESIZABLE_MAX_WIDTH * 100, 10) :
            true;

        if (!isValid) {
            return new Error(
                `Invalid prop value for ${propName} (${propValue}) supplied to ${componentName}. Validation failed.`,
            );
        }

        return null;
    },
    /**
     * If `true` or `very`, the Table will bleed off the edge.
     */
    stretch: PropTypes.oneOfType([
        PropTypes.bool,
        PropTypes.oneOf(['very']),
    ]),
    /**
     * Deprecated prop.
     */
    striped: PropTypes.bool,
    /**
     * Supply any inline styles to the Table.
     */
    style: PropTypes.shape({}),
    theme: PropTypes.shape({
        palette: PropTypes.shape({}),
    }),
};

const defaultProps = {
    basic: true,
    celled: false,
    children: null,
    classes: null,
    className: null,
    collapsing: false,
    definition: false,
    fixed: false,
    fontSize: null,
    fullWidth: false,
    id: null,
    selectable: false,
    singleLine: false,
    size: null,
    stackable: null,
    stickyColumnCount: 0,
    resizableColumnWidthPercentage: null,
    stretch: false,
    striped: false,
    style: null,
    theme: null,
};

const TABLE_HEADER_TYPE = 'TableHeader';
const STICKY_CELL_CLASS = 'sticky-cell';
const STICKY_CELL_RESIZABLE_CLASS = 'sticky-cell-resizable';
const STICKY_CELL_FIRST_OF_ROW_CLASS = 'sticky-cell-first-of-row';
const STICKY_CELL_LAST_OF_COLUMN_CLASS = 'sticky-cell-last-of-column';

const styles = ({
    palette,
    shape,
}) => ({
    root: {
        backgroundColor: palette.background.default,
        border: `1px solid ${palette.border.primary}`,
        borderCollapse: 'separate',
        borderRadius: shape.borderRadius,
        borderSpacing: 0,
        color: palette.text.primary,
        margin: 'calc(2rem - .14285em) 0 1rem',
        textAlign: 'left',
        width: '100%',
        '&:first-child': {
            marginTop: 0,
        },
        '&:last-child': {
            marginBottom: 0,
        },
        '&.table-basic': {
            border: 0,
            '& .table-header-cell, & .table-cell': {
                backgroundColor: palette.background.default,
            },
        },
        '&.table-collapsing': {
            width: 'auto',
        },
        '&.table-color-grey-xxlight': {
            backgroundColor: palette.background.secondary,
        },
        '&.table-fixed': {
            tableLayout: 'fixed',
        },
        '&.table-single-line': {
            whiteSpace: 'nowrap',
        },
    },
    tableStickyColumns: {
        '& .table-cell': {
            backgroundColor: get(palette, 'background.primary'),
        },
        '& .table--scroll_container': {
            position: 'relative',
            overflow: 'auto',
            height: 'auto',
        },
        '& .sticky-cell': {
            position: 'sticky',
            left: 0,
            zIndex: 2,
        },
        '& .table-header > tr': {
            backgroundColor: ({ basic }) => !basic && get(palette, 'background.secondary'),
        },
        '& td.sticky-cell-resizable': {
            borderRight: `1px solid ${get(palette, 'border.primary')}`,
        },
    },
    tableSticky: {
        marginBottom: '10px !important',
    },
});

/**
 * Tables display sets of data.
 */
class Table extends React.PureComponent {
    constructor() {
        super();

        this.state = {
            adjacentStickyColumnsTotalWidth: 0,
            stickyTableContainerWidth: 0,
        };

        this.applyStickyClassesToCells = this.applyStickyClassesToCells.bind(this);
        this.delayedSetScrollBarPosition = null;
        this.forceTableUpdate = this.forceTableUpdate.bind(this);
        this.parseChildren = this.parseChildren.bind(this);
        this.setResizableCellsWordWrapping = this.setResizableCellsWordWrapping.bind(this);
        this.setStickyCellsStylesOnScroll = this.setStickyCellsStylesOnScroll.bind(this);
        this.getStickyCellBorderStyle = this.getStickyCellBorderStyle.bind(this);
    }

    componentDidMount() {
        const {
            tableStickyContainer,
        } = this;

        const {
            stickyColumnCount,
        } = this.props;

        if (stickyColumnCount > 0) {
            this.setStickyColumnPositions();
            this.applyStickyClassesToCells();

            this.setState({
                stickyTableContainerWidth: tableStickyContainer.clientWidth,
            });
        }

        this.delayedSetStickyTableContainerWidth = stickyColumnCount > 0 &&
            debounce(() => {
                this.setState({
                    stickyTableContainerWidth: tableStickyContainer.clientWidth,
                });

                this.setStickyColumnPositions();
            }, DEBOUNCE_WAIT_TIME);
    }

    componentDidUpdate(prevProps) {
        const {
            children: prevChildren,
            stickyColumnCount: prevStickyColumnCount,
        } = prevProps;

        const {
            children,
            stickyColumnCount,
        } = this.props;

        const shouldRunStickyHelperFunctions = stickyColumnCount > 0 &&
            (prevChildren !== children || prevStickyColumnCount !== stickyColumnCount);

        if (shouldRunStickyHelperFunctions) {
            this.setStickyColumnPositions();
            this.applyStickyClassesToCells();
        }
    }

    getStickyCellBorderStyle() {
        const {
            theme: {
                palette,
            },
        } = this.props;

        const borderStyle = get(palette, 'border.primary');

        return `1px solid ${borderStyle}`;
    }

    setStickyColumnPositions() {
        const {
            stickyColumnCount,
            definition,
            basic,
            fullWidth,
            singleLine,
            celled,
            fixed,
            theme: {
                palette,
            },
        } = this.props;

        if (stickyColumnCount > 0 && this.tableRef) {
            const stickyCells =
                this.tableRef.querySelectorAll(`.table-cell:nth-child(-n+${stickyColumnCount})`) ?? null;

            if (!isNil(stickyCells)) {
                let cellWidths = 0;
                let cellCount = 0;
                /**
                 * Reducing each left sticky column position will help to not have a separation
                 * between them, so the content behind won't be visible when scrolling
                 */
                let leftOverFloatSpace = 1;
                let auxLeftOverFloatSpace = 0;

                if (basic) {
                    leftOverFloatSpace = 4;
                    auxLeftOverFloatSpace = -1;

                    if (celled) {
                        leftOverFloatSpace = 4;
                        auxLeftOverFloatSpace = -1;
                    }

                    if (fixed || singleLine) {
                        leftOverFloatSpace = 4;
                        auxLeftOverFloatSpace = 1;
                    }
                } else {
                    leftOverFloatSpace = 2;
                    auxLeftOverFloatSpace = 1;

                    if (celled) {
                        auxLeftOverFloatSpace = 2;
                    }

                    if (fixed || singleLine) {
                        leftOverFloatSpace = 3;
                        auxLeftOverFloatSpace = 2;
                    }
                }

                for (let rootIndex = 0; rootIndex < stickyCells.length; rootIndex += 1) {
                    cellCount += 1;
                    const shouldChangeInitialDefinitionCell = definition &&
                        !fullWidth && rootIndex === 0;

                    if (shouldChangeInitialDefinitionCell) {
                        stickyCells[rootIndex].style.backgroundColor = get(palette, 'background.primary');
                    }

                    if (cellCount <= stickyColumnCount && cellCount > 1) {
                        cellWidths += stickyCells[rootIndex - 1].clientWidth;
                        /**
                         * This makes definition/fullwidth work
                         * otherwise we'd have big separation space between sticky columns
                         */
                        if (definition && !basic) {
                            const isFirstHeaderColumn = rootIndex > 0 && cellCount > 0 && stickyCells[rootIndex].className.includes('header');
                            const definitionLeftOverflow = isFirstHeaderColumn ? 4 : 3;
                            stickyCells[rootIndex].style.left = `${cellWidths - definitionLeftOverflow}px`;
                        } else {
                            cellWidths -= leftOverFloatSpace + (
                                cellCount >= 3 ?
                                    auxLeftOverFloatSpace :
                                    0
                            );
                            stickyCells[rootIndex].style.left = `${cellWidths}px`;
                        }
                    }

                    if (cellCount === stickyColumnCount) {
                        // used to get the max cell resizable width according to the table container
                        this.setState({
                            adjacentStickyColumnsTotalWidth: cellWidths,
                        });
                        cellCount = 0;
                        cellWidths = 0;
                    }
                }
            }
        }
    }

    setResizableCellsWordWrapping(shouldBreakSpaces) {
        const {
            stickyColumnCount,
        } = this.props;

        const stickyCells =
            this.tableRef?.querySelectorAll(`.table-cell:nth-child(${stickyColumnCount})`) ?? null;

        if (!isNil(stickyCells)) {
            for (let i = 0; i < stickyCells.length; i += 1) {
                stickyCells[i].style.whiteSpace = shouldBreakSpaces ? 'break-spaces' : 'nowrap';
            }

            this.setStickyColumnPositions();
        }
    }

    setStickyCellsStylesOnScroll({ scrollLeft }) {
        const {
            basic,
            definition,
            fullWidth,
            theme: {
                palette,
            },
        } = this.props;

        const stickyCellsFirstOfRow =
            this.tableRef?.querySelectorAll(`.${STICKY_CELL_FIRST_OF_ROW_CLASS}`) ?? null;

        if (!isNil(stickyCellsFirstOfRow)) {
            const stickyCellBorderStyle = this.getStickyCellBorderStyle();
            const stickyCellBorderLeftStyle = scrollLeft > 0 && !basic ? stickyCellBorderStyle : '';

            for (let i = 0; i < stickyCellsFirstOfRow.length; i += 1) {
                stickyCellsFirstOfRow[i].style.borderLeft = stickyCellBorderLeftStyle;
                const isInitalDefinitionCell = definition && !fullWidth && i === 0;

                if (isInitalDefinitionCell) {
                    stickyCellsFirstOfRow[i].style.borderTop = get(palette, 'background.primary');
                    stickyCellsFirstOfRow[i].style.boxShadow = '';
                    stickyCellsFirstOfRow[i].style.borderLeft = '';
                    stickyCellsFirstOfRow[i].style.borderRight = stickyCellBorderStyle;
                }
            }
        }
    }

    parseChildren(children) {
        const {
            setResizableCellsWordWrapping,
        } = this;

        const {
            stickyColumnCount,
            fixed,
            singleLine,
            resizableColumnWidthPercentage,
        } = this.props;

        const {
            adjacentStickyColumnsTotalWidth,
            stickyTableContainerWidth,
        } = this.state;

        const shouldHandleStickyBehavior = !fixed && !singleLine;
        const parsedChildren = React.Children.map(children, (child) => {
            if (child.type.name === TABLE_HEADER_TYPE) {
                const tableHeaderRow = child.props.children;
                const headerCells = tableHeaderRow.props.children;
                const parsedHeaderCells = React.Children.map(
                    headerCells,
                    (headerCell, index) => {
                        const isResizable = shouldHandleStickyBehavior &&
                            (
                                index === stickyColumnCount - 1 &&
                                index !== headerCells.length - 1
                            );
                        return {
                            ...headerCell,
                            props: {
                                ...headerCell.props,
                                isResizable,
                                ...(isResizable && {
                                    adjacentStickyColumnsTotalWidth,
                                    stickyColumnCount,
                                    stickyTableContainerWidth,
                                    shouldResetWhiteSpaceStyle: shouldHandleStickyBehavior,
                                    forceTableUpdate: this.forceTableUpdate,
                                    resizableColumnWidthPercentage,
                                    setResizableCellsWordWrapping,
                                }),
                            },
                        };
                    },
                );
                const parsedTableHeaderRow = {
                    ...tableHeaderRow,
                    props: {
                        ...tableHeaderRow.props,
                        children: parsedHeaderCells,
                    },
                };
                return {
                    ...child,
                    props: {
                        ...child.props,
                        children: {
                            ...parsedTableHeaderRow,
                        },
                    },
                };
            }

            return child;
        });

        return parsedChildren;
    }

    applyStickyClassesToCells() {
        const {
            stickyColumnCount,
            definition,
            fullWidth,
        } = this.props;

        const stickyCells =
            this.tableRef?.querySelectorAll(`.table-cell:nth-child(-n+${stickyColumnCount})`) ?? null;

        if (!isNil(stickyCells)) {
            for (let i = 0; i < stickyCells.length; i += 1) {
                const stickyCell = stickyCells[i];
                stickyCell.classList.add(STICKY_CELL_CLASS);
                const isDefinitionInitalCell = definition && !fullWidth && i === 0;

                if (isDefinitionInitalCell) {
                    stickyCell.style.boxShadow = '';
                }

                const isFirstRowCell =
                    (stickyColumnCount === 1 || (i + 1) % stickyColumnCount === 1);

                if (isFirstRowCell) {
                    stickyCell.classList.add(STICKY_CELL_FIRST_OF_ROW_CLASS);
                }

                const isResizableCell = (i + 1) % stickyColumnCount === 0;

                if (isResizableCell) {
                    stickyCell.classList.add(STICKY_CELL_RESIZABLE_CLASS);
                }

                const isLastColumnCell = stickyCells.length - i <= stickyColumnCount;

                if (isLastColumnCell) {
                    stickyCell.classList.add(STICKY_CELL_LAST_OF_COLUMN_CLASS);
                }
            }
        }
    }

    forceTableUpdate() {
        this.forceUpdate();
    }

    render() {
        const {
            basic,
            celled,
            children,
            classes,
            className,
            collapsing,
            definition,
            fixed,
            fontSize,
            fullWidth,
            id,
            selectable,
            singleLine,
            size,
            stackable,
            stickyColumnCount,
            stretch,
            striped,
            style,
        } = this.props;

        const shouldHandleStickyBehavior = !fixed && !singleLine;

        const rootClasses = ClassNames(
            'ui',
            classes.root,
            'table', {
                'table-basic': basic,
                'table-celled': celled,
                'table-collapsing': collapsing,
                'table-definition': definition,
                'table-fixed': fixed,
                'table-font-size-large': fontSize === 'large',
                'table-font-size-medium': fontSize === 'medium',
                'table-font-size-small': fontSize === 'small',
                'table-font-size-xlarge': fontSize === 'xlarge',
                'table-font-size-xsmall': fontSize === 'xsmall',
                'table-font-size-xxsmall': fontSize === 'xxsmall',
                'table-full-width': fullWidth,
                'table-selectable': selectable,
                'table-single-line': singleLine,
                'table-size-medium': size === 'm' || size === 'medium',
                'table-size-small': size === 's' || size === 'small',
                'table-stretch': stretch && stretch !== 'very',
                'table-stretch-very': stretch === 'very',
                'table-striped': striped,
                'table-stackable': stackable,
                'table-unstackable': stackable === false,
                [classes.tableSticky]: stickyColumnCount,
            },
            className,
        );

        const tableChildren = stickyColumnCount > 0 ? this.parseChildren(children) : children;

        const tableJsx = (
            <table
                className={rootClasses}
                id={id}
                ref={(ref) => { this.tableRef = ref; }}
                style={style}
            >
                {tableChildren}
            </table>
        );

        if (stickyColumnCount > 0) {
            return (
                <div
                    className={classes.tableStickyColumns}
                    ref={(ref) => { this.tableStickyContainer = ref; }}
                >
                    <ScrollBar
                        className="table--scroll_container"
                        autoHide
                        onScrollFrame={this.setStickyCellsStylesOnScroll}
                        onUpdate={this.delayedSetStickyTableContainerWidth}
                        renderTrackHorizontal={(props) => (
                            <div
                                // eslint-disable-next-line react/jsx-props-no-spreading
                                {...props}
                                style={{
                                    position: 'absolute',
                                    height: 6,
                                    right: 2,
                                    bottom: 2,
                                    left: 2,
                                    borderRadius: 3,
                                    ...(!shouldHandleStickyBehavior && {
                                        display: 'none',
                                    }),
                                }}
                            />
                        )}
                        renderView={(props) => (
                            <div
                                // eslint-disable-next-line react/jsx-props-no-spreading
                                {...props}
                                style={{
                                    ...props.style,
                                    // if we don't set this the table doesn't appear
                                    position: 'relative',
                                }}
                            />
                        )}
                    >
                        {tableJsx}
                    </ScrollBar>
                </div>
            );
        }

        return tableJsx;
    }
}

Table.Body = TableBody;
Table.Cell = TableCell;
Table.Header = TableHeader;
Table.HeaderCell = TableHeaderCell;
Table.Row = TableRow;

Table.propTypes = propTypes;
Table.defaultProps = defaultProps;

export default withStyles(styles, { withTheme: true })(Table);
