import { Fragment, MutableRefObject, ReactNode, Ref, useCallback, useEffect, useLayoutEffect } from 'react';
import type { Theme } from '@mui/material';

import {
  ALPHA_100,
  ALPHA_200,
  ALPHA_300,
  ALPHA_400,
  ALPHA_500,
  ALPHA_600,
  ALPHA_700,
  ALPHA_800,
} from '@/mui/constants/opacity';

import { TYPE_NUMBER, TYPE_STRING } from '../constants/types';

const specialChaptersReg = /[-/\\^$*+?.()|[\]{}]/g;

export const autoSuggestHighlight = (query: string, label: string) => {
  const text$ = label?.trim() || '';
  const query$ = query?.trim() || '';

  if (!text$ || !query$) {
    return [
      {
        id: `text-${0}`,
        text: text$,
        highlight: false,
      },
    ];
  }

  const regex = new RegExp(`(${query$.replace(specialChaptersReg, '\\$&')})`, 'gi');

  return text$.split(regex).map((text, index) => ({
    id: `text-${index}`,
    text,
    highlight: text.toLowerCase() === query$.toLowerCase(),
  }));
};

export const getHashFromString = (str: string) => {
  let hash = 0;
  for (let i = 0; i < str.length; i += 1) {
    // eslint-disable-next-line no-bitwise
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }

  return hash;
};

export const getBgColor = (str: string) => {
  const hash = getHashFromString(str);

  let color = '#';
  for (let i = 0; i < 3; i += 1) {
    // eslint-disable-next-line no-bitwise
    const value = (hash >> (i * 8)) & 0xff;
    color += `00${value.toString(16)}`.slice(-2);
  }

  return color;
};

export const typeOf = (value: number | string) => {
  const type = Object.prototype.toString.call(value);

  return type === TYPE_NUMBER && Number.isNaN(value) ? NaN : type;
};

const toKebabKey = (key: string) =>
  key
    .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
    .toLowerCase();

export const toPXString = (value: number | string) => (typeof value === 'number' ? `${value}px` : value);

export function getCssVarsFromTheme(themeSource: Theme) {
  const theme$ = JSON.parse(
    JSON.stringify({
      palette: {
        ...themeSource.palette,
        mode: null,
        neutral: null,
        tonalOffset: null,
        contrastThreshold: null,
        grey: null,
      },
      zIndex: themeSource.zIndex,
      spacing: themeSource.spacing(1),
      shape: themeSource.shape,
      shadows: themeSource.shadows,
      typography: themeSource.typography,
      alpha: {
        100: `${ALPHA_100 * 100}%`,
        200: `${ALPHA_200 * 100}%`,
        300: `${ALPHA_300 * 100}%`,
        400: `${ALPHA_400 * 100}%`,
        500: `${ALPHA_500 * 100}%`,
        600: `${ALPHA_600 * 100}%`,
        700: `${ALPHA_700 * 100}%`,
        800: `${ALPHA_800 * 100}%`,
      },
    }),
  );

  const themeToProcess = {
    ...theme$,
    typography: Object.entries(theme$.typography).reduce((acc: { [key: string]: string }, [key, value]) => {
      let resValue: { [key: string]: string | null } | string | null = value as {
        [key: string]: string;
      };

      if (
        ['htmlFontSize', 'fontWeightLight', 'fontWeightRegular', 'fontWeightMedium', 'fontWeightBold'].includes(key)
      ) {
        resValue = null;
      } else if (typeof value === 'object') {
        resValue = {
          ...value,
          fontFamily: null,
          fontSize: toPXString(resValue.fontSize as string),
        };
      } else if (key === 'fontSize' && (typeof resValue === 'number' || typeof resValue === 'string')) {
        resValue = toPXString(resValue);
      }

      acc[key] = resValue as string;

      return acc;
    }, {}),
    shape: Object.entries(theme$.shape).reduce(
      (acc, [key, value]) => ({
        ...acc,
        [key]: toPXString(value as string),
      }),
      {},
    ),
  };

  const flattenRecursive = (obj: object, parentProperty: string, propertyMap: { [key: string]: string } = {}) => {
    Object.entries(obj).forEach(([key, value]) => {
      const kebabKey = toKebabKey(key);

      const property = parentProperty ? `${parentProperty}-${kebabKey}` : kebabKey;

      if (value !== null && typeof value === 'object') {
        flattenRecursive(value, property, propertyMap);
      } else if ([TYPE_STRING, TYPE_NUMBER].includes(typeOf(value) as string)) {
        // eslint-disable-next-line no-param-reassign
        propertyMap[property] = value;
      }
    });

    return propertyMap;
  };

  return Object.entries(flattenRecursive(themeToProcess, '--theme')).reduce(
    (acc: { [key: string]: string }, [key, value]) => {
      acc[key] = value;

      return acc;
    },
    {},
  );
}

type AttrMap = Record<string, string | boolean | number | undefined>;

export function customAttributes(attrs: AttrMap): Record<string, string | number> {
  const result: Record<string, string | number> = {};

  for (const key in attrs) {
    if (!Object.prototype.hasOwnProperty.call(attrs, key)) continue;

    const value = attrs[key];
    if (typeof value === 'boolean') {
      if (value) result[key] = '';
    } else if (typeof value === 'string') {
      result[key] = value;
    } else if (typeof value === 'number' && !Number.isNaN(value)) {
      result[key] = value;
    }
  }

  return result;
}

export function omitUndefinedProps<T extends object>(obj: T): Partial<T> {
  const result: Partial<T> = {};

  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const value = obj[key];
      if (value !== undefined) {
        result[key] = value;
      }
    }
  }

  return result;
}

export const countValidChildren = (children: ReactNode): number => {
  let count = 0;

  const iterateChildren = (childNodes: ReactNode): void => {
    if (Array.isArray(childNodes)) {
      childNodes.forEach(iterateChildren);
    } else if (childNodes && typeof childNodes === 'object' && 'type' in childNodes) {
      if (childNodes.type === Fragment) {
        // Recursively count children within fragments
        // @ts-expect-error should be rewrite
        iterateChildren(childNodes.props.children);
      } else {
        count += 1;
      }
    }
  };

  iterateChildren(children);

  return count;
};

export function useCombinedRefs<T>(...refs: (Ref<T> | undefined)[]) {
  return useCallback(
    (node: T | null) => {
      // eslint-disable-next-line no-restricted-syntax
      for (const ref of refs) {
        if (typeof ref === 'function') {
          ref(node);
        } else if (ref && typeof ref === 'object') {
          (ref as MutableRefObject<T | null>).current = node;
        }
      }
    },
    [refs],
  );
}

export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
