import React, { useState, useContext, useEffect } from 'react';
import { Label, Div } from '@cloudflare/elements';
import {
  createStyledComponent,
  filterStyleProps
} from '@cloudflare/style-container';
import { Trans } from '@cloudflare/intl-react';
import { isMobile } from '@cloudflare/util-responsive';
import { useRenderOnResize } from '@cloudflare/util-hooks';
import Error from './Error';
import FormContext from './FormContext';
import { FieldValidator } from './types';

// Label hiding: https://www.w3.org/WAI/tutorials/forms/labels/#hiding-the-label-element
// Screen reader only: https://webaim.org/techniques/css/invisiblecontent/
// role="alert": https://www.w3.org/TR/WCAG20-TECHS/ARIA19.html

type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = T | U extends object
  ? (Without<T, U> & U) | (Without<U, T> & T)
  : T | U;

type LabelProps = React.ComponentProps<typeof Label>;
type DivProps = React.ComponentProps<typeof Div>;

// Placeholders break accessibility. Do not allow them.
export type BaseProps<ComponentProps, ValueType> = Omit<
  ComponentProps,
  'placeholder' | keyof DivProps
> &
  // label properties and labelledBy are mutually exclusive
  XOR<
    {
      label: React.ReactNode;
      labelProps?: LabelProps;
      hideLabel?: boolean;
    },
    {
      labelledBy: string;
    }
  > &
  // error properties and erroredBy are mutually exclusive
  XOR<
    {
      error?: React.ReactNode;
      errorProps?: DivProps;
    },
    {
      // id of the element displaying the error
      erroredBy?: string;
    }
  > &
  XOR<
    {
      description?: React.ReactNode;
      descriptionProps?: DivProps;
    },
    {
      // id of the element displaying the description
      describedBy?: string;
    }
  > & {
    name?: string;
    required?: boolean | ((value: ValueType) => boolean);
    requiredIndicator?: React.ReactNode;
    requiredMessage?: React.ReactNode;
    requiredValidate?: boolean;
    labelFirst?: boolean;
    componentStyleProps?: DivProps;
    invalid?: boolean;
    children?: React.ReactNode;
    className?: string;
    placeholder?: string;
    unwrapped?: boolean;
    validate?: FieldValidator<ValueType>;
  } & DivProps;

type InternalBaseProps<ComponentProps, ValueType> = BaseProps<
  ComponentProps,
  ValueType
> & {
  Component: React.ComponentType<ComponentProps>;
};

const screenReaderOnlyProps: Partial<LabelProps> = {
  position: 'absolute',
  left: '-10000px',
  top: 'auto',
  width: '1px',
  height: '1px',
  overflow: 'hidden'
};

let count = 0;

const makeIds = () => ({
  input: `cf-form-input${++count}`,
  label: `cf-form-label${count}`,
  error: `cf-form-error${count}`,
  description: `cf-form-description${count}`
});

export function UnstyledBase<ComponentProps, ValueType>({
  name,
  label,
  labelProps,
  hideLabel,
  labelledBy,
  description,
  descriptionProps,
  describedBy,
  error,
  errorProps,
  erroredBy,
  required = false,
  requiredIndicator = <Trans id="common.required_field" />,
  requiredMessage,
  labelFirst = true,
  children,
  Component,
  invalid,
  className,
  unwrapped,
  validate,
  requiredValidate = true, // Required validation can be disabled by setting false
  componentStyleProps,
  ...componentProps
}: InternalBaseProps<ComponentProps, ValueType>) {
  const [ids] = useState(makeIds);
  const context = useContext(FormContext);
  const isRow = !labelFirst || !!children;
  const wrapChildren = isRow && !isMobile();

  // Force rerender on screen width change.
  useRenderOnResize();

  // Handle field level validation
  useEffect(() => {
    // Field validation is disabled if the form has a validator
    if (!context.hasValidation && name) {
      let validator: FieldValidator<ValueType> | undefined = validate;
      if (requiredValidate && (required || requiredMessage)) {
        const requiredValidator = (val: any) =>
          val
            ? undefined
            : requiredMessage || (
                <Trans
                  id="common.is_required"
                  values={{ name: label || <Trans id="common.field" /> }}
                />
              );
        validator = validate
          ? Array.isArray(validate)
            ? [requiredValidator, ...validate]
            : [requiredValidator, validate]
          : requiredValidator;
      }

      validator && context.fieldValidators.set(name, validator);
      return () => {
        context.fieldValidators.delete(name);
      };
    }
  }, [
    name,
    validate,
    requiredValidate,
    context.hasValidation,
    required,
    requiredMessage,
    // Ensure effect depends on the contents of the validate array, rather than
    // the array itself, since arrays will most likely be recreated on each render
    ...(Array.isArray(validate) ? validate : [validate])
  ]);

  const labelContainer = !labelledBy && (
    <Label
      pr={2}
      mb={labelFirst ? 1 : 0}
      ml={labelFirst ? 0 : 2}
      {...labelProps}
      id={ids.label}
      htmlFor={ids.input}
      {...(hideLabel ? screenReaderOnlyProps : {})}
    >
      {label}
      {(required || requiredMessage) && <> {requiredIndicator}</>}
    </Label>
  );

  erroredBy = (name && context.fieldNameToErrorId.get(name)) || erroredBy;

  const describedBys = [
    erroredBy ?? (error ? ids.error : undefined),
    describedBy ?? (description ? ids.description : undefined)
  ]
    .filter(Boolean)
    .join(' ');

  const components = (
    <>
      {/* @ts-ignore */}
      <Component
        name={name}
        display="block"
        mb={children && isMobile() ? 2 : 0}
        mr={children ? 2 : 0}
        {...componentStyleProps}
        // Prevent styling props passing down to the component
        {...filterStyleProps(componentProps)}
        id={ids.input}
        invalid={invalid || !!error}
        aria-labelledby={labelledBy ?? ids.label}
        aria-required={required}
        aria-describedby={describedBys || undefined}
      />
      {!labelFirst && labelContainer}
      {children}
    </>
  );

  const desc = !describedBy && description && (
    <Div id={ids.description} fontSize={2} color="gray.2" {...descriptionProps}>
      {description}
    </Div>
  );

  const content = (
    <>
      {labelFirst && labelContainer}
      {wrapChildren ? (
        <Div display="flex" flexDirection="row" alignItems="center">
          {components}
        </Div>
      ) : (
        components
      )}
      {!erroredBy && error && (
        <Error {...errorProps} id={ids.error}>
          {error}
        </Error>
      )}
      {desc}
    </>
  );

  return unwrapped ? content : <div className={className}>{content}</div>;
}

const StyledBase = createStyledComponent(
  () => ({ marginBottom: 3 }),
  UnstyledBase
);

export function Base<ComponentProps, ValueType>(
  props: InternalBaseProps<ComponentProps, ValueType>
) {
  // @ts-ignore
  return <StyledBase {...props} />;
}
