import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/divination_layout.dart'; import '../models/syncretic_session_model.dart'; import '../models/tradition_model.dart'; import '../models/word_model.dart'; import '../services/traditions_repository.dart'; import '../services/words_repository.dart'; import '../widgets/divination_instruction_modal.dart'; import 'lenormand_station_screen.dart'; class LenormandDivinationScreen extends StatefulWidget { final String? initialQuestion; const LenormandDivinationScreen({super.key, this.initialQuestion}); @override State createState() => _LenormandDivinationScreenState(); } class _LenormandDivinationScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final _questionController = TextEditingController(); final _directCardNumberController = TextEditingController(); final _repo = WordsRepository(); List _cards = []; // Layouts: 2 cards (pair), 3 cards (line), 5 cards (cross) DivinationLayout _selectedLayout = DivinationLayouts.lenormandLayouts[0]; int _currentStepIndex = 0; // Dice simulation (d6 + d12) int? _d6Value; int? _d12Value; bool _isRollingDice = false; // Drawn results List<({TraditionStation card, int cardNumber, String positionTitle})> _drawnResults = []; bool _savedToSession = false; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); if (widget.initialQuestion != null) { _questionController.text = widget.initialQuestion!; } _loadCards(); } void _loadCards() { final canon = TraditionsRepository().byId('lenormand'); if (canon != null) { _cards = canon.stations; } } @override void dispose() { _tabController.dispose(); _questionController.dispose(); _directCardNumberController.dispose(); super.dispose(); } int _calculateCardNumber(int d6, int d12) { int base = 0; if (d6 == 3 || d6 == 4) base = 12; if (d6 == 5 || d6 == 6) base = 24; return (base + d12).clamp(1, 36); } void _onLayoutChanged(DivinationLayout layout) { setState(() { _selectedLayout = layout; _currentStepIndex = 0; _drawnResults.clear(); _d6Value = null; _d12Value = null; _savedToSession = false; }); } Future _rollDiceForStep() async { if (_cards.isEmpty || _currentStepIndex >= _selectedLayout.count) return; setState(() => _isRollingDice = true); HapticFeedback.mediumImpact(); final rnd = Random(); for (int i = 0; i < 8; i++) { await Future.delayed(const Duration(milliseconds: 50)); if (mounted) { setState(() { _d6Value = rnd.nextInt(6) + 1; _d12Value = rnd.nextInt(12) + 1; }); } } int finalD6 = rnd.nextInt(6) + 1; int finalD12 = rnd.nextInt(12) + 1; int cardNum = _calculateCardNumber(finalD6, finalD12); // If card already drawn in this spread, pick next available card (no duplicates in a single layout) final alreadyDrawnNums = _drawnResults.map((r) => r.cardNumber).toSet(); if (alreadyDrawnNums.contains(cardNum)) { for (int i = 1; i <= 36; i++) { final candidate = (cardNum + i - 1) % 36 + 1; if (!alreadyDrawnNums.contains(candidate)) { cardNum = candidate; break; } } } final card = (cardNum - 1 < _cards.length) ? _cards[cardNum - 1] : null; if (card != null && mounted) { final posTitle = _selectedLayout.positionTitles[_currentStepIndex]; setState(() { _d6Value = finalD6; _d12Value = finalD12; _isRollingDice = false; _drawnResults.add((card: card, cardNumber: cardNum, positionTitle: posTitle)); _currentStepIndex++; if (_currentStepIndex >= _selectedLayout.count) { _saveToSession('dice'); } }); HapticFeedback.heavyImpact(); } } void _applyManualCardNumber(int cardNum) { if (cardNum < 1 || cardNum > 36 || _cards.isEmpty || _currentStepIndex >= _selectedLayout.count) return; final card = (cardNum - 1 < _cards.length) ? _cards[cardNum - 1] : null; if (card == null) return; final posTitle = _selectedLayout.positionTitles[_currentStepIndex]; setState(() { _drawnResults.add((card: card, cardNumber: cardNum, positionTitle: posTitle)); _currentStepIndex++; _directCardNumberController.clear(); if (_currentStepIndex >= _selectedLayout.count) { _saveToSession('manual'); } }); HapticFeedback.selectionClick(); } void _resetSpread() { setState(() { _currentStepIndex = 0; _drawnResults.clear(); _d6Value = null; _d12Value = null; _savedToSession = false; }); } Future _saveToSession(String inputMethod) async { if (_drawnResults.isEmpty) return; final items = _drawnResults.map((r) { WordItem? linkedWord; if (r.card.primary != null) { linkedWord = _repo.getWordById(r.card.primary!.id) ?? _repo.getWordByName(r.card.primary!.word); } return ReadingSignItem( title: '${r.positionTitle}: #${r.cardNumber} «${r.card.name}»', subtitle: r.card.group, symbol: '#${r.cardNumber}', lore: r.card.lore, linkedWord: linkedWord, ); }).toList(); final reading = SyncreticReading( id: 'reading_${DateTime.now().millisecondsSinceEpoch}', systemId: 'lenormand', systemName: 'Малый Оракул Ленорман', systemIcon: '🪶', timestamp: DateTime.now(), layoutTitle: '${_selectedLayout.title} (${inputMethod == "dice" ? "d6+d12" : "со стола"})', inputMethod: inputMethod, items: items, question: _questionController.text.trim(), ); await _repo.addReadingToSyncreticSession(reading, question: _questionController.text.trim()); if (mounted) { setState(() => _savedToSession = true); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Расклад Ленорман «${_selectedLayout.title}» сохранён в сессию дня!'), backgroundColor: const Color(0xFF2E7D32), behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 2), ), ); } } Future _autoDealFullTableau() async { if (_cards.length < 36) return; setState(() => _isRollingDice = true); HapticFeedback.mediumImpact(); final rnd = Random(); final shuffledIndices = List.generate(36, (i) => i)..shuffle(rnd); _drawnResults.clear(); for (int i = 0; i < 36; i++) { final cardIdx = shuffledIndices[i]; final card = _cards[cardIdx]; final cardNum = cardIdx + 1; final posTitle = _selectedLayout.positionTitles[i]; _drawnResults.add((card: card, cardNumber: cardNum, positionTitle: posTitle)); } if (mounted) { setState(() { _isRollingDice = false; _currentStepIndex = 36; }); HapticFeedback.heavyImpact(); _saveToSession('dice'); } } @override Widget build(BuildContext context) { final isSpreadComplete = _currentStepIndex >= _selectedLayout.count; return Scaffold( backgroundColor: const Color(0xFF0C0E1A), appBar: AppBar( actions: [ IconButton( tooltip: 'Инструкция по Ленорман', icon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F)), onPressed: () => DivinationInstructionModal.show(context, 'lenormand'), ), if (_drawnResults.isNotEmpty) IconButton( tooltip: 'Сбросить расклад', icon: const Icon(Icons.refresh_rounded), onPressed: _resetSpread, ), ], title: const Row( children: [ Text('🪶', style: TextStyle(fontSize: 22)), SizedBox(width: 8), Text('Ленорман (d6 + d12)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17)), ], ), bottom: TabBar( controller: _tabController, indicatorColor: const Color(0xFFFFD54F), tabs: const [ Tab(icon: Icon(Icons.casino_rounded, size: 18), text: 'Жребий d6+d12'), Tab(icon: Icon(Icons.touch_app_rounded, size: 18), text: 'Ввод 1..36 со стола'), ], ), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: _questionController, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( hintText: 'Вопрос к оракулу Ленорман...', hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 13), prefixIcon: const Icon(Icons.help_outline_rounded, color: Color(0xFFFFD54F), size: 18), filled: true, fillColor: const Color(0xFF151829), border: OutlineInputBorder(borderRadius: BorderRadius.circular(14)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(14), borderSide: const BorderSide(color: Color(0xFF2A2D4A)), ), ), ), const SizedBox(height: 14), // Layout Picker Row Row( children: [ const Icon(Icons.dashboard_customize_rounded, size: 16, color: Color(0xFFFFD54F)), const SizedBox(width: 6), const Text('РАСКЛАД:', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.bold)), const SizedBox(width: 8), Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: DivinationLayouts.lenormandLayouts.map((l) { final isSel = _selectedLayout.id == l.id; return Padding( padding: const EdgeInsets.only(right: 6), child: ChoiceChip( label: Text(l.title, style: TextStyle(fontSize: 11.5, fontWeight: isSel ? FontWeight.bold : FontWeight.normal)), selected: isSel, selectedColor: const Color(0xFF4A3E1E), side: BorderSide(color: isSel ? const Color(0xFFFFD54F) : const Color(0xFF2A2D4A)), onSelected: (_) => _onLayoutChanged(l), ), ); }).toList(), ), ), ), ], ), const SizedBox(height: 14), // Progress Banner Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: const Color(0xFF181C30), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Row( children: [ Text( 'Позиция: ${_currentStepIndex + 1} из ${_selectedLayout.count}', style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 12, fontWeight: FontWeight.bold), ), const SizedBox(width: 8), if (!isSpreadComplete) Expanded( child: Text( '«${_selectedLayout.positionTitles[_currentStepIndex]}»', style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 12, fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), ) else const Text('✓ Расклад собран', style: TextStyle(color: Color(0xFF81C784), fontSize: 12, fontWeight: FontWeight.bold)), ], ), ), const SizedBox(height: 14), if (!isSpreadComplete) SizedBox( height: 290, child: TabBarView( controller: _tabController, children: [ _buildDiceTab(), _buildManualTab(), ], ), ), if (isSpreadComplete) ...[ Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF1E2238), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF81C784)), ), child: Row( children: [ const Icon(Icons.check_circle_rounded, color: Color(0xFF81C784), size: 20), const SizedBox(width: 10), const Expanded( child: Text( 'Все карты расклада извлечены и записаны в сессию дня!', style: TextStyle(color: Color(0xFFECEFF1), fontSize: 12.5, fontWeight: FontWeight.bold), ), ), TextButton( onPressed: _resetSpread, child: const Text('Заново', style: TextStyle(color: Color(0xFFFFD54F), fontWeight: FontWeight.bold)), ), ], ), ), ], const SizedBox(height: 16), if (_drawnResults.isNotEmpty) ...[ const Text( 'ВЫПАВШИЕ КАРТЫ В РАСКЛАДЕ:', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11.5, fontWeight: FontWeight.w900, letterSpacing: 0.8), ), const SizedBox(height: 10), for (int i = 0; i < _drawnResults.length; i++) ...[ _buildResultCardItem(_drawnResults[i], i + 1), const SizedBox(height: 10), ], ], ], ), ), ); } Widget _buildDiceTab() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'БРОСОК d6 + d12 (1..36 РАВНОМЕРНО)', style: TextStyle(color: Color(0xFFFFD54F), fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 0.8), ), const SizedBox(height: 6), Text( 'Извлечение позиции: «' + _selectedLayout.positionTitles[_currentStepIndex] + '»', textAlign: TextAlign.center, style: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11.5), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildDieBox('d6 (блок)', _d6Value, isD6: true), const SizedBox(width: 20), _buildDieBox('d12 (номер)', _d12Value), ], ), const SizedBox(height: 18), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFFD54F), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), onPressed: _isRollingDice ? null : _rollDiceForStep, icon: const Icon(Icons.casino_rounded, size: 20), label: Text( _isRollingDice ? 'Кости летят...' : 'Бросить d6+d12 (шаг ${_currentStepIndex + 1})', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13.5), ), ), if (_selectedLayout.count >= 9) ...[ const SizedBox(height: 10), OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF80DEEA), side: const BorderSide(color: Color(0xFF80DEEA)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: _isRollingDice ? null : _autoDealFullTableau, icon: const Icon(Icons.auto_awesome_rounded, size: 16), label: Text( _selectedLayout.count == 36 ? 'Случайный расклад всех 36 Домов' : 'Разложить весь квадрат 3×3', style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold), ), ), ], ], ), ); } Widget _buildDieBox(String label, int? val, {bool isD6 = false}) { String sub = ''; if (val != null && isD6) { if (val <= 2) sub = 'База +0 (1..12)'; else if (val <= 4) sub = 'База +12 (13..24)'; else sub = 'База +24 (25..36)'; } return Column( children: [ Container( width: 68, height: 68, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF232742), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.6), width: 1.5), ), child: Text( val != null ? '$val' : '?', style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 26, fontWeight: FontWeight.w900), ), ), const SizedBox(height: 6), Text(label, style: const TextStyle(color: Color(0xFF90A4AE), fontSize: 10, fontWeight: FontWeight.bold)), if (sub.isNotEmpty) Text(sub, style: const TextStyle(color: Color(0xFFFFD54F), fontSize: 9, fontWeight: FontWeight.bold)), ], ); } Widget _buildManualTab() { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF151829), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Column( children: [ Row( children: [ Expanded( child: TextField( controller: _directCardNumberController, keyboardType: TextInputType.number, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( hintText: 'Номер карты для шага ${_currentStepIndex + 1} (1..36)...', hintStyle: const TextStyle(color: Color(0xFF8C93B8), fontSize: 11), isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), filled: true, fillColor: const Color(0xFF1E2238), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), ), onSubmitted: (val) { final n = int.tryParse(val.trim()); if (n != null) _applyManualCardNumber(n); }, ), ), const SizedBox(width: 8), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFFD54F), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), onPressed: () { final n = int.tryParse(_directCardNumberController.text.trim()); if (n != null) _applyManualCardNumber(n); }, child: const Text('Ввод', style: TextStyle(fontWeight: FontWeight.bold)), ), ], ), const SizedBox(height: 8), Expanded( child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 6, crossAxisSpacing: 6, mainAxisSpacing: 6, childAspectRatio: 1.0, ), itemCount: _cards.length, itemBuilder: (ctx, i) { return InkWell( onTap: () => _applyManualCardNumber(i + 1), borderRadius: BorderRadius.circular(8), child: Container( alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF1E2238), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF2A2D4A)), ), child: Text( '${i + 1}', style: const TextStyle(color: Color(0xFFE0E2EC), fontSize: 14, fontWeight: FontWeight.bold), ), ), ); }, ), ), ], ), ); } Widget _buildResultCardItem(({TraditionStation card, int cardNumber, String positionTitle}) res, int index) { final c = res.card; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF181C30), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFD54F).withValues(alpha: 0.4)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 44, height: 44, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFF232742), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFD54F)), ), child: Text( '#${res.cardNumber}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFFFFD54F)), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( '#$index. ${res.positionTitle}: ${c.name}', style: const TextStyle(color: Color(0xFFFFF9C4), fontSize: 14, fontWeight: FontWeight.bold), ), ), IconButton( icon: const Icon(Icons.auto_stories_rounded, size: 18, color: Color(0xFFFFD54F)), padding: EdgeInsets.zero, constraints: const BoxConstraints(), tooltip: 'Сочетания карты', onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (_) => LenormandStationScreen(number: res.cardNumber), ), ); }, ), ], ), const SizedBox(height: 4), Text(c.group, style: const TextStyle(color: Color(0xFF80DEEA), fontSize: 11)), const SizedBox(height: 6), Text( c.lore, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Color(0xFFD1D5DB), fontSize: 12, height: 1.4), ), ], ), ), ], ), ); } }