import React from 'react';
import classnames from 'classnames';
import { memoize } from 'lodash';

import { BREAKPOINTS } from '../../../helpers/constants/design-system';

import type {
  BoxComponent,
  BoxProps,
  PolymorphicRef,
  StyleDeclarationType,
  StylePropValueType,
  ClassNamesObject,
} from './box.types';

const BASE_CLASS_NAME = 'mm-box';

function isValidSize(
  styleProp: StyleDeclarationType,
  value: StylePropValueType,
) {
  // Only margin types allow 'auto'
  return (
    typeof value === 'number' ||
    ((styleProp === 'margin' ||
      styleProp === 'margin-top' ||
      styleProp === 'margin-right' ||
      styleProp === 'margin-bottom' ||
      styleProp === 'margin-left' ||
      styleProp === 'margin-inline' ||
      styleProp === 'margin-inline-start' ||
      styleProp === 'margin-inline-end') &&
      value === 'auto')
  );
}

function isValidString(type: StyleDeclarationType, value: StylePropValueType) {
  return typeof type === 'string' && typeof value === 'string';
}

/**
 * Generate classnames
 * Generates classnames for different utility styles
 * Also accepts responsive props in the form of an array
 * Maps responsive props to mobile first breakpoints
 *
 * @param {string} styleDeclaration - The style declaration type "margin", "margin-top", "padding", "display" etc
 * @param {array || number || string} value - prop value being passed in array props are responsive props
 * @param {*} validatorFn - The validation function for each type of value
 * @returns
 */

const generateClassNames = memoize(
  (
    styleDeclaration: StyleDeclarationType,
    value: StylePropValueType,
    validatorFn: typeof isValidString | typeof isValidSize,
  ) => {
    // if value does not exist return empty object for classnames library
    // Accepts 0 as a valid value
    if (!value && typeof value !== 'number') {
      return {};
    }
    const classNamesObject: ClassNamesObject = {};
    // if value is an array with single item e.g. marginTop={[1]}
    const singleArrayItemProp =
      Array.isArray(value) && value.length === 1 ? value[0] : undefined;
    // if value single value e.g. marginTop={1}
    const singleValueProp =
      (!Array.isArray(value) && typeof value === 'string') ||
      typeof value === 'number'
        ? value
        : undefined;
    // single digit equals single value or single array item
    let singleValue;
    if (singleValueProp || singleValueProp === 0) {
      singleValue = singleValueProp;
    }
    if (singleArrayItemProp || singleArrayItemProp === 0) {
      singleValue = singleArrayItemProp;
    }
    // 0 is an acceptable value but is falsy in js
    if (singleValue || singleValue === 0) {
      // add base style without any breakpoint prefixes to classObject
      classNamesObject[
        `${BASE_CLASS_NAME}--${styleDeclaration}-${singleValue}`
      ] = validatorFn(styleDeclaration, singleValue);
    } else if (Array.isArray(value)) {
      // If array with more than one item
      switch (value.length) {
        case 4:
          // add base/sm/md/lg
          classNamesObject[
            `${BASE_CLASS_NAME}--${styleDeclaration}-${value[0]}`
          ] = validatorFn(styleDeclaration, value[0]);
          classNamesObject[
            `${BASE_CLASS_NAME}--${BREAKPOINTS[1]}:${styleDeclaration}-${value[1]}`
          ] = validatorFn(styleDeclaration, value[1]);
          classNamesObject[
            `${BASE_CLASS_NAME}--${BREAKPOINTS[2]}:${styleDeclaration}-${value[2]}`
          ] = validatorFn(styleDeclaration, value[2]);
          classNamesObject[
            `${BASE_CLASS_NAME}--${BREAKPOINTS[3]}:${styleDeclaration}-${value[3]}`
          ] = validatorFn(styleDeclaration, value[3]);
          break;
        case 3:
          // add base/sm/md
          classNamesObject[
            `${BASE_CLASS_NAME}--${styleDeclaration}-${value[0]}`
          ] = validatorFn(styleDeclaration, value[0]);
          classNamesObject[
            `${BASE_CLASS_NAME}--${BREAKPOINTS[1]}:${styleDeclaration}-${value[1]}`
          ] = validatorFn(styleDeclaration, value[1]);
          classNamesObject[
            `${BASE_CLASS_NAME}--${BREAKPOINTS[2]}:${styleDeclaration}-${value[2]}`
          ] = validatorFn(styleDeclaration, value[2]);
          break;
        case 2:
          // add base/sm
          classNamesObject[
            `${BASE_CLASS_NAME}--${styleDeclaration}-${value[0]}`
          ] = validatorFn(styleDeclaration, value[0]);
          classNamesObject[
            `${BASE_CLASS_NAME}--${BREAKPOINTS[1]}:${styleDeclaration}-${value[1]}`
          ] = validatorFn(styleDeclaration, value[1]);
          break;
        default:
          console.log(`Invalid array prop length: ${value.length}`);
      }
    }
    return classNamesObject;
  },
  (styleDeclaration, value) => [styleDeclaration, value],
);

export const Box: BoxComponent = React.forwardRef(
  <C extends React.ElementType = 'div'>(
    {
      as,
      padding,
      paddingTop,
      paddingRight,
      paddingBottom,
      paddingLeft,
      paddingInline,
      paddingInlineStart,
      paddingInlineEnd,
      margin,
      marginTop,
      marginRight,
      marginBottom,
      marginLeft,
      marginInline,
      marginInlineStart,
      marginInlineEnd,
      borderColor,
      borderWidth,
      borderRadius,
      borderStyle,
      alignItems,
      justifyContent,
      textAlign,
      flexDirection,
      flexWrap,
      gap,
      display,
      width,
      height,
      children,
      className = '',
      backgroundColor,
      color,
      ...props
    }: BoxProps<C>,
    ref?: PolymorphicRef<C>,
  ) => {
    const Component = as || 'div';
    const boxClassName = classnames(
      BASE_CLASS_NAME,
      className,
      // Margin
      generateClassNames('margin', margin, isValidSize),
      generateClassNames('margin-top', marginTop, isValidSize),
      generateClassNames('margin-right', marginRight, isValidSize),
      generateClassNames('margin-bottom', marginBottom, isValidSize),
      generateClassNames('margin-left', marginLeft, isValidSize),
      generateClassNames('margin-inline', marginInline, isValidSize),
      generateClassNames('margin-inline-start', marginInlineStart, isValidSize),
      generateClassNames('margin-inline-end', marginInlineEnd, isValidSize),
      // Padding
      generateClassNames('padding', padding, isValidSize),
      generateClassNames('padding-top', paddingTop, isValidSize),
      generateClassNames('padding-right', paddingRight, isValidSize),
      generateClassNames('padding-bottom', paddingBottom, isValidSize),
      generateClassNames('padding-left', paddingLeft, isValidSize),
      generateClassNames('padding-inline', paddingInline, isValidSize),
      generateClassNames(
        'padding-inline-start',
        paddingInlineStart,
        isValidSize,
      ),
      generateClassNames('padding-inline-end', paddingInlineEnd, isValidSize),
      generateClassNames('display', display, isValidString),
      generateClassNames('gap', gap, isValidSize),
      generateClassNames('flex-direction', flexDirection, isValidString),
      generateClassNames('flex-wrap', flexWrap, isValidString),
      generateClassNames('justify-content', justifyContent, isValidString),
      generateClassNames('align-items', alignItems, isValidString),
      generateClassNames('text-align', textAlign, isValidString),
      generateClassNames('width', width, isValidString),
      generateClassNames('height', height, isValidString),
      generateClassNames('color', color, isValidString),
      generateClassNames('background-color', backgroundColor, isValidString),
      generateClassNames('rounded', borderRadius, isValidString),
      generateClassNames('border-style', borderStyle, isValidString),
      generateClassNames('border-color', borderColor, isValidString),
      generateClassNames('border-width', borderWidth, isValidSize),
      {
        // Auto applied classes
        // ---Borders---
        // if borderWidth or borderColor is supplied w/o style, default to solid
        'box--border-style-solid':
          !borderStyle && (Boolean(borderWidth) || Boolean(borderColor)),
        // if borderColor supplied w/o width, default to 1
        'box--border-width-1': !borderWidth && Boolean(borderColor),
      },
    );
    return (
      <Component className={boxClassName} ref={ref} {...props}>
        {children}
      </Component>
    );
  },
);
