import React, { useCallback, useEffect, useMemo, useState } from 'react' import { FlatList, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native' import Animated, { useAnimatedRef } from 'react-native-reanimated' import Sortable from 'react-native-sortables' import { useGlobal } from 'reactn' import { icons } from '../../assets' import { Palette } from '../../styles' import { FONT_FAMILY } from '../../styles/Fonts' import { arraysAreSame, formatStructureLabel, getSegmentMeta, OPTIONAL_STRUCTURE_SEGMENTS, sanitizeStructureList, } from '../../utils/songStructure' import ItemContainer from '../../components/ItemContainer/ItemContainer' import CreateLyricsHeader from './components/CreateLyricsHeader' const randomSuffix = () => Math.random().toString(36).slice(2, 8) const createSortableItem = (type, index = 0) => ({ id: `${type}-${index}-${randomSuffix()}`, type, value: type, }) const PRE_REFRAIN_TYPE = 'pre_refrain_instrumental' const INTRO_TYPES = new Set(['short_intro', 'long_intro', 'intro', 'introduction']) const PRE_REFRAIN_ERROR_MESSAGE = 'Impossible de positionner un pré-refrain ici. Place-le juste avant un refrain.' const INTRO_ERROR_MESSAGE = "L'introduction musicale doit rester en première position." const SANITIZE_OPTIONS = { autoInjectPreChorus: false } const buildSortableItems = (values = []) => sanitizeStructureList(values, SANITIZE_OPTIONS).map((type, index) => createSortableItem(type, index) ) const validateStructureRules = (values = []) => { for (let index = 0; index < values.length; index += 1) { const type = values[index] if (type === PRE_REFRAIN_TYPE) { const nextType = values[index + 1] if (nextType !== 'refrain') { return { valid: false, reason: 'pre_refrain_invalid_position', message: PRE_REFRAIN_ERROR_MESSAGE, } } } } const introIndex = values.findIndex((type) => INTRO_TYPES.has(type)) if (introIndex > 0) { return { valid: false, reason: 'intro_must_be_first', message: INTRO_ERROR_MESSAGE, } } return { valid: true } } const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { const scrollRef = useAnimatedRef() const [, setTooltip] = useGlobal('_tooltip') const sanitizedBase = useMemo( () => sanitizeStructureList(baseStructure, SANITIZE_OPTIONS), [baseStructure] ) const extractValues = useCallback( (items) => (items || []).map((item) => item?.type || item?.value).filter(Boolean), [] ) const [sortableItems, setSortableItems] = useState(() => buildSortableItems(sanitizedBase)) const showTooltip = useCallback( (text, type = 'error') => { if (!text) return setTooltip({ text, type }) }, [setTooltip] ) useEffect(() => { setSortableItems((prev) => { const currentValues = extractValues(prev) if (arraysAreSame(currentValues, sanitizedBase)) { return prev } return buildSortableItems(sanitizedBase) }) }, [sanitizedBase, extractValues]) useEffect(() => { const values = extractValues(sortableItems) onChange?.(values) }, [sortableItems, onChange, extractValues]) const labeledItems = useMemo(() => { const counts = {} return sortableItems.map((item) => { const baseType = item.type || item.value const nextCount = (counts[baseType] || 0) + 1 counts[baseType] = nextCount return { ...item, type: baseType, value: baseType, label: formatStructureLabel(baseType, nextCount), sortable: true, } }) }, [sortableItems]) const optionalSegments = useMemo( () => OPTIONAL_STRUCTURE_SEGMENTS.map((type) => { const meta = getSegmentMeta(type) return { type, label: meta?.label || formatStructureLabel(type, 1) } }), [] ) const segmentCounts = useMemo(() => { const counts = {} extractValues(sortableItems).forEach((type) => { counts[type] = (counts[type] || 0) + 1 }) return counts }, [sortableItems, extractValues]) const handleRemoveItem = useCallback( (itemId) => { setSortableItems((prev) => { const remaining = prev.filter((item) => item.id !== itemId) const sanitized = sanitizeStructureList(extractValues(remaining), SANITIZE_OPTIONS) return buildSortableItems(sanitized) }) }, [extractValues] ) const handleOptionalSegmentPress = useCallback( (type) => { const meta = getSegmentMeta(type) setSortableItems((prev) => { const currentValues = extractValues(prev) let nextValues = [...currentValues] if (meta?.exclusiveGroup) { nextValues = nextValues.filter((value) => { const itemMeta = getSegmentMeta(value) return itemMeta?.exclusiveGroup !== meta.exclusiveGroup }) } const allowsMultiple = meta?.allowMultiple !== false if (!allowsMultiple && nextValues.includes(type)) { const filtered = nextValues.filter((value) => value !== type) const sanitized = sanitizeStructureList(filtered, SANITIZE_OPTIONS) return buildSortableItems(sanitized) } if (type === PRE_REFRAIN_TYPE) { const firstRefrainIndex = nextValues.findIndex((value) => value === 'refrain') if (firstRefrainIndex === -1) { showTooltip(PRE_REFRAIN_ERROR_MESSAGE) return prev } nextValues.splice(firstRefrainIndex, 0, type) } else if (meta?.exclusiveGroup === 'intro') { nextValues.unshift(type) } else if (meta?.exclusiveGroup === 'outro') { nextValues.push(type) } else { nextValues.push(type) } const validation = validateStructureRules(nextValues) if (!validation.valid) { showTooltip(validation.message) return prev } const sanitized = sanitizeStructureList(nextValues, SANITIZE_OPTIONS) return buildSortableItems(sanitized) }) }, [extractValues, showTooltip] ) const Row = ({ item: rowData, onPress }) => { const baseType = rowData?.type || rowData?.value const meta = getSegmentMeta(baseType) const canRemove = rowData?.sortable && meta?.optional === true const selected = rowData?.selected const count = rowData?.count ?? 0 const containerStyle = StyleSheet.flatten([styles.rowContainer]) const blurStyle = StyleSheet.flatten([styles.rowBlur, selected ? styles.selectedBlur : null]) const badgeStyle = StyleSheet.flatten([ styles.countBadge, count > 0 ? styles.countBadgeActive : null, ]) return ( {rowData?.label || ''} {rowData?.sortable ? ( {canRemove && ( handleRemoveItem(rowData.id)} style={styles.removeButton} hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }} > )} ) : ( {count} )} ) } return ( Structure actuelle } customHandle scrollableRef={scrollRef} onDragEnd={({ data: newData }) => { const values = extractValues(newData) const validation = validateStructureRules(values) if (!validation.valid) { showTooltip(validation.message) setSortableItems((prev) => buildSortableItems(extractValues(prev))) return } const sanitized = sanitizeStructureList(values, SANITIZE_OPTIONS) setSortableItems(buildSortableItems(sanitized)) }} /> Éléments à ajouter { const count = segmentCounts[item.type] || 0 const selected = count > 0 const wrapperStyle = index === 0 ? null : styles.optionWrapper return ( handleOptionalSegmentPress(item.type)} /> ) }} keyExtractor={(item) => item.type} contentContainerStyle={{ paddingBottom: 24 }} scrollEnabled={false} nestedScrollEnabled={false} /> ) } export default CustomizeSongStructure const styles = StyleSheet.create({ rowContainer: { borderRadius: 14, width: '100%', }, blurSection: { marginTop: 12, width: '100%', }, rowBlur: { minHeight: 54, paddingHorizontal: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, selectedBlur: { backgroundColor: Palette.transparentWhite, }, rowContent: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, rowText: { fontSize: 16, color: Palette.white, fontFamily: FONT_FAMILY.InterRegular, }, handleIcon: { width: 22, height: 22, tintColor: Palette.white, marginRight: 12, }, countBadge: { minWidth: 28, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 999, backgroundColor: 'rgba(255,255,255,0.08)', alignItems: 'center', justifyContent: 'center', }, countBadgeActive: { backgroundColor: Palette.transparentWhite, }, countBadgeText: { fontSize: 14, color: Palette.white, fontFamily: FONT_FAMILY.InterMedium, }, rowActions: { flexDirection: 'row', alignItems: 'center', }, removeButton: { padding: 6, borderRadius: 999, backgroundColor: 'rgba(255,255,255,0.08)', }, removeIcon: { width: 16, height: 16, tintColor: Palette.white, }, optionWrapper: { marginTop: 12, }, sectionTag: { alignSelf: 'flex-start', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999, backgroundColor: 'rgba(255,255,255,0.12)', marginBottom: 16, }, sectionTagText: { fontSize: 12, color: Palette.white, fontFamily: FONT_FAMILY.InterSemiBold, letterSpacing: 1, textTransform: 'uppercase', }, })