/**
 * External dependencies
 */
import { escape as lodashEscape, isObject, isArray, uniqBy } from "lodash";
import isDeepEqual from "fast-deep-equal/es6";

/**
 * WordPress dependencies
 */
import { __ } from "@wordpress/i18n";
import { addFilter } from "@wordpress/hooks";
import { createHigherOrderComponent } from "@wordpress/compose";
import { InspectorControls } from "@wordpress/block-editor";
import {
  Fill,
  BaseControl,
  __experimentalToolsPanel as ToolsPanel,
  __experimentalToolsPanelItem as ToolsPanelItem,
} from "@wordpress/components";
import { useEffect } from "@wordpress/element";

/**
 * Internal dependencies
 */
import {
  getSettingFieldValue,
  handleChangeSettingField,
  handleChangeMultipleSettingField,
  isUnsetValue,
  getColorCSSValue,
  addEditProps,
  usePropsStyle,
} from "sdk/utils";
import {
  CustomAttributes,
  AttributesControl,
} from "../../components/custom-attributes";
import { boldblocksHasSupport, isJSONString } from "../../utils";
import { labels } from "../../utils/labels";
import { toType } from "../../utils/dom";

/**
 * Define feature name
 */
const featureName = "customAttributes";

/**
 * Build attritubes for the feature.
 *
 * @param {Object} boldblocks
 * @param {Object}
 */
function buildFeatureAttributes(boldblocks) {
  let dataset = {};
  let style = {};
  const { blockAttributes = [], customAttributes = [] } = boldblocks;

  if (!blockAttributes.length && !customAttributes.length) {
    return dataset;
  }

  const attributes = uniqBy(blockAttributes.concat(customAttributes), "name");

  const nameRegex = new RegExp(
    "^[a-zA-Z]([a-zA-Z0-9])*([-]([a-zA-Z][a-zA-Z0-9]*)*)*",
  );

  attributes.forEach(({ attributeType = "data", name, type, value }) => {
    if (type === "boolean") {
      if (["data", "aria", "var", "custom"].includes(attributeType)) {
        value = isUnsetValue(value) ? false : value;
      } else {
        if (value) {
          value = ""; // Empty is a valid boolean html attribute
        } else {
          name = ""; // Remove false attribute
        }
      }
    } else if (type === "json") {
      if (!isJSONString(value)) {
        name = ""; // Remove this attribute
      }
    } else if (type === "color" || type === "background") {
      value = getColorCSSValue(value);
    }

    if (nameRegex.test(name)) {
      const attrValue =
        isObject(value) || isArray(value)
          ? JSON.stringify(value)
          : lodashEscape(value);

      if (attributeType === "var") {
        style[`--bb-attr--${name}`] = attrValue;
      } else {
        if (attributeType !== "custom") {
          dataset[`${attributeType}-${name}`] = attrValue;
        } else {
          dataset[name] = attrValue;
        }
      }
    }
  });

  return { dataset, style };
}

/**
 * Override the default edit UI to include new inspector controls for custom settings.
 *
 * @param {Function} BlockEdit Block edit component.
 * @return {Function} BlockEdit Modified block edit component.
 */
const withInspectorControls = createHigherOrderComponent((BlockEdit) => {
  return (props) => {
    if (!boldblocksHasSupport(props.name, featureName)) {
      return <BlockEdit {...props} />;
    }

    const blockAttributes = boldblocksHasSupport(props.name, "blockAttributes");
    if (
      !blockAttributes?.attributes?.length &&
      !blockAttributes?.enableCustomAttributes
    ) {
      return <BlockEdit {...props} />;
    }

    const { attributes, setAttributes, isSelected } = props;

    const customAttributes = getSettingFieldValue({
      fieldName: "customAttributes",
      attributes,
      defaultValue: [],
    });

    const onFeatureChange = (fieldName) => (newValue) => {
      handleChangeSettingField({
        fieldName,
        setAttributes,
        attributes,
      })(newValue);
    };

    let blockAttritubesValue = [];
    if (!!blockAttributes?.attributes?.length) {
      const savedAttritubesValue = getSettingFieldValue({
        fieldName: "blockAttributes",
        attributes,
        defaultValue: [],
      });

      // Pre-defined attributes changed.
      let attributeChanged = false;

      blockAttritubesValue = blockAttributes.attributes.map((item, index) => {
        item = { ...item, default: item?.value };
        const itemValue = savedAttritubesValue[index] ?? false;
        if (!itemValue) {
          attributeChanged = true;
          return item;
        } else {
          if (itemValue?.name !== item?.name) {
            attributeChanged = true;
            return item;
          } else {
            if (
              itemValue?.attributeType !== item?.attributeType ||
              itemValue?.type !== item?.type ||
              itemValue?.multiple !== item?.multiple
            ) {
              attributeChanged = true;
            }

            return {
              ...item,
              value: itemValue?.value,
            };
          }
        }
      });

      useEffect(() => {
        if (
          attributeChanged ||
          savedAttritubesValue.length !== blockAttritubesValue.length
        ) {
          // Save new attributes
          onFeatureChange("blockAttributes")(blockAttritubesValue);
        }
      }, [
        attributeChanged,
        savedAttritubesValue.length,
        blockAttritubesValue.length,
      ]);
    }

    const customAttributesControl = (
      <CustomAttributes
        values={customAttributes}
        onChange={onFeatureChange("customAttributes")}
        startIndex={blockAttributes?.attributes?.length ?? 0}
      />
    );

    return (
      <>
        <BlockEdit {...props} />
        {isSelected && (
          <>
            <Fill name="cbb-block-toolbar">
              <div className="block-custom-attributes">
                <BaseControl label={labels.attributes} />
                {!!blockAttributes?.attributes?.length && (
                  <CustomAttributes
                    isEditable={false}
                    values={blockAttritubesValue}
                    onChange={onFeatureChange("blockAttributes")}
                  />
                )}
                {blockAttributes?.enableCustomAttributes &&
                  customAttributesControl}
              </div>
            </Fill>
            <InspectorControls>
              <ToolsPanel
                label={labels.attributes}
                resetAll={() => {
                  let resetValues = { customAttributes: [] };
                  if (!!blockAttributes?.attributes?.length) {
                    resetValues = {
                      ...resetValues,
                      blockAttributes: blockAttributes.attributes,
                    };
                  }
                  handleChangeMultipleSettingField({
                    attributes,
                    setAttributes,
                  })(resetValues);
                }}
                className="cbb-settings"
              >
                {blockAttritubesValue.map((item, index) => {
                  return (
                    <ToolsPanelItem
                      key={index}
                      label={item?.label ?? item?.name}
                      isShownByDefault={
                        item?.type === "label" || item?.initialOpen
                      }
                      hasValue={() =>
                        item?.type !== "label" &&
                        (toType(item?.value) === "object"
                          ? !isDeepEqual(item?.value, item?.default)
                          : item?.value !== item?.default)
                      }
                      onDeselect={() =>
                        onFeatureChange("blockAttributes")(
                          blockAttritubesValue.with(index, {
                            ...item,
                            value: item?.default,
                          }),
                        )
                      }
                      className={`attribute-${item?.type}`}
                    >
                      <AttributesControl
                        values={item}
                        onChange={(value) =>
                          onFeatureChange("blockAttributes")(
                            blockAttritubesValue.with(index, value),
                          )
                        }
                        isEditable={false}
                        setOpenStates={false}
                      />
                    </ToolsPanelItem>
                  );
                })}
                {blockAttributes?.enableCustomAttributes && (
                  <ToolsPanelItem
                    label={__("Add attributes", "content-blocks-builder")}
                    hasValue={() => customAttributes?.length}
                    onDeselect={() => onFeatureChange("customAttributes")([])}
                    className="block-custom-attributes"
                  >
                    {customAttributesControl}
                  </ToolsPanelItem>
                )}
              </ToolsPanel>
            </InspectorControls>
          </>
        )}
      </>
    );
  };
}, "withInspectorControls");
addFilter(
  "editor.BlockEdit",
  `boldblocks/${featureName}/withInspectorControls`,
  withInspectorControls,
);

/**
 * Override props assigned to save component to inject the CSS variables definition.
 *
 * @param {Object} props      Additional props applied to save element.
 * @param {Object} blockType  Block type.
 * @param {Object} attributes Block attributes.
 *
 * @return {Object} Filtered props applied to save element.
 */
export function addSaveProps(props, blockType, attributes) {
  if (!boldblocksHasSupport(blockType, featureName)) {
    return props;
  }

  const { boldblocks = {} } = attributes;

  let featureAttributes;
  if (attributes?.isBlockEdit) {
    featureAttributes = usePropsStyle({
      value: boldblocks,
      getStyle: (value) => () => buildFeatureAttributes(value),
    });
  } else {
    featureAttributes = buildFeatureAttributes(boldblocks);
  }

  props = {
    ...props,
    ...(featureAttributes?.dataset ?? {}),
    style: { ...(props?.style ?? {}), ...(featureAttributes?.style ?? {}) },
  };

  return props;
}
addFilter(
  "blocks.getSaveContent.extraProps",
  `boldblocks/${featureName}/addSaveProps`,
  addSaveProps,
);

/**
 * Filters registered block settings to extend the block edit wrapper
 * to apply the desired styles and className properly.
 *
 * @param {Object} settings Original block settings.
 *
 * @return {Object}.Filtered block settings.
 */
addFilter(
  "blocks.registerBlockType",
  `boldblocks/${featureName}/addEditProps`,
  addEditProps(
    addSaveProps,
    (settings, { featureName }) => boldblocksHasSupport(settings, featureName),
    { featureName },
  ),
);
