import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useIsFocused, useRoute } from '@react-navigation/native'
import { FlatList, Modal, StyleSheet, Text, View, useWindowDimensions } from 'react-native'
import { background } from '../../assets'
import BackgroundVideo from '../../components/BackgroundVideo'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import AppAlert from '../../components/Alert'
import CreditAmount from '../../components/CreditAmount'
import firebase, { getFunctionsClient } from '../../config/firebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { normalizeStructureType } from '../../utils/songStructure'
import { openCoinPackModal } from '../../utils/coinPackModal'
import ChooseGenre from './ChooseGenre'
import ChooseInstruments from './ChooseInstruments'
import ChooseRhythm from './ChooseRhythm'
import CustomizeVoice from './CustomizeVoice'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const MUSIC_GENERATION_COIN_COST = 8
const CONFIRM_MODAL_MAX_WIDTH = 540
const FUNCTIONS_REGION = 'europe-west1'
const VOICE_SECTION_TITLES = ['BASE', 'SENSIBILITÉ', 'TECHNIQUE']
const FIRST_VOICE_STEP_INDEX = 1
const OPTIONAL_VOICE_CATEGORIES = new Set(['SENSIBILITÉ', 'TECHNIQUE'])
const PAGE_WIDTH_RATIO_WEB = 0.6 // keep in sync with Page default width on web
const PAGE_MAX_WIDTH = 1200
const PAGE_HORIZONTAL_PADDING = gutters * 2
const ComposeSong = () => {
const isFocused = useIsFocused()
const scrollRef = useRef(null)
const { width: windowWidth } = useWindowDimensions()
const [selectedIndex, setSelectedIndex] = useState(0)
const [progress, setProgress] = useState(18)
const [parentLayout, setParentLayout] = useState(null)
const estimatedContainerWidth = useMemo(() => {
const safeWindowWidth =
typeof windowWidth === 'number' && Number.isFinite(windowWidth) ? windowWidth : 0
const estimatedPageWidth = Math.min(safeWindowWidth * PAGE_WIDTH_RATIO_WEB, PAGE_MAX_WIDTH)
const paddedWidth = estimatedPageWidth - PAGE_HORIZONTAL_PADDING
return Math.max(1, paddedWidth)
}, [windowWidth])
const containerWidth = parentLayout?.width || estimatedContainerWidth
const route = useRoute()
const {
selectedProjectId,
selectedProject,
updateProjectData,
currentUserData,
currentUID,
videos,
} = useUser()
const [genres, setGenres] = useState([])
const [voice, setVoice] = useState({})
const [instruments, setInstruments] = useState([])
const [rhythm, setRhythm] = useState(null)
const [isConfirmVisible, setIsConfirmVisible] = useState(false)
const [isProcessingConfirmation, setIsProcessingConfirmation] = useState(false)
const isRegenerationFlow = route?.params?.isRegeneration === true
const fromLyrics = route?.params?.fromLyrics === true
const coinBalance = useMemo(() => {
const value = currentUserData?.coins
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string') {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return parsed
}
}
return 0
}, [currentUserData?.coins])
const steps = useMemo(
() => [
{
key: 'genres',
render: () => ,
},
...VOICE_SECTION_TITLES.map((section) => ({
key: `voice-${section}`,
render: () => ,
})),
{
key: 'instruments',
render: () => ,
},
{
key: 'rhythm',
render: () => ,
},
],
[genres, voice, instruments, rhythm]
)
const totalSteps = steps.length
const lastStepIndex = totalSteps - 1
const instrumentStepIndex = Math.max(lastStepIndex - 1, 0)
const isStepValid = useMemo(() => {
if (selectedIndex === 0) {
return Array.isArray(genres) && genres.length > 0
}
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX
const isVoiceStep = voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length
if (isVoiceStep) {
const category = VOICE_SECTION_TITLES[voiceStepIndex]
if (OPTIONAL_VOICE_CATEGORIES.has(category)) {
return true
}
return !!(voice && typeof voice === 'object' && voice[category])
}
if (selectedIndex === instrumentStepIndex) {
return Array.isArray(instruments) && instruments.length > 0
}
if (selectedIndex === lastStepIndex) {
return !!rhythm
}
return true
}, [selectedIndex, genres, voice, instruments, rhythm, instrumentStepIndex, lastStepIndex])
const musicConfig = useMemo(() => {
let lyricsArr = []
if (Array.isArray(selectedProject?.lyrics)) {
lyricsArr = selectedProject.lyrics.map((s) => ({
type: normalizeStructureType(s?.type),
lyrics: s?.lyrics || '',
}))
} else {
const c = selectedProject?.lyrics?.couplet
const r = selectedProject?.lyrics?.refrain
if (c) lyricsArr.push({ type: 'couplet', lyrics: c })
if (r) lyricsArr.push({ type: 'refrain', lyrics: r })
}
const voiceArray = Object.entries(voice || {})
.filter(([, v]) => typeof v === 'string' && v.trim())
.map(([category, value]) => ({ category, value }))
return {
title: selectedProject?.title || '',
lyrics: lyricsArr,
genres: Array.isArray(genres) ? genres : [],
voice: voiceArray,
instruments: Array.isArray(instruments) ? instruments : [],
tempo: rhythm || undefined,
projectId: selectedProjectId || undefined,
}
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId])
useEffect(() => {
const nextProgress = 18 + selectedIndex * 9
if (nextProgress !== progress) {
setProgress(nextProgress)
}
try {
scrollRef.current?.scrollToIndex?.({
index: selectedIndex,
animated: true,
})
} catch (_) {}
}, [selectedIndex, progress, containerWidth])
const getItemLayout = useCallback(
(_data, index) => ({
length: containerWidth,
offset: containerWidth * index,
index,
}),
[containerWidth]
)
const handleCancelGeneration = () => {
setIsConfirmVisible(false)
navigate(Routes.SongReady)
}
const persistMusicConfig = useCallback(async () => {
try {
if (!selectedProjectId) return
const payload = {
musicConfig: {
title: musicConfig?.title || '',
lyrics: Array.isArray(musicConfig?.lyrics) ? musicConfig.lyrics : [],
genres: Array.isArray(musicConfig?.genres) ? musicConfig.genres : [],
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
instruments: Array.isArray(musicConfig?.instruments) ? musicConfig.instruments : [],
tempo: musicConfig?.tempo || '',
},
musicStatus: null,
sunoTaskId: firebase.firestore.FieldValue.delete(),
musicCreditsRefunded: firebase.firestore.FieldValue.delete(),
musicCreditsRefundOrderId: firebase.firestore.FieldValue.delete(),
}
if (!isRegenerationFlow) {
payload.musicUrls = firebase.firestore.FieldValue.delete()
payload.musicAudioIds = firebase.firestore.FieldValue.delete()
payload.musicDurations = firebase.firestore.FieldValue.delete()
payload.musicTaskIds = firebase.firestore.FieldValue.delete()
payload.musicTimestamps = firebase.firestore.FieldValue.delete()
payload.lyricsSyncStatus = firebase.firestore.FieldValue.delete()
payload.lyricsSyncError = firebase.firestore.FieldValue.delete()
payload.lyricsSyncDiagnostics = firebase.firestore.FieldValue.delete()
}
await updateProjectData(payload)
} catch (_error) {}
}, [isRegenerationFlow, musicConfig, selectedProjectId, updateProjectData])
const spendCoinsForGeneration = useCallback(async () => {
if (!currentUID) {
throw new Error('Utilisateur introuvable. Merci de réessayer.')
}
try {
const functionsClient = getFunctionsClient(FUNCTIONS_REGION)
const createSongOrder = functionsClient.httpsCallable('orders-createSongOrder')
await createSongOrder({
amount: -MUSIC_GENERATION_COIN_COST,
songId: selectedProjectId || null,
source: 'music_generation',
})
} catch (error) {
const message =
typeof error?.message === 'string'
? error.message.replace(/^functions\.https\.HttpsError:\s*/iu, '')
: null
throw new Error(
message || 'Une erreur est survenue lors de la création de la commande de crédits.'
)
}
}, [currentUID, selectedProjectId])
const handleConfirmGeneration = useCallback(async () => {
if (isProcessingConfirmation) return
setIsProcessingConfirmation(true)
try {
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0
if (availableCoins < MUSIC_GENERATION_COIN_COST) {
setIsConfirmVisible(false)
AppAlert(
'Crédits insuffisants',
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer."
)
openCoinPackModal()
return
}
await spendCoinsForGeneration()
await persistMusicConfig()
setIsConfirmVisible(false)
navigate(Routes.GeneratingSong, { config: musicConfig })
} catch (error) {
setIsConfirmVisible(false)
const message =
error?.message || 'Une erreur est survenue lors du lancement de la génération.'
AppAlert('Impossible de lancer la génération', message)
} finally {
setIsProcessingConfirmation(false)
}
}, [
coinBalance,
isProcessingConfirmation,
musicConfig,
persistMusicConfig,
spendCoinsForGeneration,
])
const onPressNext = () => {
if (selectedIndex === steps.length - 1) {
setIsConfirmVisible(true)
return
}
setSelectedIndex((prev) => Math.min(prev + 1, steps.length - 1))
}
const onPressBack = () => {
if (selectedIndex > 0) {
setSelectedIndex((prev) => Math.max(prev - 1, 0))
} else {
if (fromLyrics) {
navigate(Routes.BottomTab, {
screen: Routes.HomeStack,
params: {
screen: Routes.Home,
},
})
return
}
goBack()
}
}
return (
) : null
}
coinBadgeContainerStyle={styles.rightSideCredits}
headerType="NONE"
>
setParentLayout(event.nativeEvent.layout)}>
item.key}
horizontal
pagingEnabled
scrollEnabled={false}
showsHorizontalScrollIndicator={false}
initialScrollIndex={selectedIndex}
getItemLayout={getItemLayout}
style={{ width: containerWidth }}
renderItem={({ item }) => (
{item.render()}
)}
/>
{selectedIndex !== steps.length && (
{selectedIndex === steps.length - 1 && isRegenerationFlow && (
)}
)}
{
if (isProcessingConfirmation) return
setIsConfirmVisible(false)
}}
>
Générer la musique ?
Cette action coûte
Souhaites-tu les utiliser pour lancer la génération ?
Solde disponible :
{
if (isProcessingConfirmation) return
setIsConfirmVisible(false)
}}
disabled={isProcessingConfirmation}
/>
)
}
export default ComposeSong
const styles = StyleSheet.create({
rightSideControl: {
right: 24,
left: 'auto',
},
rightSideCredits: {
right: 24,
left: 'auto',
alignItems: 'flex-end',
},
})