import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:logic_circuits_simulator/models.dart'; import 'package:logic_circuits_simulator/utils/simulation.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:tuple/tuple.dart'; class ComponentState extends ChangeNotifier { ProjectEntry? _currentProject; ComponentEntry? _currentComponent; Wiring _wiring = const Wiring(instances: [], wires: []); Wiring? _wiringDraft; Design _design = const Design(components: [], wires: [], inputs: [], outputs: []); Design? _designDraft; SimulatedComponent? _simulatedComponent; PartialVisualSimulation? _partialVisualSimulation; final Map> _dependenciesMap = {}; ProjectEntry? get currentProject => _currentProject; ComponentEntry? get currentComponent => _currentComponent; Wiring get wiring => _wiring; Wiring get wiringDraft => _wiringDraft ?? _wiring; Design get design => _design; Design get designDraft => _designDraft ?? _design; PartialVisualSimulation? get partialVisualSimulation => _partialVisualSimulation; Future _onRequiredDependency(String depId) async { final t = _dependenciesMap[depId]!; final proj = t.item1; final comp = t.item2; final state = comp.visualDesigned ? ComponentState() : null; if (state != null) { await state.setCurrentComponent( project: proj, component: comp, onDependencyNeeded: (projId, compId) async => _dependenciesMap['${projId == "self" ? proj.projectId : projId}/$compId'], ); } return SimulatedComponent( project: proj, component: comp, onRequiredDependency: _onRequiredDependency, state: state, ); } Future _getComponentDir() async { if (_currentProject == null) { throw Exception('Cannot get component directory without knowing project'); } if (_currentComponent == null) { throw Exception('Cannot get component directory of null'); } final appDir = await getApplicationDocumentsDirectory(); final result = Directory(path.join(appDir.path, 'LogicCircuitsSimulator', 'projects', _currentProject!.projectId, 'components', _currentComponent!.componentId)); if (!await result.exists()) { await result.create(recursive: true); } return result; } Future _getWiringFile() async { final result = File(path.join((await _getComponentDir()).path, 'wiring.json')); return result; } Future _getDesignFile() async { final result = File(path.join((await _getComponentDir()).path, 'design.json')); return result; } Future _loadComponentFiles() async { final wiringFile = await _getWiringFile(); if (!await wiringFile.exists()) { _wiring = const Wiring(instances: [], wires: []); await wiringFile.writeAsString(jsonEncode(_wiring)); } else { _wiring = Wiring.fromJson(jsonDecode(await wiringFile.readAsString())); } _wiringDraft = null; final designFile = await _getDesignFile(); if (!await designFile.exists()) { _design = const Design(components: [], wires: [], inputs: [], outputs: []); await designFile.writeAsString(jsonEncode(_design)); } else { _design = Design.fromJson(jsonDecode(await designFile.readAsString())); } _designDraft = null; } Future setCurrentComponent({ required ProjectEntry project, required ComponentEntry component, required Future?> Function(String projectId, String componentId) onDependencyNeeded, }) async { _dependenciesMap.clear(); _simulatedComponent = null; _currentProject = project; _currentComponent = component; // Load dependencies final unsatisfiedDependencies = []; final neededDependencies = component.dependencies.toList(); while (neededDependencies.isNotEmpty) { final tmp = neededDependencies.toList(); neededDependencies.clear(); for (final depId in tmp) { if (!hasDependency(depId)) { final splitted = depId.split('/'); final maybeDep = await onDependencyNeeded(splitted[0], splitted[1]); if (maybeDep == null) { unsatisfiedDependencies.add(depId); } else { addDependency(depId, maybeDep); neededDependencies.addAll( maybeDep.item2.dependencies .map((depId) { final splitted = depId.split('/'); final projectId = splitted[0] == 'self' ? maybeDep.item1.projectId : splitted[0]; return '$projectId/${splitted[1]}'; }) .where((depId) => !hasDependency(depId)) ); } } } } if (unsatisfiedDependencies.isNotEmpty) { throw DependenciesNotSatisfiedException(dependencies: unsatisfiedDependencies); } await _loadComponentFiles(); await recreatePartialSimulation(); notifyListeners(); } void addDependency(String depId, Tuple2 dependency, {bool modifyCurrentComponent = false}) { _dependenciesMap[depId] = dependency; if (modifyCurrentComponent && _currentComponent?.dependencies.contains(depId) == false) { _currentComponent = _currentComponent?.copyWith( dependencies: (_currentComponent?.dependencies ?? []) + [depId], ); } } void removeDependency(String depId, {bool modifyCurrentComponent = false}) { _dependenciesMap.remove(depId); if (modifyCurrentComponent && _currentComponent?.dependencies.contains(depId) == true) { _currentComponent = _currentComponent?.copyWith( dependencies: _currentComponent?.dependencies.where((dep) => dep != depId).toList() ?? [], ); } } Future recreatePartialSimulation() async { if (_currentComponent!.visualDesigned) { _partialVisualSimulation = await PartialVisualSimulation.init( project: _currentProject!, component: _currentComponent!, state: this, onRequiredDependency: _onRequiredDependency, ); } notifyListeners(); } bool hasDependency(String depId) => _dependenciesMap.containsKey(depId); void noComponent() { _dependenciesMap.clear(); _currentProject = null; _currentComponent = null; _wiring = const Wiring(instances: [], wires: []); _design = const Design(components: [], wires: [], inputs: [], outputs: []); _wiringDraft = _designDraft = null; _simulatedComponent = null; _partialVisualSimulation = null; notifyListeners(); } Tuple2 getMetaByInstance(String instanceId) { for (final instance in wiring.instances) { if (instance.instanceId == instanceId) { return _dependenciesMap[instance.componentId]!; } } throw Exception('Instance $instanceId not found in the dependencies map'); } Future> simulate(Map inputs) async { _simulatedComponent ??= SimulatedComponent( project: _currentProject!, component: _currentComponent!, onRequiredDependency: _onRequiredDependency, state: this, ); return _simulatedComponent!.simulate(inputs); } Future updateDesign(Design newDesign, {bool commit = true}) async { if (commit) { _design = newDesign; _designDraft = null; final designFile = await _getDesignFile(); await designFile.writeAsString(jsonEncode(newDesign)); } else { _designDraft = newDesign; } notifyListeners(); return designDraft; } Future updateWiring(Wiring newWiring, {bool commit = true}) async { if (commit) { _wiring = newWiring; _wiringDraft = null; final wiringFile = await _getWiringFile(); await wiringFile.writeAsString(jsonEncode(newWiring)); } else { _wiringDraft = newWiring; } notifyListeners(); return wiringDraft; } } class DependenciesNotSatisfiedException with Exception { final List dependencies; const DependenciesNotSatisfiedException({required this.dependencies}); @override String toString() { return 'DependenciesNotSatisfiedException(${dependencies.join(", ")})'; } }