'use strict';

const { lazy } = require('@discordjs/util');
const { ComponentType } = require('discord-api-types/v10');

// Fixes circular dependencies.
const getActionRow = lazy(() => require('../structures/ActionRow.js').ActionRow);
const getButtonComponent = lazy(() => require('../structures/ButtonComponent.js').ButtonComponent);
const getChannelSelectMenuComponent = lazy(
  () => require('../structures/ChannelSelectMenuComponent.js').ChannelSelectMenuComponent,
);
const getComponent = lazy(() => require('../structures/Component.js').Component);
const getContainerComponent = lazy(() => require('../structures/ContainerComponent.js').ContainerComponent);
const getFileComponent = lazy(() => require('../structures/FileComponent.js').FileComponent);
const getLabelComponent = lazy(() => require('../structures/LabelComponent.js').LabelComponent);
const getMediaGalleryComponent = lazy(() => require('../structures/MediaGalleryComponent.js').MediaGalleryComponent);
const getMentionableSelectMenuComponent = lazy(
  () => require('../structures/MentionableSelectMenuComponent.js').MentionableSelectMenuComponent,
);
const getRoleSelectMenuComponent = lazy(
  () => require('../structures/RoleSelectMenuComponent.js').RoleSelectMenuComponent,
);
const getSectionComponent = lazy(() => require('../structures/SectionComponent.js').SectionComponent);
const getSeparatorComponent = lazy(() => require('../structures/SeparatorComponent.js').SeparatorComponent);
const getStringSelectMenuComponent = lazy(
  () => require('../structures/StringSelectMenuComponent.js').StringSelectMenuComponent,
);
const getTextDisplayComponent = lazy(() => require('../structures/TextDisplayComponent.js').TextDisplayComponent);
const getTextInputComponent = lazy(() => require('../structures/TextInputComponent.js').TextInputComponent);
const getThumbnailComponent = lazy(() => require('../structures/ThumbnailComponent.js').ThumbnailComponent);
const getUserSelectMenuComponent = lazy(
  () => require('../structures/UserSelectMenuComponent.js').UserSelectMenuComponent,
);

/**
 * @typedef {Object} BaseComponentData
 * @property {number} [id] the id of this component
 * @property {ComponentType} type The type of component
 */

/**
 * @typedef {BaseComponentData} ActionRowData
 * @property {ComponentData[]} components The components in this action row
 */

/**
 * @typedef {Object} ModalComponentData
 * @property {string} title The title of the modal
 * @property {string} customId The custom id of the modal
 * @property {Array<TextDisplayComponentData|LabelData>} components The components within this modal
 */

/**
 * @typedef {StringSelectMenuComponentData|TextInputComponentData|UserSelectMenuComponentData|
 * RoleSelectMenuComponentData|MentionableSelectMenuComponentData|ChannelSelectMenuComponentData|FileUploadComponentData|
 * RadioGroupComponentData|CheckboxGroupComponentData|CheckboxComponentData} ComponentInLabelData
 */

/**
 * @typedef {BaseComponentData} LabelData
 * @property {string} label The label to use
 * @property {string} [description] The optional description for the label
 * @property {ComponentInLabelData} component The component within the label
 */

/**
 * @typedef {BaseComponentData} ButtonComponentData
 * @property {ButtonStyle} style The style of the button
 * @property {boolean} [disabled] Whether this button is disabled
 * @property {string} label The label of this button
 * @property {APIMessageComponentEmoji} [emoji] The emoji on this button
 * @property {string} [customId] The custom id of the button
 * @property {string} [url] The URL of the button
 */

/**
 * @typedef {BaseComponentData} FileUploadComponentData
 * @property {string} customId The custom id of the file upload
 * @property {number} [minValues] The minimum number of files that must be uploaded (0-10)
 * @property {number} [maxValues] The maximum number of files that can be uploaded (1-10)
 * @property {boolean} [required] Whether this component is required in modals
 */

/**
 * @typedef {Object} RadioGroupOption
 * @property {string} value The value of the radio group option
 * @property {string} label The label to use
 * @property {string} [description] The optional description for the radio group option
 * @property {boolean} [default] Whether this option is default selected
 */

/**
 * @typedef {BaseComponentData} RadioGroupComponentData
 * @property {string} customId The custom id of the radio group
 * @property {RadioGroupOption[]} options The options in this radio group (2-10)
 * @property {boolean} [required] Whether this component is required in modals
 */

/**
 * @typedef {Object} CheckboxGroupOption
 * @property {string} value The value of the checkbox group option
 * @property {string} label The label to use
 * @property {string} [description] The optional description for the checkbox group option
 * @property {boolean} [default] Whether this option is default selected
 */

/**
 * @typedef {BaseComponentData} CheckboxGroupComponentData
 * @property {string} customId The custom id of the checkbox group
 * @property {CheckboxGroupOption[]} options The options in this checkbox group
 * @property {number} [minValues] The minimum number of options that must be selected (0-10)
 * @property {number} [maxValues] The maximum number of options that can be selected (defaults to options length)
 * @property {boolean} [required] Whether this component is required in modals
 */

/**
 * @typedef {BaseComponentData} CheckboxComponentData
 * @property {string} customId The custom id of the checkbox
 * @property {boolean} [default] Whether this component is default selected in modals
 */

/**
 * @typedef {BaseComponentData} BaseSelectMenuComponentData
 * @property {string} customId The custom id of the select menu
 * @property {boolean} [disabled] Whether the select menu is disabled or not
 * @property {number} [maxValues] The maximum amount of options that can be selected
 * @property {number} [minValues] The minimum amount of options that must be selected
 * @property {string} [placeholder] The placeholder of the select menu
 * @property {boolean} [required] Whether this component is required in modals
 */

/**
 * @typedef {BaseSelectMenuComponentData} StringSelectMenuComponentData
 * @property {SelectMenuComponentOptionData[]} [options] The options in this select menu
 */

/**
 * @typedef {BaseSelectMenuComponentData} UserSelectMenuComponentData
 * @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
 */

/**
 * @typedef {BaseSelectMenuComponentData} RoleSelectMenuComponentData
 * @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
 */

/**
 * @typedef {BaseSelectMenuComponentData} MentionableSelectMenuComponentData
 * @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
 */

/**
 * @typedef {BaseSelectMenuComponentData} ChannelSelectMenuComponentData
 * @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
 * @property {ChannelType[]} [channelTypes] The types of channels that can be selected
 */

/**
 * @typedef {Object} SelectMenuComponentOptionData
 * @property {string} label The label of the option
 * @property {string} value The value of the option
 * @property {string} [description] The description of the option
 * @property {APIMessageComponentEmoji} [emoji] The emoji on the option
 * @property {boolean} [default] Whether this option is selected by default
 */

/**
 * @typedef {BaseComponentData} SelectMenuComponentData
 * @property {string} customId The custom id of the select menu
 * @property {boolean} [disabled] Whether the select menu is disabled or not
 * @property {number} [maxValues] The maximum amount of options that can be selected
 * @property {number} [minValues] The minimum amount of options that can be selected
 * @property {SelectMenuComponentOptionData[]} [options] The options in this select menu
 * @property {string} [placeholder] The placeholder of the select menu
 */

/**
 * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData} MessageComponentData
 */

/**
 * @typedef {BaseComponentData} TextInputComponentData
 * @property {string} customId The custom id of the text input
 * @property {TextInputStyle} style The style of the text input
 * @property {number} [minLength] The minimum number of characters that can be entered in the text input
 * @property {number} [maxLength] The maximum number of characters that can be entered in the text input
 * @property {boolean} [required] Whether or not the text input is required or not
 * @property {string} [value] The pre-filled text in the text input
 * @property {string} [placeholder] Placeholder for the text input
 */

/**
 * @typedef {Object} UnfurledMediaItemData
 * @property {string} url The url of this media item. Accepts either http:, https: or attachment: protocol
 */

/**
 * @typedef {BaseComponentData} ThumbnailComponentData
 * @property {UnfurledMediaItemData} media The media for the thumbnail
 * @property {string} [description] The description of the thumbnail
 * @property {boolean} [spoiler] Whether the thumbnail should be spoilered
 */

/**
 * @typedef {BaseComponentData} FileComponentData
 * @property {UnfurledMediaItemData} file The file media in this component
 * @property {boolean} [spoiler] Whether the file should be spoilered
 */

/**
 * @typedef {Object} MediaGalleryItemData
 * @property {UnfurledMediaItemData} media The media for the media gallery item
 * @property {string} [description] The description of the media gallery item
 * @property {boolean} [spoiler] Whether the media gallery item should be spoilered
 */

/**
 * @typedef {BaseComponentData} MediaGalleryComponentData
 * @property {MediaGalleryItemData[]} items The media gallery items in this media gallery component
 */

/**
 * @typedef {BaseComponentData} SeparatorComponentData
 * @property {SeparatorSpacingSize} [spacing] The spacing size of this component
 * @property {boolean} [divider] Whether the separator shows as a divider
 */

/**
 * @typedef {BaseComponentData} SectionComponentData
 * @property {Components[]} components The components in this section
 * @property {ButtonComponentData|ThumbnailComponentData} accessory The accessory shown next to this section
 */

/**
 * @typedef {BaseComponentData} TextDisplayComponentData
 * @property {string} content The content displayed in this component
 */

/**
 * @typedef {ActionRowData|FileComponentData|MediaGalleryComponentData|SectionComponentData|
 * SeparatorComponentData|TextDisplayComponentData} ComponentInContainerData
 */

/**
 * @typedef {BaseComponentData} ContainerComponentData
 * @property {ComponentInContainerData} components The components in this container
 * @property {?number} [accentColor] The accent color of this container
 * @property {boolean} [spoiler] Whether the container should be spoilered
 */

/**
 * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData|
 * ThumbnailComponentData|FileComponentData|MediaGalleryComponentData|SeparatorComponentData|
 * SectionComponentData|TextDisplayComponentData|ContainerComponentData} ComponentData
 */

/**
 * @typedef {ActionRow|ContainerComponent|FileComponent|MediaGalleryComponent|
 * SectionComponent|SeparatorComponent|TextDisplayComponent} MessageTopLevelComponent
 */

const ComponentTypeToClass = {
  [ComponentType.ActionRow]: getActionRow,
  [ComponentType.Button]: getButtonComponent,
  [ComponentType.StringSelect]: getStringSelectMenuComponent,
  [ComponentType.TextInput]: getTextInputComponent,
  [ComponentType.UserSelect]: getUserSelectMenuComponent,
  [ComponentType.RoleSelect]: getRoleSelectMenuComponent,
  [ComponentType.MentionableSelect]: getMentionableSelectMenuComponent,
  [ComponentType.ChannelSelect]: getChannelSelectMenuComponent,
  [ComponentType.Container]: getContainerComponent,
  [ComponentType.TextDisplay]: getTextDisplayComponent,
  [ComponentType.File]: getFileComponent,
  [ComponentType.MediaGallery]: getMediaGalleryComponent,
  [ComponentType.Section]: getSectionComponent,
  [ComponentType.Separator]: getSeparatorComponent,
  [ComponentType.Thumbnail]: getThumbnailComponent,
  [ComponentType.Label]: getLabelComponent,
};

/**
 * Transforms API data into a component
 *
 * @param {APIMessageComponent|Component} data The data to create the component from
 * @returns {Component}
 * @ignore
 */
function createComponent(data) {
  return data instanceof getComponent() ? data : new (ComponentTypeToClass[data.type]?.() ?? getComponent())(data);
}

/**
 * Extracts all interactive components from the component tree
 *
 * @param {Component|APIMessageComponent} component The component to find all interactive components in
 * @returns {Array<Component|APIMessageComponent>}
 * @ignore
 */
function extractInteractiveComponents(component) {
  switch (component.type) {
    case ComponentType.ActionRow:
      return component.components;
    case ComponentType.Section:
      return [...component.components, component.accessory];
    case ComponentType.Container:
      return component.components.flatMap(extractInteractiveComponents);
    default:
      return [component];
  }
}

/**
 * Finds a component by customId in nested components
 *
 * @param {Array<Component|APIMessageComponent>} components The components to search in
 * @param {string} customId The customId to search for
 * @returns {Component|APIMessageComponent}
 * @ignore
 */
function findComponentByCustomId(components, customId) {
  return (
    components
      .flatMap(extractInteractiveComponents)
      .find(component => (component.customId ?? component.custom_id) === customId) ?? null
  );
}

exports.createComponent = createComponent;
exports.findComponentByCustomId = findComponentByCustomId;
