#
tokens: 48742/50000 3/263 files (page 10/13)
lines: off (toggle) GitHub
raw markdown copy
This is page 10 of 13. Use http://codebase.md/justinpbarnett/unity-mcp?page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── prompts
│   │   ├── nl-unity-suite-nl.md
│   │   └── nl-unity-suite-t.md
│   └── settings.json
├── .github
│   ├── scripts
│   │   └── mark_skipped.py
│   └── workflows
│       ├── bump-version.yml
│       ├── claude-nl-suite.yml
│       ├── github-repo-stats.yml
│       └── unity-tests.yml
├── .gitignore
├── deploy-dev.bat
├── docs
│   ├── CURSOR_HELP.md
│   ├── CUSTOM_TOOLS.md
│   ├── README-DEV-zh.md
│   ├── README-DEV.md
│   ├── screenshots
│   │   ├── v5_01_uninstall.png
│   │   ├── v5_02_install.png
│   │   ├── v5_03_open_mcp_window.png
│   │   ├── v5_04_rebuild_mcp_server.png
│   │   ├── v5_05_rebuild_success.png
│   │   ├── v6_2_create_python_tools_asset.png
│   │   ├── v6_2_python_tools_asset.png
│   │   ├── v6_new_ui_asset_store_version.png
│   │   ├── v6_new_ui_dark.png
│   │   └── v6_new_ui_light.png
│   ├── TELEMETRY.md
│   ├── v5_MIGRATION.md
│   └── v6_NEW_UI_CHANGES.md
├── LICENSE
├── logo.png
├── mcp_source.py
├── MCPForUnity
│   ├── Editor
│   │   ├── AssemblyInfo.cs
│   │   ├── AssemblyInfo.cs.meta
│   │   ├── Data
│   │   │   ├── DefaultServerConfig.cs
│   │   │   ├── DefaultServerConfig.cs.meta
│   │   │   ├── McpClients.cs
│   │   │   ├── McpClients.cs.meta
│   │   │   ├── PythonToolsAsset.cs
│   │   │   └── PythonToolsAsset.cs.meta
│   │   ├── Data.meta
│   │   ├── Dependencies
│   │   │   ├── DependencyManager.cs
│   │   │   ├── DependencyManager.cs.meta
│   │   │   ├── Models
│   │   │   │   ├── DependencyCheckResult.cs
│   │   │   │   ├── DependencyCheckResult.cs.meta
│   │   │   │   ├── DependencyStatus.cs
│   │   │   │   └── DependencyStatus.cs.meta
│   │   │   ├── Models.meta
│   │   │   ├── PlatformDetectors
│   │   │   │   ├── IPlatformDetector.cs
│   │   │   │   ├── IPlatformDetector.cs.meta
│   │   │   │   ├── LinuxPlatformDetector.cs
│   │   │   │   ├── LinuxPlatformDetector.cs.meta
│   │   │   │   ├── MacOSPlatformDetector.cs
│   │   │   │   ├── MacOSPlatformDetector.cs.meta
│   │   │   │   ├── PlatformDetectorBase.cs
│   │   │   │   ├── PlatformDetectorBase.cs.meta
│   │   │   │   ├── WindowsPlatformDetector.cs
│   │   │   │   └── WindowsPlatformDetector.cs.meta
│   │   │   └── PlatformDetectors.meta
│   │   ├── Dependencies.meta
│   │   ├── External
│   │   │   ├── Tommy.cs
│   │   │   └── Tommy.cs.meta
│   │   ├── External.meta
│   │   ├── Helpers
│   │   │   ├── AssetPathUtility.cs
│   │   │   ├── AssetPathUtility.cs.meta
│   │   │   ├── CodexConfigHelper.cs
│   │   │   ├── CodexConfigHelper.cs.meta
│   │   │   ├── ConfigJsonBuilder.cs
│   │   │   ├── ConfigJsonBuilder.cs.meta
│   │   │   ├── ExecPath.cs
│   │   │   ├── ExecPath.cs.meta
│   │   │   ├── GameObjectSerializer.cs
│   │   │   ├── GameObjectSerializer.cs.meta
│   │   │   ├── McpConfigFileHelper.cs
│   │   │   ├── McpConfigFileHelper.cs.meta
│   │   │   ├── McpConfigurationHelper.cs
│   │   │   ├── McpConfigurationHelper.cs.meta
│   │   │   ├── McpLog.cs
│   │   │   ├── McpLog.cs.meta
│   │   │   ├── McpPathResolver.cs
│   │   │   ├── McpPathResolver.cs.meta
│   │   │   ├── PackageDetector.cs
│   │   │   ├── PackageDetector.cs.meta
│   │   │   ├── PackageInstaller.cs
│   │   │   ├── PackageInstaller.cs.meta
│   │   │   ├── PortManager.cs
│   │   │   ├── PortManager.cs.meta
│   │   │   ├── PythonToolSyncProcessor.cs
│   │   │   ├── PythonToolSyncProcessor.cs.meta
│   │   │   ├── Response.cs
│   │   │   ├── Response.cs.meta
│   │   │   ├── ServerInstaller.cs
│   │   │   ├── ServerInstaller.cs.meta
│   │   │   ├── ServerPathResolver.cs
│   │   │   ├── ServerPathResolver.cs.meta
│   │   │   ├── TelemetryHelper.cs
│   │   │   ├── TelemetryHelper.cs.meta
│   │   │   ├── Vector3Helper.cs
│   │   │   └── Vector3Helper.cs.meta
│   │   ├── Helpers.meta
│   │   ├── Importers
│   │   │   ├── PythonFileImporter.cs
│   │   │   └── PythonFileImporter.cs.meta
│   │   ├── Importers.meta
│   │   ├── MCPForUnity.Editor.asmdef
│   │   ├── MCPForUnity.Editor.asmdef.meta
│   │   ├── MCPForUnityBridge.cs
│   │   ├── MCPForUnityBridge.cs.meta
│   │   ├── Models
│   │   │   ├── Command.cs
│   │   │   ├── Command.cs.meta
│   │   │   ├── McpClient.cs
│   │   │   ├── McpClient.cs.meta
│   │   │   ├── McpConfig.cs
│   │   │   ├── McpConfig.cs.meta
│   │   │   ├── MCPConfigServer.cs
│   │   │   ├── MCPConfigServer.cs.meta
│   │   │   ├── MCPConfigServers.cs
│   │   │   ├── MCPConfigServers.cs.meta
│   │   │   ├── McpStatus.cs
│   │   │   ├── McpStatus.cs.meta
│   │   │   ├── McpTypes.cs
│   │   │   ├── McpTypes.cs.meta
│   │   │   ├── ServerConfig.cs
│   │   │   └── ServerConfig.cs.meta
│   │   ├── Models.meta
│   │   ├── Resources
│   │   │   ├── McpForUnityResourceAttribute.cs
│   │   │   ├── McpForUnityResourceAttribute.cs.meta
│   │   │   ├── MenuItems
│   │   │   │   ├── GetMenuItems.cs
│   │   │   │   └── GetMenuItems.cs.meta
│   │   │   ├── MenuItems.meta
│   │   │   ├── Tests
│   │   │   │   ├── GetTests.cs
│   │   │   │   └── GetTests.cs.meta
│   │   │   └── Tests.meta
│   │   ├── Resources.meta
│   │   ├── Services
│   │   │   ├── BridgeControlService.cs
│   │   │   ├── BridgeControlService.cs.meta
│   │   │   ├── ClientConfigurationService.cs
│   │   │   ├── ClientConfigurationService.cs.meta
│   │   │   ├── IBridgeControlService.cs
│   │   │   ├── IBridgeControlService.cs.meta
│   │   │   ├── IClientConfigurationService.cs
│   │   │   ├── IClientConfigurationService.cs.meta
│   │   │   ├── IPackageUpdateService.cs
│   │   │   ├── IPackageUpdateService.cs.meta
│   │   │   ├── IPathResolverService.cs
│   │   │   ├── IPathResolverService.cs.meta
│   │   │   ├── IPythonToolRegistryService.cs
│   │   │   ├── IPythonToolRegistryService.cs.meta
│   │   │   ├── ITestRunnerService.cs
│   │   │   ├── ITestRunnerService.cs.meta
│   │   │   ├── IToolSyncService.cs
│   │   │   ├── IToolSyncService.cs.meta
│   │   │   ├── MCPServiceLocator.cs
│   │   │   ├── MCPServiceLocator.cs.meta
│   │   │   ├── PackageUpdateService.cs
│   │   │   ├── PackageUpdateService.cs.meta
│   │   │   ├── PathResolverService.cs
│   │   │   ├── PathResolverService.cs.meta
│   │   │   ├── PythonToolRegistryService.cs
│   │   │   ├── PythonToolRegistryService.cs.meta
│   │   │   ├── TestRunnerService.cs
│   │   │   ├── TestRunnerService.cs.meta
│   │   │   ├── ToolSyncService.cs
│   │   │   └── ToolSyncService.cs.meta
│   │   ├── Services.meta
│   │   ├── Setup
│   │   │   ├── SetupWizard.cs
│   │   │   ├── SetupWizard.cs.meta
│   │   │   ├── SetupWizardWindow.cs
│   │   │   └── SetupWizardWindow.cs.meta
│   │   ├── Setup.meta
│   │   ├── Tools
│   │   │   ├── CommandRegistry.cs
│   │   │   ├── CommandRegistry.cs.meta
│   │   │   ├── ExecuteMenuItem.cs
│   │   │   ├── ExecuteMenuItem.cs.meta
│   │   │   ├── ManageAsset.cs
│   │   │   ├── ManageAsset.cs.meta
│   │   │   ├── ManageEditor.cs
│   │   │   ├── ManageEditor.cs.meta
│   │   │   ├── ManageGameObject.cs
│   │   │   ├── ManageGameObject.cs.meta
│   │   │   ├── ManageScene.cs
│   │   │   ├── ManageScene.cs.meta
│   │   │   ├── ManageScript.cs
│   │   │   ├── ManageScript.cs.meta
│   │   │   ├── ManageShader.cs
│   │   │   ├── ManageShader.cs.meta
│   │   │   ├── McpForUnityToolAttribute.cs
│   │   │   ├── McpForUnityToolAttribute.cs.meta
│   │   │   ├── Prefabs
│   │   │   │   ├── ManagePrefabs.cs
│   │   │   │   └── ManagePrefabs.cs.meta
│   │   │   ├── Prefabs.meta
│   │   │   ├── ReadConsole.cs
│   │   │   ├── ReadConsole.cs.meta
│   │   │   ├── RunTests.cs
│   │   │   └── RunTests.cs.meta
│   │   ├── Tools.meta
│   │   ├── Windows
│   │   │   ├── ManualConfigEditorWindow.cs
│   │   │   ├── ManualConfigEditorWindow.cs.meta
│   │   │   ├── MCPForUnityEditorWindow.cs
│   │   │   ├── MCPForUnityEditorWindow.cs.meta
│   │   │   ├── MCPForUnityEditorWindowNew.cs
│   │   │   ├── MCPForUnityEditorWindowNew.cs.meta
│   │   │   ├── MCPForUnityEditorWindowNew.uss
│   │   │   ├── MCPForUnityEditorWindowNew.uss.meta
│   │   │   ├── MCPForUnityEditorWindowNew.uxml
│   │   │   ├── MCPForUnityEditorWindowNew.uxml.meta
│   │   │   ├── VSCodeManualSetupWindow.cs
│   │   │   └── VSCodeManualSetupWindow.cs.meta
│   │   └── Windows.meta
│   ├── Editor.meta
│   ├── package.json
│   ├── package.json.meta
│   ├── README.md
│   ├── README.md.meta
│   ├── Runtime
│   │   ├── MCPForUnity.Runtime.asmdef
│   │   ├── MCPForUnity.Runtime.asmdef.meta
│   │   ├── Serialization
│   │   │   ├── UnityTypeConverters.cs
│   │   │   └── UnityTypeConverters.cs.meta
│   │   └── Serialization.meta
│   ├── Runtime.meta
│   └── UnityMcpServer~
│       └── src
│           ├── __init__.py
│           ├── config.py
│           ├── Dockerfile
│           ├── models.py
│           ├── module_discovery.py
│           ├── port_discovery.py
│           ├── pyproject.toml
│           ├── pyrightconfig.json
│           ├── registry
│           │   ├── __init__.py
│           │   ├── resource_registry.py
│           │   └── tool_registry.py
│           ├── reload_sentinel.py
│           ├── resources
│           │   ├── __init__.py
│           │   ├── menu_items.py
│           │   └── tests.py
│           ├── server_version.txt
│           ├── server.py
│           ├── telemetry_decorator.py
│           ├── telemetry.py
│           ├── test_telemetry.py
│           ├── tools
│           │   ├── __init__.py
│           │   ├── execute_menu_item.py
│           │   ├── manage_asset.py
│           │   ├── manage_editor.py
│           │   ├── manage_gameobject.py
│           │   ├── manage_prefabs.py
│           │   ├── manage_scene.py
│           │   ├── manage_script.py
│           │   ├── manage_shader.py
│           │   ├── read_console.py
│           │   ├── resource_tools.py
│           │   ├── run_tests.py
│           │   └── script_apply_edits.py
│           ├── unity_connection.py
│           └── uv.lock
├── prune_tool_results.py
├── README-zh.md
├── README.md
├── restore-dev.bat
├── scripts
│   └── validate-nlt-coverage.sh
├── test_unity_socket_framing.py
├── TestProjects
│   └── UnityMCPTests
│       ├── .gitignore
│       ├── Assets
│       │   ├── Editor.meta
│       │   ├── Scenes
│       │   │   ├── SampleScene.unity
│       │   │   └── SampleScene.unity.meta
│       │   ├── Scenes.meta
│       │   ├── Scripts
│       │   │   ├── Hello.cs
│       │   │   ├── Hello.cs.meta
│       │   │   ├── LongUnityScriptClaudeTest.cs
│       │   │   ├── LongUnityScriptClaudeTest.cs.meta
│       │   │   ├── TestAsmdef
│       │   │   │   ├── CustomComponent.cs
│       │   │   │   ├── CustomComponent.cs.meta
│       │   │   │   ├── TestAsmdef.asmdef
│       │   │   │   └── TestAsmdef.asmdef.meta
│       │   │   └── TestAsmdef.meta
│       │   ├── Scripts.meta
│       │   ├── Tests
│       │   │   ├── EditMode
│       │   │   │   ├── Data
│       │   │   │   │   ├── PythonToolsAssetTests.cs
│       │   │   │   │   └── PythonToolsAssetTests.cs.meta
│       │   │   │   ├── Data.meta
│       │   │   │   ├── Helpers
│       │   │   │   │   ├── CodexConfigHelperTests.cs
│       │   │   │   │   ├── CodexConfigHelperTests.cs.meta
│       │   │   │   │   ├── WriteToConfigTests.cs
│       │   │   │   │   └── WriteToConfigTests.cs.meta
│       │   │   │   ├── Helpers.meta
│       │   │   │   ├── MCPForUnityTests.Editor.asmdef
│       │   │   │   ├── MCPForUnityTests.Editor.asmdef.meta
│       │   │   │   ├── Resources
│       │   │   │   │   ├── GetMenuItemsTests.cs
│       │   │   │   │   └── GetMenuItemsTests.cs.meta
│       │   │   │   ├── Resources.meta
│       │   │   │   ├── Services
│       │   │   │   │   ├── PackageUpdateServiceTests.cs
│       │   │   │   │   ├── PackageUpdateServiceTests.cs.meta
│       │   │   │   │   ├── PythonToolRegistryServiceTests.cs
│       │   │   │   │   ├── PythonToolRegistryServiceTests.cs.meta
│       │   │   │   │   ├── ToolSyncServiceTests.cs
│       │   │   │   │   └── ToolSyncServiceTests.cs.meta
│       │   │   │   ├── Services.meta
│       │   │   │   ├── Tools
│       │   │   │   │   ├── AIPropertyMatchingTests.cs
│       │   │   │   │   ├── AIPropertyMatchingTests.cs.meta
│       │   │   │   │   ├── CommandRegistryTests.cs
│       │   │   │   │   ├── CommandRegistryTests.cs.meta
│       │   │   │   │   ├── ComponentResolverTests.cs
│       │   │   │   │   ├── ComponentResolverTests.cs.meta
│       │   │   │   │   ├── ExecuteMenuItemTests.cs
│       │   │   │   │   ├── ExecuteMenuItemTests.cs.meta
│       │   │   │   │   ├── ManageGameObjectTests.cs
│       │   │   │   │   ├── ManageGameObjectTests.cs.meta
│       │   │   │   │   ├── ManagePrefabsTests.cs
│       │   │   │   │   ├── ManagePrefabsTests.cs.meta
│       │   │   │   │   ├── ManageScriptValidationTests.cs
│       │   │   │   │   └── ManageScriptValidationTests.cs.meta
│       │   │   │   ├── Tools.meta
│       │   │   │   ├── Windows
│       │   │   │   │   ├── ManualConfigJsonBuilderTests.cs
│       │   │   │   │   └── ManualConfigJsonBuilderTests.cs.meta
│       │   │   │   └── Windows.meta
│       │   │   └── EditMode.meta
│       │   └── Tests.meta
│       ├── Packages
│       │   └── manifest.json
│       └── ProjectSettings
│           ├── Packages
│           │   └── com.unity.testtools.codecoverage
│           │       └── Settings.json
│           └── ProjectVersion.txt
├── tests
│   ├── conftest.py
│   ├── test_edit_normalization_and_noop.py
│   ├── test_edit_strict_and_warnings.py
│   ├── test_find_in_file_minimal.py
│   ├── test_get_sha.py
│   ├── test_improved_anchor_matching.py
│   ├── test_logging_stdout.py
│   ├── test_manage_script_uri.py
│   ├── test_read_console_truncate.py
│   ├── test_read_resource_minimal.py
│   ├── test_resources_api.py
│   ├── test_script_editing.py
│   ├── test_script_tools.py
│   ├── test_telemetry_endpoint_validation.py
│   ├── test_telemetry_queue_worker.py
│   ├── test_telemetry_subaction.py
│   ├── test_transport_framing.py
│   └── test_validate_script_summary.py
├── tools
│   └── stress_mcp.py
└── UnityMcpBridge
    ├── Editor
    │   ├── AssemblyInfo.cs
    │   ├── AssemblyInfo.cs.meta
    │   ├── Data
    │   │   ├── DefaultServerConfig.cs
    │   │   ├── DefaultServerConfig.cs.meta
    │   │   ├── McpClients.cs
    │   │   └── McpClients.cs.meta
    │   ├── Data.meta
    │   ├── Dependencies
    │   │   ├── DependencyManager.cs
    │   │   ├── DependencyManager.cs.meta
    │   │   ├── Models
    │   │   │   ├── DependencyCheckResult.cs
    │   │   │   ├── DependencyCheckResult.cs.meta
    │   │   │   ├── DependencyStatus.cs
    │   │   │   └── DependencyStatus.cs.meta
    │   │   ├── Models.meta
    │   │   ├── PlatformDetectors
    │   │   │   ├── IPlatformDetector.cs
    │   │   │   ├── IPlatformDetector.cs.meta
    │   │   │   ├── LinuxPlatformDetector.cs
    │   │   │   ├── LinuxPlatformDetector.cs.meta
    │   │   │   ├── MacOSPlatformDetector.cs
    │   │   │   ├── MacOSPlatformDetector.cs.meta
    │   │   │   ├── PlatformDetectorBase.cs
    │   │   │   ├── PlatformDetectorBase.cs.meta
    │   │   │   ├── WindowsPlatformDetector.cs
    │   │   │   └── WindowsPlatformDetector.cs.meta
    │   │   └── PlatformDetectors.meta
    │   ├── Dependencies.meta
    │   ├── External
    │   │   ├── Tommy.cs
    │   │   └── Tommy.cs.meta
    │   ├── External.meta
    │   ├── Helpers
    │   │   ├── AssetPathUtility.cs
    │   │   ├── AssetPathUtility.cs.meta
    │   │   ├── CodexConfigHelper.cs
    │   │   ├── CodexConfigHelper.cs.meta
    │   │   ├── ConfigJsonBuilder.cs
    │   │   ├── ConfigJsonBuilder.cs.meta
    │   │   ├── ExecPath.cs
    │   │   ├── ExecPath.cs.meta
    │   │   ├── GameObjectSerializer.cs
    │   │   ├── GameObjectSerializer.cs.meta
    │   │   ├── McpConfigFileHelper.cs
    │   │   ├── McpConfigFileHelper.cs.meta
    │   │   ├── McpConfigurationHelper.cs
    │   │   ├── McpConfigurationHelper.cs.meta
    │   │   ├── McpLog.cs
    │   │   ├── McpLog.cs.meta
    │   │   ├── McpPathResolver.cs
    │   │   ├── McpPathResolver.cs.meta
    │   │   ├── PackageDetector.cs
    │   │   ├── PackageDetector.cs.meta
    │   │   ├── PackageInstaller.cs
    │   │   ├── PackageInstaller.cs.meta
    │   │   ├── PortManager.cs
    │   │   ├── PortManager.cs.meta
    │   │   ├── Response.cs
    │   │   ├── Response.cs.meta
    │   │   ├── ServerInstaller.cs
    │   │   ├── ServerInstaller.cs.meta
    │   │   ├── ServerPathResolver.cs
    │   │   ├── ServerPathResolver.cs.meta
    │   │   ├── TelemetryHelper.cs
    │   │   ├── TelemetryHelper.cs.meta
    │   │   ├── Vector3Helper.cs
    │   │   └── Vector3Helper.cs.meta
    │   ├── Helpers.meta
    │   ├── MCPForUnity.Editor.asmdef
    │   ├── MCPForUnity.Editor.asmdef.meta
    │   ├── MCPForUnityBridge.cs
    │   ├── MCPForUnityBridge.cs.meta
    │   ├── Models
    │   │   ├── Command.cs
    │   │   ├── Command.cs.meta
    │   │   ├── McpClient.cs
    │   │   ├── McpClient.cs.meta
    │   │   ├── McpConfig.cs
    │   │   ├── McpConfig.cs.meta
    │   │   ├── MCPConfigServer.cs
    │   │   ├── MCPConfigServer.cs.meta
    │   │   ├── MCPConfigServers.cs
    │   │   ├── MCPConfigServers.cs.meta
    │   │   ├── McpStatus.cs
    │   │   ├── McpStatus.cs.meta
    │   │   ├── McpTypes.cs
    │   │   ├── McpTypes.cs.meta
    │   │   ├── ServerConfig.cs
    │   │   └── ServerConfig.cs.meta
    │   ├── Models.meta
    │   ├── Setup
    │   │   ├── SetupWizard.cs
    │   │   ├── SetupWizard.cs.meta
    │   │   ├── SetupWizardWindow.cs
    │   │   └── SetupWizardWindow.cs.meta
    │   ├── Setup.meta
    │   ├── Tools
    │   │   ├── CommandRegistry.cs
    │   │   ├── CommandRegistry.cs.meta
    │   │   ├── ManageAsset.cs
    │   │   ├── ManageAsset.cs.meta
    │   │   ├── ManageEditor.cs
    │   │   ├── ManageEditor.cs.meta
    │   │   ├── ManageGameObject.cs
    │   │   ├── ManageGameObject.cs.meta
    │   │   ├── ManageScene.cs
    │   │   ├── ManageScene.cs.meta
    │   │   ├── ManageScript.cs
    │   │   ├── ManageScript.cs.meta
    │   │   ├── ManageShader.cs
    │   │   ├── ManageShader.cs.meta
    │   │   ├── McpForUnityToolAttribute.cs
    │   │   ├── McpForUnityToolAttribute.cs.meta
    │   │   ├── MenuItems
    │   │   │   ├── ManageMenuItem.cs
    │   │   │   ├── ManageMenuItem.cs.meta
    │   │   │   ├── MenuItemExecutor.cs
    │   │   │   ├── MenuItemExecutor.cs.meta
    │   │   │   ├── MenuItemsReader.cs
    │   │   │   └── MenuItemsReader.cs.meta
    │   │   ├── MenuItems.meta
    │   │   ├── Prefabs
    │   │   │   ├── ManagePrefabs.cs
    │   │   │   └── ManagePrefabs.cs.meta
    │   │   ├── Prefabs.meta
    │   │   ├── ReadConsole.cs
    │   │   └── ReadConsole.cs.meta
    │   ├── Tools.meta
    │   ├── Windows
    │   │   ├── ManualConfigEditorWindow.cs
    │   │   ├── ManualConfigEditorWindow.cs.meta
    │   │   ├── MCPForUnityEditorWindow.cs
    │   │   ├── MCPForUnityEditorWindow.cs.meta
    │   │   ├── VSCodeManualSetupWindow.cs
    │   │   └── VSCodeManualSetupWindow.cs.meta
    │   └── Windows.meta
    ├── Editor.meta
    ├── package.json
    ├── package.json.meta
    ├── README.md
    ├── README.md.meta
    ├── Runtime
    │   ├── MCPForUnity.Runtime.asmdef
    │   ├── MCPForUnity.Runtime.asmdef.meta
    │   ├── Serialization
    │   │   ├── UnityTypeConverters.cs
    │   │   └── UnityTypeConverters.cs.meta
    │   └── Serialization.meta
    ├── Runtime.meta
    └── UnityMcpServer~
        └── src
            ├── __init__.py
            ├── config.py
            ├── Dockerfile
            ├── port_discovery.py
            ├── pyproject.toml
            ├── pyrightconfig.json
            ├── registry
            │   ├── __init__.py
            │   └── tool_registry.py
            ├── reload_sentinel.py
            ├── server_version.txt
            ├── server.py
            ├── telemetry_decorator.py
            ├── telemetry.py
            ├── test_telemetry.py
            ├── tools
            │   ├── __init__.py
            │   ├── manage_asset.py
            │   ├── manage_editor.py
            │   ├── manage_gameobject.py
            │   ├── manage_menu_item.py
            │   ├── manage_prefabs.py
            │   ├── manage_scene.py
            │   ├── manage_script.py
            │   ├── manage_shader.py
            │   ├── read_console.py
            │   ├── resource_tools.py
            │   └── script_apply_edits.py
            ├── unity_connection.py
            └── uv.lock
```

# Files

--------------------------------------------------------------------------------
/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs:
--------------------------------------------------------------------------------

```csharp
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Windows
{
    public class MCPForUnityEditorWindow : EditorWindow
    {
        private bool isUnityBridgeRunning = false;
        private Vector2 scrollPosition;
        private string pythonServerInstallationStatus = "Not Installed";
        private Color pythonServerInstallationStatusColor = Color.red;
        private const int mcpPort = 6500; // MCP port (still hardcoded for MCP server)
        private readonly McpClients mcpClients = new();
        private bool autoRegisterEnabled;
        private bool lastClientRegisteredOk;
        private bool lastBridgeVerifiedOk;
        private string pythonDirOverride = null;
        private bool debugLogsEnabled;

        // Script validation settings
        private int validationLevelIndex = 1; // Default to Standard
        private readonly string[] validationLevelOptions = new string[]
        {
            "Basic - Only syntax checks",
            "Standard - Syntax + Unity practices",
            "Comprehensive - All checks + semantic analysis",
            "Strict - Full semantic validation (requires Roslyn)"
        };

        // UI state
        private int selectedClientIndex = 0;

        public static void ShowWindow()
        {
            GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
        }

        private void OnEnable()
        {
            UpdatePythonServerInstallationStatus();

            // Refresh bridge status
            isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
            autoRegisterEnabled = EditorPrefs.GetBool("MCPForUnity.AutoRegisterEnabled", true);
            debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
            if (debugLogsEnabled)
            {
                LogDebugPrefsState();
            }
            foreach (McpClient mcpClient in mcpClients.clients)
            {
                CheckMcpConfiguration(mcpClient);
            }

            // Load validation level setting
            LoadValidationLevelSetting();

            // First-run auto-setup only if Claude CLI is available
            if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
            {
                AutoFirstRunSetup();
            }
        }

        private void OnFocus()
        {
            // Refresh bridge running state on focus in case initialization completed after domain reload
            isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
            if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
            {
                McpClient selectedClient = mcpClients.clients[selectedClientIndex];
                CheckMcpConfiguration(selectedClient);
            }
            Repaint();
        }

        private Color GetStatusColor(McpStatus status)
        {
            // Return appropriate color based on the status enum
            return status switch
            {
                McpStatus.Configured => Color.green,
                McpStatus.Running => Color.green,
                McpStatus.Connected => Color.green,
                McpStatus.IncorrectPath => Color.yellow,
                McpStatus.CommunicationError => Color.yellow,
                McpStatus.NoResponse => Color.yellow,
                _ => Color.red, // Default to red for error states or not configured
            };
        }

        private void UpdatePythonServerInstallationStatus()
        {
            try
            {
                string installedPath = ServerInstaller.GetServerPath();
                bool installedOk = !string.IsNullOrEmpty(installedPath) && File.Exists(Path.Combine(installedPath, "server.py"));
                if (installedOk)
                {
                    pythonServerInstallationStatus = "Installed";
                    pythonServerInstallationStatusColor = Color.green;
                    return;
                }

                // Fall back to embedded/dev source via our existing resolution logic
                string embeddedPath = FindPackagePythonDirectory();
                bool embeddedOk = !string.IsNullOrEmpty(embeddedPath) && File.Exists(Path.Combine(embeddedPath, "server.py"));
                if (embeddedOk)
                {
                    pythonServerInstallationStatus = "Installed (Embedded)";
                    pythonServerInstallationStatusColor = Color.green;
                }
                else
                {
                    pythonServerInstallationStatus = "Not Installed";
                    pythonServerInstallationStatusColor = Color.red;
                }
            }
            catch
            {
                pythonServerInstallationStatus = "Not Installed";
                pythonServerInstallationStatusColor = Color.red;
            }
        }


        private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12)
        {
            float offsetX = (statusRect.width - size) / 2;
            float offsetY = (statusRect.height - size) / 2;
            Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size);
            Vector3 center = new(
                dotRect.x + (dotRect.width / 2),
                dotRect.y + (dotRect.height / 2),
                0
            );
            float radius = size / 2;

            // Draw the main dot
            Handles.color = statusColor;
            Handles.DrawSolidDisc(center, Vector3.forward, radius);

            // Draw the border
            Color borderColor = new(
                statusColor.r * 0.7f,
                statusColor.g * 0.7f,
                statusColor.b * 0.7f
            );
            Handles.color = borderColor;
            Handles.DrawWireDisc(center, Vector3.forward, radius);
        }

        private void OnGUI()
        {
            scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

            // Header
            DrawHeader();

            // Compute equal column widths for uniform layout
            float horizontalSpacing = 2f;
            float outerPadding = 20f; // approximate padding
            // Make columns a bit less wide for a tighter layout
            float computed = (position.width - outerPadding - horizontalSpacing) / 2f;
            float colWidth = Mathf.Clamp(computed, 220f, 340f);
            // Use fixed heights per row so paired panels match exactly
            float topPanelHeight = 190f;
            float bottomPanelHeight = 230f;

            // Top row: Server Status (left) and Unity Bridge (right)
            EditorGUILayout.BeginHorizontal();
            {
                EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight));
                DrawServerStatusSection();
                EditorGUILayout.EndVertical();

                EditorGUILayout.Space(horizontalSpacing);

                EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight));
                DrawBridgeSection();
                EditorGUILayout.EndVertical();
            }
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.Space(10);

            // Second row: MCP Client Configuration (left) and Script Validation (right)
            EditorGUILayout.BeginHorizontal();
            {
                EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight));
                DrawUnifiedClientConfiguration();
                EditorGUILayout.EndVertical();

                EditorGUILayout.Space(horizontalSpacing);

                EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight));
                DrawValidationSection();
                EditorGUILayout.EndVertical();
            }
            EditorGUILayout.EndHorizontal();

            // Minimal bottom padding
            EditorGUILayout.Space(2);

            EditorGUILayout.EndScrollView();
        }

        private void DrawHeader()
        {
            EditorGUILayout.Space(15);
            Rect titleRect = EditorGUILayout.GetControlRect(false, 40);
            EditorGUI.DrawRect(titleRect, new Color(0.2f, 0.2f, 0.2f, 0.1f));

            GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel)
            {
                fontSize = 16,
                alignment = TextAnchor.MiddleLeft
            };

            GUI.Label(
                new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height),
                "MCP For Unity",
                titleStyle
            );

            // Place the Show Debug Logs toggle on the same header row, right-aligned
            float toggleWidth = 160f;
            Rect toggleRect = new Rect(titleRect.xMax - toggleWidth - 12f, titleRect.y + 10f, toggleWidth, 20f);
            bool newDebug = GUI.Toggle(toggleRect, debugLogsEnabled, "Show Debug Logs");
            if (newDebug != debugLogsEnabled)
            {
                debugLogsEnabled = newDebug;
                EditorPrefs.SetBool("MCPForUnity.DebugLogs", debugLogsEnabled);
                if (debugLogsEnabled)
                {
                    LogDebugPrefsState();
                }
            }
            EditorGUILayout.Space(15);
        }

        private void LogDebugPrefsState()
        {
            try
            {
                string pythonDirOverridePref = SafeGetPrefString("MCPForUnity.PythonDirOverride");
                string uvPathPref = SafeGetPrefString("MCPForUnity.UvPath");
                string serverSrcPref = SafeGetPrefString("MCPForUnity.ServerSrc");
                bool useEmbedded = SafeGetPrefBool("MCPForUnity.UseEmbeddedServer");

                // Version-scoped detection key
                string embeddedVer = ReadEmbeddedVersionOrFallback();
                string detectKey = $"MCPForUnity.LegacyDetectLogged:{embeddedVer}";
                bool detectLogged = SafeGetPrefBool(detectKey);

                // Project-scoped auto-register key
                string projectPath = Application.dataPath ?? string.Empty;
                string autoKey = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}";
                bool autoRegistered = SafeGetPrefBool(autoKey);

                MCPForUnity.Editor.Helpers.McpLog.Info(
                    "MCP Debug Prefs:\n" +
                    $"  DebugLogs: {debugLogsEnabled}\n" +
                    $"  PythonDirOverride: '{pythonDirOverridePref}'\n" +
                    $"  UvPath: '{uvPathPref}'\n" +
                    $"  ServerSrc: '{serverSrcPref}'\n" +
                    $"  UseEmbeddedServer: {useEmbedded}\n" +
                    $"  DetectOnceKey: '{detectKey}' => {detectLogged}\n" +
                    $"  AutoRegisteredKey: '{autoKey}' => {autoRegistered}",
                    always: false
                );
            }
            catch (Exception ex)
            {
                UnityEngine.Debug.LogWarning($"MCP Debug Prefs logging failed: {ex.Message}");
            }
        }

        private static string SafeGetPrefString(string key)
        {
            try { return EditorPrefs.GetString(key, string.Empty) ?? string.Empty; } catch { return string.Empty; }
        }

        private static bool SafeGetPrefBool(string key)
        {
            try { return EditorPrefs.GetBool(key, false); } catch { return false; }
        }

        private static string ReadEmbeddedVersionOrFallback()
        {
            try
            {
                if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
                {
                    var p = Path.Combine(embeddedSrc, "server_version.txt");
                    if (File.Exists(p))
                    {
                        var s = File.ReadAllText(p)?.Trim();
                        if (!string.IsNullOrEmpty(s)) return s;
                    }
                }
            }
            catch { }
            return "unknown";
        }

        private void DrawServerStatusSection()
        {
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
            {
                fontSize = 14
            };
            EditorGUILayout.LabelField("Server Status", sectionTitleStyle);
            EditorGUILayout.Space(8);

            EditorGUILayout.BeginHorizontal();
            Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
            DrawStatusDot(statusRect, pythonServerInstallationStatusColor, 16);

            GUIStyle statusStyle = new GUIStyle(EditorStyles.label)
            {
                fontSize = 12,
                fontStyle = FontStyle.Bold
            };
            EditorGUILayout.LabelField(pythonServerInstallationStatus, statusStyle, GUILayout.Height(28));
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.Space(5);

            EditorGUILayout.BeginHorizontal();
            bool isAutoMode = MCPForUnityBridge.IsAutoConnectMode();
            GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 };
            EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle);
            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();

            int currentUnityPort = MCPForUnityBridge.GetCurrentPort();
            GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel)
            {
                fontSize = 11
            };
            EditorGUILayout.LabelField($"Ports: Unity {currentUnityPort}, MCP {mcpPort}", portStyle);
            EditorGUILayout.Space(5);

            /// Auto-Setup button below ports
            string setupButtonText = (lastClientRegisteredOk && lastBridgeVerifiedOk) ? "Connected ✓" : "Auto-Setup";
            if (GUILayout.Button(setupButtonText, GUILayout.Height(24)))
            {
                RunSetupNow();
            }
            EditorGUILayout.Space(4);

            // Rebuild MCP Server button with tooltip tag
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();
                GUIContent repairLabel = new GUIContent(
                    "Rebuild MCP Server",
                    "Deletes the installed server and re-copies it from the package. Use this to update the server after making source code changes or if the installation is corrupted."
                );
                if (GUILayout.Button(repairLabel, GUILayout.Width(160), GUILayout.Height(22)))
                {
                    bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RebuildMcpServer();
                    if (ok)
                    {
                        EditorUtility.DisplayDialog("MCP For Unity", "Server rebuilt successfully.", "OK");
                        UpdatePythonServerInstallationStatus();
                    }
                    else
                    {
                        EditorUtility.DisplayDialog("MCP For Unity", "Rebuild failed. Please check Console for details.", "OK");
                    }
                }
            }
            // (Removed descriptive tool tag under the Repair button)

            // (Show Debug Logs toggle moved to header)
            EditorGUILayout.Space(2);

            // Python detection warning with link
            if (!IsPythonDetected())
            {
                GUIStyle warnStyle = new GUIStyle(EditorStyles.label) { richText = true, wordWrap = true };
                EditorGUILayout.LabelField("<color=#cc3333><b>Warning:</b></color> No Python installation found.", warnStyle);
                using (new EditorGUILayout.HorizontalScope())
                {
                    if (GUILayout.Button("Open Install Instructions", GUILayout.Width(200)))
                    {
                        Application.OpenURL("https://www.python.org/downloads/");
                    }
                }
                EditorGUILayout.Space(4);
            }

            // Troubleshooting helpers
            if (pythonServerInstallationStatusColor != Color.green)
            {
                using (new EditorGUILayout.HorizontalScope())
                {
                    if (GUILayout.Button("Select server folder…", GUILayout.Width(160)))
                    {
                        string picked = EditorUtility.OpenFolderPanel("Select UnityMcpServer/src", Application.dataPath, "");
                        if (!string.IsNullOrEmpty(picked) && File.Exists(Path.Combine(picked, "server.py")))
                        {
                            pythonDirOverride = picked;
                            EditorPrefs.SetString("MCPForUnity.PythonDirOverride", pythonDirOverride);
                            UpdatePythonServerInstallationStatus();
                        }
                        else if (!string.IsNullOrEmpty(picked))
                        {
                            EditorUtility.DisplayDialog("Invalid Selection", "The selected folder does not contain server.py", "OK");
                        }
                    }
                    if (GUILayout.Button("Verify again", GUILayout.Width(120)))
                    {
                        UpdatePythonServerInstallationStatus();
                    }
                }
            }
            EditorGUILayout.EndVertical();
        }

        private void DrawBridgeSection()
        {
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            // Always reflect the live state each repaint to avoid stale UI after recompiles
            isUnityBridgeRunning = MCPForUnityBridge.IsRunning;

            GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
            {
                fontSize = 14
            };
            EditorGUILayout.LabelField("Unity Bridge", sectionTitleStyle);
            EditorGUILayout.Space(8);

            EditorGUILayout.BeginHorizontal();
            Color bridgeColor = isUnityBridgeRunning ? Color.green : Color.red;
            Rect bridgeStatusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
            DrawStatusDot(bridgeStatusRect, bridgeColor, 16);

            GUIStyle bridgeStatusStyle = new GUIStyle(EditorStyles.label)
            {
                fontSize = 12,
                fontStyle = FontStyle.Bold
            };
            EditorGUILayout.LabelField(isUnityBridgeRunning ? "Running" : "Stopped", bridgeStatusStyle, GUILayout.Height(28));
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.Space(8);
            if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge", GUILayout.Height(32)))
            {
                ToggleUnityBridge();
            }
            EditorGUILayout.Space(5);
            EditorGUILayout.EndVertical();
        }

        private void DrawValidationSection()
        {
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
            {
                fontSize = 14
            };
            EditorGUILayout.LabelField("Script Validation", sectionTitleStyle);
            EditorGUILayout.Space(8);

            EditorGUI.BeginChangeCheck();
            validationLevelIndex = EditorGUILayout.Popup("Validation Level", validationLevelIndex, validationLevelOptions, GUILayout.Height(20));
            if (EditorGUI.EndChangeCheck())
            {
                SaveValidationLevelSetting();
            }

            EditorGUILayout.Space(8);
            string description = GetValidationLevelDescription(validationLevelIndex);
            EditorGUILayout.HelpBox(description, MessageType.Info);
            EditorGUILayout.Space(4);
            // (Show Debug Logs toggle moved to header)
            EditorGUILayout.Space(2);
            EditorGUILayout.EndVertical();
        }

        private void DrawUnifiedClientConfiguration()
        {
            EditorGUILayout.BeginVertical(EditorStyles.helpBox);

            GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
            {
                fontSize = 14
            };
            EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle);
            EditorGUILayout.Space(10);

            // (Auto-connect toggle removed per design)

            // Client selector
            string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray();
            EditorGUI.BeginChangeCheck();
            selectedClientIndex = EditorGUILayout.Popup("Select Client", selectedClientIndex, clientNames, GUILayout.Height(20));
            if (EditorGUI.EndChangeCheck())
            {
                selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1);
            }

            EditorGUILayout.Space(10);

            if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
            {
                McpClient selectedClient = mcpClients.clients[selectedClientIndex];
                DrawClientConfigurationCompact(selectedClient);
            }

            EditorGUILayout.Space(5);
            EditorGUILayout.EndVertical();
        }

        private void AutoFirstRunSetup()
        {
            try
            {
                // Project-scoped one-time flag
                string projectPath = Application.dataPath ?? string.Empty;
                string key = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}";
                if (EditorPrefs.GetBool(key, false))
                {
                    return;
                }

                // Attempt client registration using discovered Python server dir
                pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null);
                string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
                if (!string.IsNullOrEmpty(pythonDir) && File.Exists(Path.Combine(pythonDir, "server.py")))
                {
                    bool anyRegistered = false;
                    foreach (McpClient client in mcpClients.clients)
                    {
                        try
                        {
                            if (client.mcpType == McpTypes.ClaudeCode)
                            {
                                // Only attempt if Claude CLI is present
                                if (!IsClaudeConfigured() && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
                                {
                                    RegisterWithClaudeCode(pythonDir);
                                    anyRegistered = true;
                                }
                            }
                            else
                            {
                                CheckMcpConfiguration(client);
                                bool alreadyConfigured = client.status == McpStatus.Configured;
                                if (!alreadyConfigured)
                                {
                                    ConfigureMcpClient(client);
                                    anyRegistered = true;
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}");
                        }
                    }
                    lastClientRegisteredOk = anyRegistered
                        || IsCursorConfigured(pythonDir)
                        || CodexConfigHelper.IsCodexConfigured(pythonDir)
                        || IsClaudeConfigured();
                }

                // Ensure the bridge is listening and has a fresh saved port
                if (!MCPForUnityBridge.IsRunning)
                {
                    try
                    {
                        MCPForUnityBridge.StartAutoConnect();
                        isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
                        Repaint();
                    }
                    catch (Exception ex)
                    {
                        MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup StartAutoConnect failed: {ex.Message}");
                    }
                }

                // Verify bridge with a quick ping
                lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort());

                EditorPrefs.SetBool(key, true);
            }
            catch (Exception e)
            {
                MCPForUnity.Editor.Helpers.McpLog.Warn($"MCP for Unity auto-setup skipped: {e.Message}");
            }
        }

        private static string ComputeSha1(string input)
        {
            try
            {
                using SHA1 sha1 = SHA1.Create();
                byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
                byte[] hash = sha1.ComputeHash(bytes);
                StringBuilder sb = new StringBuilder(hash.Length * 2);
                foreach (byte b in hash)
                {
                    sb.Append(b.ToString("x2"));
                }
                return sb.ToString();
            }
            catch
            {
                return "";
            }
        }

        private void RunSetupNow()
        {
            // Force a one-shot setup regardless of first-run flag
            try
            {
                pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null);
                string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
                if (string.IsNullOrEmpty(pythonDir) || !File.Exists(Path.Combine(pythonDir, "server.py")))
                {
                    EditorUtility.DisplayDialog("Setup", "Python server not found. Please select UnityMcpServer/src.", "OK");
                    return;
                }

                bool anyRegistered = false;
                foreach (McpClient client in mcpClients.clients)
                {
                    try
                    {
                        if (client.mcpType == McpTypes.ClaudeCode)
                        {
                            if (!IsClaudeConfigured())
                            {
                                RegisterWithClaudeCode(pythonDir);
                                anyRegistered = true;
                            }
                        }
                        else
                        {
                            CheckMcpConfiguration(client);
                            bool alreadyConfigured = client.status == McpStatus.Configured;
                            if (!alreadyConfigured)
                            {
                                ConfigureMcpClient(client);
                                anyRegistered = true;
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}");
                    }
                }
                lastClientRegisteredOk = anyRegistered
                    || IsCursorConfigured(pythonDir)
                    || CodexConfigHelper.IsCodexConfigured(pythonDir)
                    || IsClaudeConfigured();

                // Restart/ensure bridge
                MCPForUnityBridge.StartAutoConnect();
                isUnityBridgeRunning = MCPForUnityBridge.IsRunning;

                // Verify
                lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort());
                Repaint();
            }
            catch (Exception e)
            {
                EditorUtility.DisplayDialog("Setup Failed", e.Message, "OK");
            }
        }

        private static bool IsCursorConfigured(string pythonDir)
        {
            try
            {
                string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                    ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                        ".cursor", "mcp.json")
                    : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                        ".cursor", "mcp.json");
                if (!File.Exists(configPath)) return false;
                string json = File.ReadAllText(configPath);
                dynamic cfg = JsonConvert.DeserializeObject(json);
                var servers = cfg?.mcpServers;
                if (servers == null) return false;
                var unity = servers.unityMCP ?? servers.UnityMCP;
                if (unity == null) return false;
                var args = unity.args;
                if (args == null) return false;
                // Prefer exact extraction of the --directory value and compare normalized paths
                string[] strArgs = ((System.Collections.Generic.IEnumerable<object>)args)
                    .Select(x => x?.ToString() ?? string.Empty)
                    .ToArray();
                string dir = McpConfigFileHelper.ExtractDirectoryArg(strArgs);
                if (string.IsNullOrEmpty(dir)) return false;
                return McpConfigFileHelper.PathsEqual(dir, pythonDir);
            }
            catch { return false; }
        }

        private static bool IsClaudeConfigured()
        {
            try
            {
                string claudePath = ExecPath.ResolveClaude();
                if (string.IsNullOrEmpty(claudePath)) return false;

                // Only prepend PATH on Unix
                string pathPrepend = null;
                if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
                {
                    pathPrepend = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
                        ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
                        : "/usr/local/bin:/usr/bin:/bin";
                }

                if (!ExecPath.TryRun(claudePath, "mcp list", workingDir: null, out var stdout, out var stderr, 5000, pathPrepend))
                {
                    return false;
                }
                return (stdout ?? string.Empty).IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0;
            }
            catch { return false; }
        }

        private static bool VerifyBridgePing(int port)
        {
            // Use strict framed protocol to match bridge (FRAMING=1)
            const int ConnectTimeoutMs = 1000;
            const int FrameTimeoutMs = 30000; // match bridge frame I/O timeout

            try
            {
                using TcpClient client = new TcpClient();
                var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
                if (!connectTask.Wait(ConnectTimeoutMs)) return false;

                using NetworkStream stream = client.GetStream();
                try { client.NoDelay = true; } catch { }

                // 1) Read handshake line (ASCII, newline-terminated)
                string handshake = ReadLineAscii(stream, 2000);
                if (string.IsNullOrEmpty(handshake) || handshake.IndexOf("FRAMING=1", StringComparison.OrdinalIgnoreCase) < 0)
                {
                    UnityEngine.Debug.LogWarning("MCP for Unity: Bridge handshake missing FRAMING=1");
                    return false;
                }

                // 2) Send framed "ping"
                byte[] payload = Encoding.UTF8.GetBytes("ping");
                WriteFrame(stream, payload, FrameTimeoutMs);

                // 3) Read framed response and check for pong
                string response = ReadFrameUtf8(stream, FrameTimeoutMs);
                bool ok = !string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0;
                if (!ok)
                {
                    UnityEngine.Debug.LogWarning($"MCP for Unity: Framed ping failed; response='{response}'");
                }
                return ok;
            }
            catch (Exception ex)
            {
                UnityEngine.Debug.LogWarning($"MCP for Unity: VerifyBridgePing error: {ex.Message}");
                return false;
            }
        }

        // Minimal framing helpers (8-byte big-endian length prefix), blocking with timeouts
        private static void WriteFrame(NetworkStream stream, byte[] payload, int timeoutMs)
        {
            if (payload == null) throw new ArgumentNullException(nameof(payload));
            if (payload.LongLength < 1) throw new IOException("Zero-length frames are not allowed");
            byte[] header = new byte[8];
            ulong len = (ulong)payload.LongLength;
            header[0] = (byte)(len >> 56);
            header[1] = (byte)(len >> 48);
            header[2] = (byte)(len >> 40);
            header[3] = (byte)(len >> 32);
            header[4] = (byte)(len >> 24);
            header[5] = (byte)(len >> 16);
            header[6] = (byte)(len >> 8);
            header[7] = (byte)(len);

            stream.WriteTimeout = timeoutMs;
            stream.Write(header, 0, header.Length);
            stream.Write(payload, 0, payload.Length);
        }

        private static string ReadFrameUtf8(NetworkStream stream, int timeoutMs)
        {
            byte[] header = ReadExact(stream, 8, timeoutMs);
            ulong len = ((ulong)header[0] << 56)
                      | ((ulong)header[1] << 48)
                      | ((ulong)header[2] << 40)
                      | ((ulong)header[3] << 32)
                      | ((ulong)header[4] << 24)
                      | ((ulong)header[5] << 16)
                      | ((ulong)header[6] << 8)
                      | header[7];
            if (len == 0UL) throw new IOException("Zero-length frames are not allowed");
            if (len > int.MaxValue) throw new IOException("Frame too large");
            byte[] payload = ReadExact(stream, (int)len, timeoutMs);
            return Encoding.UTF8.GetString(payload);
        }

        private static byte[] ReadExact(NetworkStream stream, int count, int timeoutMs)
        {
            byte[] buffer = new byte[count];
            int offset = 0;
            stream.ReadTimeout = timeoutMs;
            while (offset < count)
            {
                int read = stream.Read(buffer, offset, count - offset);
                if (read <= 0) throw new IOException("Connection closed before reading expected bytes");
                offset += read;
            }
            return buffer;
        }

        private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int maxLen = 512)
        {
            stream.ReadTimeout = timeoutMs;
            using var ms = new MemoryStream();
            byte[] one = new byte[1];
            while (ms.Length < maxLen)
            {
                int n = stream.Read(one, 0, 1);
                if (n <= 0) break;
                if (one[0] == (byte)'\n') break;
                ms.WriteByte(one[0]);
            }
            return Encoding.ASCII.GetString(ms.ToArray());
        }

        private void DrawClientConfigurationCompact(McpClient mcpClient)
        {
            // Special pre-check for Claude Code: if CLI missing, reflect in status UI
            if (mcpClient.mcpType == McpTypes.ClaudeCode)
            {
                string claudeCheck = ExecPath.ResolveClaude();
                if (string.IsNullOrEmpty(claudeCheck))
                {
                    mcpClient.configStatus = "Claude Not Found";
                    mcpClient.status = McpStatus.NotConfigured;
                }
            }

            // Pre-check for clients that require uv (all except Claude Code)
            bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode;
            bool uvMissingEarly = false;
            if (uvRequired)
            {
                string uvPathEarly = FindUvPath();
                if (string.IsNullOrEmpty(uvPathEarly))
                {
                    uvMissingEarly = true;
                    mcpClient.configStatus = "uv Not Found";
                    mcpClient.status = McpStatus.NotConfigured;
                }
            }

            // Status display
            EditorGUILayout.BeginHorizontal();
            Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
            Color statusColor = GetStatusColor(mcpClient.status);
            DrawStatusDot(statusRect, statusColor, 16);

            GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label)
            {
                fontSize = 12,
                fontStyle = FontStyle.Bold
            };
            EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28));
            EditorGUILayout.EndHorizontal();
            // When Claude CLI is missing, show a clear install hint directly below status
            if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude()))
            {
                GUIStyle installHintStyle = new GUIStyle(clientStatusStyle);
                installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange
                EditorGUILayout.BeginHorizontal();
                GUIContent installText = new GUIContent("Make sure Claude Code is installed!");
                Vector2 textSize = installHintStyle.CalcSize(installText);
                EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false));
                GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold };
                GUILayout.Space(6);
                if (GUILayout.Button("[HELP]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false)))
                {
                    Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code");
                }
                EditorGUILayout.EndHorizontal();
            }

            EditorGUILayout.Space(10);

            // If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls
            if (uvRequired && uvMissingEarly)
            {
                GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label)
                {
                    fontSize = 12,
                    fontStyle = FontStyle.Bold,
                    wordWrap = false
                };
                installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f);
                EditorGUILayout.BeginHorizontal();
                GUIContent installText2 = new GUIContent("Make sure uv is installed!");
                Vector2 sz = installHintStyle2.CalcSize(installText2);
                EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false));
                GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold };
                GUILayout.Space(6);
                if (GUILayout.Button("[HELP]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false)))
                {
                    Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf");
                }
                EditorGUILayout.EndHorizontal();

                EditorGUILayout.Space(8);
                EditorGUILayout.BeginHorizontal();
                if (GUILayout.Button("Choose uv Install Location", GUILayout.Width(260), GUILayout.Height(22)))
                {
                    string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
                    string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, "");
                    if (!string.IsNullOrEmpty(picked))
                    {
                        EditorPrefs.SetString("MCPForUnity.UvPath", picked);
                        ConfigureMcpClient(mcpClient);
                        Repaint();
                    }
                }
                EditorGUILayout.EndHorizontal();
                return;
            }

            // Action buttons in horizontal layout
            EditorGUILayout.BeginHorizontal();

            if (mcpClient.mcpType == McpTypes.VSCode)
            {
                if (GUILayout.Button("Auto Configure", GUILayout.Height(32)))
                {
                    ConfigureMcpClient(mcpClient);
                }
            }
            else if (mcpClient.mcpType == McpTypes.ClaudeCode)
            {
                bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
                if (claudeAvailable)
                {
                    bool isConfigured = mcpClient.status == McpStatus.Configured;
                    string buttonText = isConfigured ? "Unregister MCP for Unity with Claude Code" : "Register with Claude Code";
                    if (GUILayout.Button(buttonText, GUILayout.Height(32)))
                    {
                        if (isConfigured)
                        {
                            UnregisterWithClaudeCode();
                        }
                        else
                        {
                            string pythonDir = FindPackagePythonDirectory();
                            RegisterWithClaudeCode(pythonDir);
                        }
                    }
                    // Hide the picker once a valid binary is available
                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                    GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true };
                    string resolvedClaude = ExecPath.ResolveClaude();
                    EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle);
                    EditorGUILayout.EndHorizontal();
                    EditorGUILayout.BeginHorizontal();
                }
                // CLI picker row (only when not found)
                EditorGUILayout.EndHorizontal();
                EditorGUILayout.BeginHorizontal();
                if (!claudeAvailable)
                {
                    // Only show the picker button in not-found state (no redundant "not found" label)
                    if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22)))
                    {
                        string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
                        string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, "");
                        if (!string.IsNullOrEmpty(picked))
                        {
                            ExecPath.SetClaudeCliPath(picked);
                            // Auto-register after setting a valid path
                            string pythonDir = FindPackagePythonDirectory();
                            RegisterWithClaudeCode(pythonDir);
                            Repaint();
                        }
                    }
                }
                EditorGUILayout.EndHorizontal();
                EditorGUILayout.BeginHorizontal();
            }
            else
            {
                if (GUILayout.Button($"Auto Configure", GUILayout.Height(32)))
                {
                    ConfigureMcpClient(mcpClient);
                }
            }

            if (mcpClient.mcpType != McpTypes.ClaudeCode)
            {
                if (GUILayout.Button("Manual Setup", GUILayout.Height(32)))
                {
                    string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                        ? mcpClient.windowsConfigPath
                        : mcpClient.linuxConfigPath;

                    if (mcpClient.mcpType == McpTypes.VSCode)
                    {
                        string pythonDir = FindPackagePythonDirectory();
                        string uvPath = FindUvPath();
                        if (uvPath == null)
                        {
                            UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode.");
                            return;
                        }
                        // VSCode now reads from mcp.json with a top-level "servers" block
                        var vscodeConfig = new
                        {
                            servers = new
                            {
                                unityMCP = new
                                {
                                    command = uvPath,
                                    args = new[] { "run", "--directory", pythonDir, "server.py" }
                                }
                            }
                        };
                        JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
                        string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
                        VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson);
                    }
                    else
                    {
                        ShowManualInstructionsWindow(configPath, mcpClient);
                    }
                }
            }

            EditorGUILayout.EndHorizontal();

            EditorGUILayout.Space(8);
            // Quick info (hide when Claude is not found to avoid confusion)
            bool hideConfigInfo =
                (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude()))
                || ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath()));
            if (!hideConfigInfo)
            {
                GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel)
                {
                    fontSize = 10
                };
                EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle);
            }
        }

        private void ToggleUnityBridge()
        {
            if (isUnityBridgeRunning)
            {
                MCPForUnityBridge.Stop();
            }
            else
            {
                MCPForUnityBridge.Start();
            }
            // Reflect the actual state post-operation (avoid optimistic toggle)
            isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
            Repaint();
        }

        // New method to show manual instructions without changing status
        private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient)
        {
            // Get the Python directory path using Package Manager API
            string pythonDir = FindPackagePythonDirectory();
            // Build manual JSON centrally using the shared builder
            string uvPathForManual = FindUvPath();
            if (uvPathForManual == null)
            {
                UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration.");
                return;
            }

            string manualConfig = mcpClient?.mcpType == McpTypes.Codex
                ? CodexConfigHelper.BuildCodexServerBlock(uvPathForManual, McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)).TrimEnd() + Environment.NewLine
                : ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient);
            ManualConfigEditorWindow.ShowWindow(configPath, manualConfig, mcpClient);
        }

        private string FindPackagePythonDirectory()
        {
            // Use shared helper for consistent path resolution across both windows
            return McpPathResolver.FindPackagePythonDirectory(debugLogsEnabled);
        }

        private string ConfigureMcpClient(McpClient mcpClient)
        {
            try
            {
                // Use shared helper for consistent config path resolution
                string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient);

                // Create directory if it doesn't exist
                McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);

                // Find the server.py file location using shared helper
                string pythonDir = FindPackagePythonDirectory();

                if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))
                {
                    ShowManualInstructionsWindow(configPath, mcpClient);
                    return "Manual Configuration Required";
                }

                string result = mcpClient.mcpType == McpTypes.Codex
                    ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient)
                    : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient);

                // Update the client status after successful configuration
                if (result == "Configured successfully")
                {
                    mcpClient.SetStatus(McpStatus.Configured);
                }

                return result;
            }
            catch (Exception e)
            {
                // Determine the config file path based on OS for error message
                string configPath = "";
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                {
                    configPath = mcpClient.windowsConfigPath;
                }
                else if (
                    RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
                )
                {
                    configPath = string.IsNullOrEmpty(mcpClient.macConfigPath)
                        ? mcpClient.linuxConfigPath
                        : mcpClient.macConfigPath;
                }
                else if (
                    RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
                )
                {
                    configPath = mcpClient.linuxConfigPath;
                }

                ShowManualInstructionsWindow(configPath, mcpClient);
                UnityEngine.Debug.LogError(
                    $"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}"
                );
                return $"Failed to configure {mcpClient.name}";
            }
        }

        private void LoadValidationLevelSetting()
        {
            string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard");
            validationLevelIndex = savedLevel.ToLower() switch
            {
                "basic" => 0,
                "standard" => 1,
                "comprehensive" => 2,
                "strict" => 3,
                _ => 1 // Default to Standard
            };
        }

        private void SaveValidationLevelSetting()
        {
            string levelString = validationLevelIndex switch
            {
                0 => "basic",
                1 => "standard",
                2 => "comprehensive",
                3 => "strict",
                _ => "standard"
            };
            EditorPrefs.SetString("MCPForUnity_ScriptValidationLevel", levelString);
        }

        private string GetValidationLevelDescription(int index)
        {
            return index switch
            {
                0 => "Only basic syntax checks (braces, quotes, comments)",
                1 => "Syntax checks + Unity best practices and warnings",
                2 => "All checks + semantic analysis and performance warnings",
                3 => "Full semantic validation with namespace/type resolution (requires Roslyn)",
                _ => "Standard validation"
            };
        }

        private void CheckMcpConfiguration(McpClient mcpClient)
        {
            try
            {
                // Special handling for Claude Code
                if (mcpClient.mcpType == McpTypes.ClaudeCode)
                {
                    CheckClaudeCodeConfiguration(mcpClient);
                    return;
                }

                // Use shared helper for consistent config path resolution
                string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient);

                if (!File.Exists(configPath))
                {
                    mcpClient.SetStatus(McpStatus.NotConfigured);
                    return;
                }

                string configJson = File.ReadAllText(configPath);
                // Use the same path resolution as configuration to avoid false "Incorrect Path" in dev mode
                string pythonDir = FindPackagePythonDirectory();

                // Use switch statement to handle different client types, extracting common logic
                string[] args = null;
                bool configExists = false;

                switch (mcpClient.mcpType)
                {
                    case McpTypes.VSCode:
                        dynamic config = JsonConvert.DeserializeObject(configJson);

                        // New schema: top-level servers
                        if (config?.servers?.unityMCP != null)
                        {
                            args = config.servers.unityMCP.args.ToObject<string[]>();
                            configExists = true;
                        }
                        // Back-compat: legacy mcp.servers
                        else if (config?.mcp?.servers?.unityMCP != null)
                        {
                            args = config.mcp.servers.unityMCP.args.ToObject<string[]>();
                            configExists = true;
                        }
                        break;

                    case McpTypes.Codex:
                        if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs))
                        {
                            args = codexArgs;
                            configExists = true;
                        }
                        break;

                    default:
                        // Standard MCP configuration check for Claude Desktop, Cursor, etc.
                        McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);

                        if (standardConfig?.mcpServers?.unityMCP != null)
                        {
                            args = standardConfig.mcpServers.unityMCP.args;
                            configExists = true;
                        }
                        break;
                }

                // Common logic for checking configuration status
                if (configExists)
                {
                    string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args);
                    bool matches = !string.IsNullOrEmpty(configuredDir) && McpConfigFileHelper.PathsEqual(configuredDir, pythonDir);
                    if (matches)
                    {
                        mcpClient.SetStatus(McpStatus.Configured);
                    }
                    else
                    {
                        // Attempt auto-rewrite once if the package path changed
                        try
                        {
                            string rewriteResult = mcpClient.mcpType == McpTypes.Codex
                                ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient)
                                : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient);
                            if (rewriteResult == "Configured successfully")
                            {
                                if (debugLogsEnabled)
                                {
                                    MCPForUnity.Editor.Helpers.McpLog.Info($"Auto-updated MCP config for '{mcpClient.name}' to new path: {pythonDir}", always: false);
                                }
                                mcpClient.SetStatus(McpStatus.Configured);
                            }
                            else
                            {
                                mcpClient.SetStatus(McpStatus.IncorrectPath);
                            }
                        }
                        catch (Exception ex)
                        {
                            mcpClient.SetStatus(McpStatus.IncorrectPath);
                            if (debugLogsEnabled)
                            {
                                UnityEngine.Debug.LogWarning($"MCP for Unity: Auto-config rewrite failed for '{mcpClient.name}': {ex.Message}");
                            }
                        }
                    }
                }
                else
                {
                    mcpClient.SetStatus(McpStatus.MissingConfig);
                }
            }
            catch (Exception e)
            {
                mcpClient.SetStatus(McpStatus.Error, e.Message);
            }
        }

        private void RegisterWithClaudeCode(string pythonDir)
        {
            // Resolve claude and uv; then run register command
            string claudePath = ExecPath.ResolveClaude();
            if (string.IsNullOrEmpty(claudePath))
            {
                UnityEngine.Debug.LogError("MCP for Unity: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
                return;
            }
            string uvPath = ExecPath.ResolveUv() ?? "uv";

            // Prefer embedded/dev path when available
            string srcDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
            if (string.IsNullOrEmpty(srcDir)) srcDir = pythonDir;

            string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{srcDir}\" server.py";

            string projectDir = Path.GetDirectoryName(Application.dataPath);
            // Ensure PATH includes common locations on Unix; on Windows leave PATH as-is
            string pathPrepend = null;
            if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor)
            {
                pathPrepend = Application.platform == RuntimePlatform.OSXEditor
                    ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
                    : "/usr/local/bin:/usr/bin:/bin";
            }
            if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
            {
                string combined = ($"{stdout}\n{stderr}") ?? string.Empty;
                if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    // Treat as success if Claude reports existing registration
                    var existingClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
                    if (existingClient != null) CheckClaudeCodeConfiguration(existingClient);
                    Repaint();
                    UnityEngine.Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCP for Unity already registered with Claude Code.");
                }
                else
                {
                    UnityEngine.Debug.LogError($"MCP for Unity: Failed to start Claude CLI.\n{stderr}\n{stdout}");
                }
                return;
            }

            // Update status
            var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
            if (claudeClient != null) CheckClaudeCodeConfiguration(claudeClient);
            Repaint();
            UnityEngine.Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Registered with Claude Code.");
        }

        private void UnregisterWithClaudeCode()
        {
            string claudePath = ExecPath.ResolveClaude();
            if (string.IsNullOrEmpty(claudePath))
            {
                UnityEngine.Debug.LogError("MCP for Unity: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
                return;
            }

            string projectDir = Path.GetDirectoryName(Application.dataPath);
            string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
                ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
                : null; // On Windows, don't modify PATH - use system PATH as-is

            // Determine if Claude has a "UnityMCP" server registered by using exit codes from `claude mcp get <name>`
            string[] candidateNamesForGet = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
            List<string> existingNames = new List<string>();
            foreach (var candidate in candidateNamesForGet)
            {
                if (ExecPath.TryRun(claudePath, $"mcp get {candidate}", projectDir, out var getStdout, out var getStderr, 7000, pathPrepend))
                {
                    // Success exit code indicates the server exists
                    existingNames.Add(candidate);
                }
            }

            if (existingNames.Count == 0)
            {
                // Nothing to unregister – set status and bail early
                var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
                if (claudeClient != null)
                {
                    claudeClient.SetStatus(McpStatus.NotConfigured);
                    UnityEngine.Debug.Log("Claude CLI reports no MCP for Unity server via 'mcp get' - setting status to NotConfigured and aborting unregister.");
                    Repaint();
                }
                return;
            }

            // Try different possible server names
            string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" };
            bool success = false;

            foreach (string serverName in possibleNames)
            {
                if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
                {
                    success = true;
                    UnityEngine.Debug.Log($"MCP for Unity: Successfully removed MCP server: {serverName}");
                    break;
                }
                else if (!string.IsNullOrEmpty(stderr) &&
                         !stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase))
                {
                    // If it's not a "not found" error, log it and stop trying
                    UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}");
                    break;
                }
            }

            if (success)
            {
                var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
                if (claudeClient != null)
                {
                    // Optimistically flip to NotConfigured; then verify
                    claudeClient.SetStatus(McpStatus.NotConfigured);
                    CheckClaudeCodeConfiguration(claudeClient);
                }
                Repaint();
                UnityEngine.Debug.Log("MCP for Unity: MCP server successfully unregistered from Claude Code.");
            }
            else
            {
                // If no servers were found to remove, they're already unregistered
                // Force status to NotConfigured and update the UI
                UnityEngine.Debug.Log("No MCP servers found to unregister - already unregistered.");
                var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
                if (claudeClient != null)
                {
                    claudeClient.SetStatus(McpStatus.NotConfigured);
                    CheckClaudeCodeConfiguration(claudeClient);
                }
                Repaint();
            }
        }

        // Removed unused ParseTextOutput

        private string FindUvPath()
        {
            try { return MCPForUnity.Editor.Helpers.ServerInstaller.FindUvPath(); } catch { return null; }
        }

        // Validation and platform-specific scanning are handled by ServerInstaller.FindUvPath()

        // Windows-specific discovery removed; use ServerInstaller.FindUvPath() instead

        // Removed unused FindClaudeCommand

        private void CheckClaudeCodeConfiguration(McpClient mcpClient)
        {
            try
            {
                // Get the Unity project directory to check project-specific config
                string unityProjectDir = Application.dataPath;
                string projectDir = Path.GetDirectoryName(unityProjectDir);

                // Read the global Claude config file (honor macConfigPath on macOS)
                string configPath;
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                    configPath = mcpClient.windowsConfigPath;
                else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
                    configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) ? mcpClient.linuxConfigPath : mcpClient.macConfigPath;
                else
                    configPath = mcpClient.linuxConfigPath;

                if (debugLogsEnabled)
                {
                    MCPForUnity.Editor.Helpers.McpLog.Info($"Checking Claude config at: {configPath}", always: false);
                }

                if (!File.Exists(configPath))
                {
                    UnityEngine.Debug.LogWarning($"Claude config file not found at: {configPath}");
                    mcpClient.SetStatus(McpStatus.NotConfigured);
                    return;
                }

                string configJson = File.ReadAllText(configPath);
                dynamic claudeConfig = JsonConvert.DeserializeObject(configJson);

                // Check for "UnityMCP" server in the mcpServers section (current format)
                if (claudeConfig?.mcpServers != null)
                {
                    var servers = claudeConfig.mcpServers;
                    if (servers.UnityMCP != null || servers.unityMCP != null)
                    {
                        // Found MCP for Unity configured
                        mcpClient.SetStatus(McpStatus.Configured);
                        return;
                    }
                }

                // Also check if there's a project-specific configuration for this Unity project (legacy format)
                if (claudeConfig?.projects != null)
                {
                    // Look for the project path in the config
                    foreach (var project in claudeConfig.projects)
                    {
                        string projectPath = project.Name;

                        // Normalize paths for comparison (handle forward/back slash differences)
                        string normalizedProjectPath = Path.GetFullPath(projectPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
                        string normalizedProjectDir = Path.GetFullPath(projectDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

                        if (string.Equals(normalizedProjectPath, normalizedProjectDir, StringComparison.OrdinalIgnoreCase) && project.Value?.mcpServers != null)
                        {
                            // Check for "UnityMCP" (case variations)
                            var servers = project.Value.mcpServers;
                            if (servers.UnityMCP != null || servers.unityMCP != null)
                            {
                                // Found MCP for Unity configured for this project
                                mcpClient.SetStatus(McpStatus.Configured);
                                return;
                            }
                        }
                    }
                }

                // No configuration found for this project
                mcpClient.SetStatus(McpStatus.NotConfigured);
            }
            catch (Exception e)
            {
                UnityEngine.Debug.LogWarning($"Error checking Claude Code config: {e.Message}");
                mcpClient.SetStatus(McpStatus.Error, e.Message);
            }
        }

        private bool IsPythonDetected()
        {
            try
            {
                // Windows-specific Python detection
                if (Application.platform == RuntimePlatform.WindowsEditor)
                {
                    // Common Windows Python installation paths
                    string[] windowsCandidates =
                    {
                        @"C:\Python313\python.exe",
                        @"C:\Python312\python.exe",
                        @"C:\Python311\python.exe",
                        @"C:\Python310\python.exe",
                        @"C:\Python39\python.exe",
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python313\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python312\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python311\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python310\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python313\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python312\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python311\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"),
                        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\python.exe"),
                    };

                    foreach (string c in windowsCandidates)
                    {
                        if (File.Exists(c)) return true;
                    }

                    // Try 'where python' command (Windows equivalent of 'which')
                    var psi = new ProcessStartInfo
                    {
                        FileName = "where",
                        Arguments = "python",
                        UseShellExecute = false,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                        CreateNoWindow = true
                    };
                    using var p = Process.Start(psi);
                    string outp = p.StandardOutput.ReadToEnd().Trim();
                    p.WaitForExit(2000);
                    if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
                    {
                        string[] lines = outp.Split('\n');
                        foreach (string line in lines)
                        {
                            string trimmed = line.Trim();
                            if (File.Exists(trimmed)) return true;
                        }
                    }
                }
                else
                {
                    // macOS/Linux detection (existing code)
                    string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
                    string[] candidates =
                    {
                        "/opt/homebrew/bin/python3",
                        "/usr/local/bin/python3",
                        "/usr/bin/python3",
                        "/opt/local/bin/python3",
                        Path.Combine(home, ".local", "bin", "python3"),
                        "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
                        "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
                    };
                    foreach (string c in candidates)
                    {
                        if (File.Exists(c)) return true;
                    }

                    // Try 'which python3'
                    var psi = new ProcessStartInfo
                    {
                        FileName = "/usr/bin/which",
                        Arguments = "python3",
                        UseShellExecute = false,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                        CreateNoWindow = true
                    };
                    using var p = Process.Start(psi);
                    string outp = p.StandardOutput.ReadToEnd().Trim();
                    p.WaitForExit(2000);
                    if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true;
                }
            }
            catch { }
            return false;
        }
    }
}

```

--------------------------------------------------------------------------------
/MCPForUnity/Editor/External/Tommy.cs:
--------------------------------------------------------------------------------

```csharp
#region LICENSE

/*
 * MIT License
 * 
 * Copyright (c) 2020 Denis Zhidkikh
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace MCPForUnity.External.Tommy
{
    #region TOML Nodes

    public abstract class TomlNode : IEnumerable
    {
        public virtual bool HasValue { get; } = false;
        public virtual bool IsArray { get; } = false;
        public virtual bool IsTable { get; } = false;
        public virtual bool IsString { get; } = false;
        public virtual bool IsInteger { get; } = false;
        public virtual bool IsFloat { get; } = false;
        public bool IsDateTime => IsDateTimeLocal || IsDateTimeOffset;
        public virtual bool IsDateTimeLocal { get; } = false;
        public virtual bool IsDateTimeOffset { get; } = false;
        public virtual bool IsBoolean { get; } = false;
        public virtual string Comment { get; set; }
        public virtual int CollapseLevel { get; set; }

        public virtual TomlTable AsTable => this as TomlTable;
        public virtual TomlString AsString => this as TomlString;
        public virtual TomlInteger AsInteger => this as TomlInteger;
        public virtual TomlFloat AsFloat => this as TomlFloat;
        public virtual TomlBoolean AsBoolean => this as TomlBoolean;
        public virtual TomlDateTimeLocal AsDateTimeLocal => this as TomlDateTimeLocal;
        public virtual TomlDateTimeOffset AsDateTimeOffset => this as TomlDateTimeOffset;
        public virtual TomlDateTime AsDateTime => this as TomlDateTime;
        public virtual TomlArray AsArray => this as TomlArray;

        public virtual int ChildrenCount => 0;

        public virtual TomlNode this[string key]
        {
            get => null;
            set { }
        }

        public virtual TomlNode this[int index]
        {
            get => null;
            set { }
        }

        public virtual IEnumerable<TomlNode> Children
        {
            get { yield break; }
        }

        public virtual IEnumerable<string> Keys
        {
            get { yield break; }
        }

        public IEnumerator GetEnumerator() => Children.GetEnumerator();

        public virtual bool TryGetNode(string key, out TomlNode node)
        {
            node = null;
            return false;
        }

        public virtual bool HasKey(string key) => false;

        public virtual bool HasItemAt(int index) => false;

        public virtual void Add(string key, TomlNode node) { }

        public virtual void Add(TomlNode node) { }

        public virtual void Delete(TomlNode node) { }

        public virtual void Delete(string key) { }

        public virtual void Delete(int index) { }

        public virtual void AddRange(IEnumerable<TomlNode> nodes)
        {
            foreach (var tomlNode in nodes) Add(tomlNode);
        }

        public virtual void WriteTo(TextWriter tw, string name = null) => tw.WriteLine(ToInlineToml());

        public virtual string ToInlineToml() => ToString();

        #region Native type to TOML cast

        public static implicit operator TomlNode(string value) => new TomlString { Value = value };

        public static implicit operator TomlNode(bool value) => new TomlBoolean { Value = value };

        public static implicit operator TomlNode(long value) => new TomlInteger { Value = value };

        public static implicit operator TomlNode(float value) => new TomlFloat { Value = value };

        public static implicit operator TomlNode(double value) => new TomlFloat { Value = value };

        public static implicit operator TomlNode(DateTime value) => new TomlDateTimeLocal { Value = value };

        public static implicit operator TomlNode(DateTimeOffset value) => new TomlDateTimeOffset { Value = value };

        public static implicit operator TomlNode(TomlNode[] nodes)
        {
            var result = new TomlArray();
            result.AddRange(nodes);
            return result;
        }

        #endregion

        #region TOML to native type cast

        public static implicit operator string(TomlNode value) => value.ToString();

        public static implicit operator int(TomlNode value) => (int)value.AsInteger.Value;

        public static implicit operator long(TomlNode value) => value.AsInteger.Value;

        public static implicit operator float(TomlNode value) => (float)value.AsFloat.Value;

        public static implicit operator double(TomlNode value) => value.AsFloat.Value;

        public static implicit operator bool(TomlNode value) => value.AsBoolean.Value;

        public static implicit operator DateTime(TomlNode value) => value.AsDateTimeLocal.Value;

        public static implicit operator DateTimeOffset(TomlNode value) => value.AsDateTimeOffset.Value;

        #endregion
    }

    public class TomlString : TomlNode
    {
        public override bool HasValue { get; } = true;
        public override bool IsString { get; } = true;
        public bool IsMultiline { get; set; }
        public bool MultilineTrimFirstLine { get; set; }
        public bool PreferLiteral { get; set; }

        public string Value { get; set; }

        public override string ToString() => Value;

        public override string ToInlineToml()
        {
            // Automatically convert literal to non-literal if there are too many literal string symbols
            if (Value.IndexOf(new string(TomlSyntax.LITERAL_STRING_SYMBOL, IsMultiline ? 3 : 1), StringComparison.Ordinal) != -1 && PreferLiteral) PreferLiteral = false;
            var quotes = new string(PreferLiteral ? TomlSyntax.LITERAL_STRING_SYMBOL : TomlSyntax.BASIC_STRING_SYMBOL,
                                    IsMultiline ? 3 : 1);
            var result = PreferLiteral ? Value : Value.Escape(!IsMultiline);
            if (IsMultiline)
                result = result.Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
            if (IsMultiline && (MultilineTrimFirstLine || !MultilineTrimFirstLine && result.StartsWith(Environment.NewLine)))
                result = $"{Environment.NewLine}{result}";
            return $"{quotes}{result}{quotes}";
        }
    }

    public class TomlInteger : TomlNode
    {
        public enum Base
        {
            Binary = 2,
            Octal = 8,
            Decimal = 10,
            Hexadecimal = 16
        }

        public override bool IsInteger { get; } = true;
        public override bool HasValue { get; } = true;
        public Base IntegerBase { get; set; } = Base.Decimal;

        public long Value { get; set; }

        public override string ToString() => Value.ToString();

        public override string ToInlineToml() =>
            IntegerBase != Base.Decimal
                ? $"0{TomlSyntax.BaseIdentifiers[(int)IntegerBase]}{Convert.ToString(Value, (int)IntegerBase)}"
                : Value.ToString(CultureInfo.InvariantCulture);
    }

    public class TomlFloat : TomlNode, IFormattable
    {
        public override bool IsFloat { get; } = true;
        public override bool HasValue { get; } = true;

        public double Value { get; set; }

        public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);

        public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider);

        public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);

        public override string ToInlineToml() =>
            Value switch
            {
                var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE,
                var v when double.IsPositiveInfinity(v) => TomlSyntax.INF_VALUE,
                var v when double.IsNegativeInfinity(v) => TomlSyntax.NEG_INF_VALUE,
                var v => v.ToString("G", CultureInfo.InvariantCulture).ToLowerInvariant()
            };
    }

    public class TomlBoolean : TomlNode
    {
        public override bool IsBoolean { get; } = true;
        public override bool HasValue { get; } = true;

        public bool Value { get; set; }

        public override string ToString() => Value.ToString();

        public override string ToInlineToml() => Value ? TomlSyntax.TRUE_VALUE : TomlSyntax.FALSE_VALUE;
    }

    public class TomlDateTime : TomlNode, IFormattable
    {
        public int SecondsPrecision { get; set; }
        public override bool HasValue { get; } = true;
        public virtual string ToString(string format, IFormatProvider formatProvider) => string.Empty;
        public virtual string ToString(IFormatProvider formatProvider) => string.Empty;
        protected virtual string ToInlineTomlInternal() => string.Empty;

        public override string ToInlineToml() => ToInlineTomlInternal()
                                                .Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator)
                                                .Replace(TomlSyntax.ISO861ZeroZone, TomlSyntax.RFC3339ZeroZone);
    }

    public class TomlDateTimeOffset : TomlDateTime
    {
        public override bool IsDateTimeOffset { get; } = true;
        public DateTimeOffset Value { get; set; }

        public override string ToString() => Value.ToString(CultureInfo.CurrentCulture);
        public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);

        public override string ToString(string format, IFormatProvider formatProvider) =>
            Value.ToString(format, formatProvider);

        protected override string ToInlineTomlInternal() => Value.ToString(TomlSyntax.RFC3339Formats[SecondsPrecision]);
    }

    public class TomlDateTimeLocal : TomlDateTime
    {
        public enum DateTimeStyle
        {
            Date,
            Time,
            DateTime
        }

        public override bool IsDateTimeLocal { get; } = true;
        public DateTimeStyle Style { get; set; } = DateTimeStyle.DateTime;
        public DateTime Value { get; set; }

        public override string ToString() => Value.ToString(CultureInfo.CurrentCulture);

        public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);

        public override string ToString(string format, IFormatProvider formatProvider) =>
            Value.ToString(format, formatProvider);

        public override string ToInlineToml() =>
            Style switch
            {
                DateTimeStyle.Date => Value.ToString(TomlSyntax.LocalDateFormat),
                DateTimeStyle.Time => Value.ToString(TomlSyntax.RFC3339LocalTimeFormats[SecondsPrecision]),
                var _ => Value.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision])
            };
    }

    public class TomlArray : TomlNode
    {
        private List<TomlNode> values;

        public override bool HasValue { get; } = true;
        public override bool IsArray { get; } = true;
        public bool IsMultiline { get; set; }
        public bool IsTableArray { get; set; }
        public List<TomlNode> RawArray => values ??= new List<TomlNode>();

        public override TomlNode this[int index]
        {
            get
            {
                if (index < RawArray.Count) return RawArray[index];
                var lazy = new TomlLazy(this);
                this[index] = lazy;
                return lazy;
            }
            set
            {
                if (index == RawArray.Count)
                    RawArray.Add(value);
                else
                    RawArray[index] = value;
            }
        }

        public override int ChildrenCount => RawArray.Count;

        public override IEnumerable<TomlNode> Children => RawArray.AsEnumerable();

        public override void Add(TomlNode node) => RawArray.Add(node);

        public override void AddRange(IEnumerable<TomlNode> nodes) => RawArray.AddRange(nodes);

        public override void Delete(TomlNode node) => RawArray.Remove(node);

        public override void Delete(int index) => RawArray.RemoveAt(index);

        public override string ToString() => ToString(false);

        public string ToString(bool multiline)
        {
            var sb = new StringBuilder();
            sb.Append(TomlSyntax.ARRAY_START_SYMBOL);
            if (ChildrenCount != 0)
            {
                var arrayStart = multiline ? $"{Environment.NewLine}  " : " ";
                var arraySeparator = multiline ? $"{TomlSyntax.ITEM_SEPARATOR}{Environment.NewLine}  " : $"{TomlSyntax.ITEM_SEPARATOR} ";
                var arrayEnd = multiline ? Environment.NewLine : " ";
                sb.Append(arrayStart)
                  .Append(arraySeparator.Join(RawArray.Select(n => n.ToInlineToml())))
                  .Append(arrayEnd);
            }
            sb.Append(TomlSyntax.ARRAY_END_SYMBOL);
            return sb.ToString();
        }

        public override void WriteTo(TextWriter tw, string name = null)
        {
            // If it's a normal array, write it as usual
            if (!IsTableArray)
            {
                tw.WriteLine(ToString(IsMultiline));
                return;
            }

            if (!(Comment is null))
            {
                tw.WriteLine();
                Comment.AsComment(tw);
            }
            tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
            tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
            tw.Write(name);
            tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
            tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
            tw.WriteLine();

            var first = true;

            foreach (var tomlNode in RawArray)
            {
                if (!(tomlNode is TomlTable tbl))
                    throw new TomlFormatException("The array is marked as array table but contains non-table nodes!");

                // Ensure it's parsed as a section
                tbl.IsInline = false;

                if (!first)
                {
                    tw.WriteLine();

                    Comment?.AsComment(tw);
                    tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
                    tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
                    tw.Write(name);
                    tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
                    tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
                    tw.WriteLine();
                }

                first = false;

                // Don't write section since it's already written here
                tbl.WriteTo(tw, name, false);
            }
        }
    }

    public class TomlTable : TomlNode
    {
        private Dictionary<string, TomlNode> children;
        internal bool isImplicit;

        public override bool HasValue { get; } = false;
        public override bool IsTable { get; } = true;
        public bool IsInline { get; set; }
        public Dictionary<string, TomlNode> RawTable => children ??= new Dictionary<string, TomlNode>();

        public override TomlNode this[string key]
        {
            get
            {
                if (RawTable.TryGetValue(key, out var result)) return result;
                var lazy = new TomlLazy(this);
                RawTable[key] = lazy;
                return lazy;
            }
            set => RawTable[key] = value;
        }

        public override int ChildrenCount => RawTable.Count;
        public override IEnumerable<TomlNode> Children => RawTable.Select(kv => kv.Value);
        public override IEnumerable<string> Keys => RawTable.Select(kv => kv.Key);
        public override bool HasKey(string key) => RawTable.ContainsKey(key);
        public override void Add(string key, TomlNode node) => RawTable.Add(key, node);
        public override bool TryGetNode(string key, out TomlNode node) => RawTable.TryGetValue(key, out node);
        public override void Delete(TomlNode node) => RawTable.Remove(RawTable.First(kv => kv.Value == node).Key);
        public override void Delete(string key) => RawTable.Remove(key);

        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.Append(TomlSyntax.INLINE_TABLE_START_SYMBOL);

            if (ChildrenCount != 0)
            {
                var collapsed = CollectCollapsedItems(normalizeOrder: false);

                if (collapsed.Count != 0)
                    sb.Append(' ')
                      .Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(collapsed.Select(n =>
                                                                       $"{n.Key} {TomlSyntax.KEY_VALUE_SEPARATOR} {n.Value.ToInlineToml()}")));
                sb.Append(' ');
            }

            sb.Append(TomlSyntax.INLINE_TABLE_END_SYMBOL);
            return sb.ToString();
        }

        private LinkedList<KeyValuePair<string, TomlNode>> CollectCollapsedItems(string prefix = "", int level = 0, bool normalizeOrder = true)
        {
            var nodes = new LinkedList<KeyValuePair<string, TomlNode>>();
            var postNodes = normalizeOrder ? new LinkedList<KeyValuePair<string, TomlNode>>() : nodes;

            foreach (var keyValuePair in RawTable)
            {
                var node = keyValuePair.Value;
                var key = keyValuePair.Key.AsKey();

                if (node is TomlTable tbl)
                {
                    var subnodes = tbl.CollectCollapsedItems($"{prefix}{key}.", level + 1, normalizeOrder);
                    // Write main table first before writing collapsed items
                    if (subnodes.Count == 0 && node.CollapseLevel == level)
                    {
                        postNodes.AddLast(new KeyValuePair<string, TomlNode>($"{prefix}{key}", node));
                    }
                    foreach (var kv in subnodes)
                        postNodes.AddLast(kv);
                }
                else if (node.CollapseLevel == level)
                    nodes.AddLast(new KeyValuePair<string, TomlNode>($"{prefix}{key}", node));
            }

            if (normalizeOrder)
                foreach (var kv in postNodes)
                    nodes.AddLast(kv);

            return nodes;
        }

        public override void WriteTo(TextWriter tw, string name = null) => WriteTo(tw, name, true);

        internal void WriteTo(TextWriter tw, string name, bool writeSectionName)
        {
            // The table is inline table
            if (IsInline && name != null)
            {
                tw.WriteLine(ToInlineToml());
                return;
            }

            var collapsedItems = CollectCollapsedItems();

            if (collapsedItems.Count == 0)
                return;

            var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } or TomlArray { IsTableArray: true });

            Comment?.AsComment(tw);

            if (name != null && (hasRealValues || Comment != null) && writeSectionName)
            {
                tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
                tw.Write(name);
                tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
                tw.WriteLine();
            }
            else if (Comment != null) // Add some spacing between the first node and the comment
            {
                tw.WriteLine();
            }

            var namePrefix = name == null ? "" : $"{name}.";
            var first = true;

            foreach (var collapsedItem in collapsedItems)
            {
                var key = collapsedItem.Key;
                if (collapsedItem.Value is TomlArray { IsTableArray: true } or TomlTable { IsInline: false })
                {
                    if (!first) tw.WriteLine();
                    first = false;
                    collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}");
                    continue;
                }
                first = false;

                collapsedItem.Value.Comment?.AsComment(tw);
                tw.Write(key);
                tw.Write(' ');
                tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR);
                tw.Write(' ');

                collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}");
            }
        }
    }

    internal class TomlLazy : TomlNode
    {
        private readonly TomlNode parent;
        private TomlNode replacement;

        public TomlLazy(TomlNode parent) => this.parent = parent;

        public override TomlNode this[int index]
        {
            get => Set<TomlArray>()[index];
            set => Set<TomlArray>()[index] = value;
        }

        public override TomlNode this[string key]
        {
            get => Set<TomlTable>()[key];
            set => Set<TomlTable>()[key] = value;
        }

        public override void Add(TomlNode node) => Set<TomlArray>().Add(node);

        public override void Add(string key, TomlNode node) => Set<TomlTable>().Add(key, node);

        public override void AddRange(IEnumerable<TomlNode> nodes) => Set<TomlArray>().AddRange(nodes);

        private TomlNode Set<T>() where T : TomlNode, new()
        {
            if (replacement != null) return replacement;

            var newNode = new T
            {
                Comment = Comment
            };

            if (parent.IsTable)
            {
                var key = parent.Keys.FirstOrDefault(s => parent.TryGetNode(s, out var node) && node.Equals(this));
                if (key == null) return default(T);

                parent[key] = newNode;
            }
            else if (parent.IsArray)
            {
                var index = parent.Children.TakeWhile(child => child != this).Count();
                if (index == parent.ChildrenCount) return default(T);
                parent[index] = newNode;
            }
            else
            {
                return default(T);
            }

            replacement = newNode;
            return newNode;
        }
    }

    #endregion

    #region Parser

    public class TOMLParser : IDisposable
    {
        public enum ParseState
        {
            None,
            KeyValuePair,
            SkipToNextLine,
            Table
        }

        private readonly TextReader reader;
        private ParseState currentState;
        private int line, col;
        private List<TomlSyntaxException> syntaxErrors;

        public TOMLParser(TextReader reader)
        {
            this.reader = reader;
            line = col = 0;
        }

        public bool ForceASCII { get; set; }

        public void Dispose() => reader?.Dispose();

        public TomlTable Parse()
        {
            syntaxErrors = new List<TomlSyntaxException>();
            line = col = 1;
            var rootNode = new TomlTable();
            var currentNode = rootNode;
            currentState = ParseState.None;
            var keyParts = new List<string>();
            var arrayTable = false;
            StringBuilder latestComment = null;
            var firstComment = true;

            int currentChar;
            while ((currentChar = reader.Peek()) >= 0)
            {
                var c = (char)currentChar;

                if (currentState == ParseState.None)
                {
                    // Skip white space
                    if (TomlSyntax.IsWhiteSpace(c)) goto consume_character;

                    if (TomlSyntax.IsNewLine(c))
                    {
                        // Check if there are any comments and so far no items being declared
                        if (latestComment != null && firstComment)
                        {
                            rootNode.Comment = latestComment.ToString().TrimEnd();
                            latestComment = null;
                            firstComment = false;
                        }

                        if (TomlSyntax.IsLineBreak(c))
                            AdvanceLine();

                        goto consume_character;
                    }

                    // Start of a comment; ignore until newline
                    if (c == TomlSyntax.COMMENT_SYMBOL)
                    {
                        latestComment ??= new StringBuilder();
                        latestComment.AppendLine(ParseComment());
                        AdvanceLine(1);
                        continue;
                    }

                    // Encountered a non-comment value. The comment must belong to it (ignore possible newlines)!
                    firstComment = false;

                    if (c == TomlSyntax.TABLE_START_SYMBOL)
                    {
                        currentState = ParseState.Table;
                        goto consume_character;
                    }

                    if (TomlSyntax.IsBareKey(c) || TomlSyntax.IsQuoted(c))
                    {
                        currentState = ParseState.KeyValuePair;
                    }
                    else
                    {
                        AddError($"Unexpected character \"{c}\"");
                        continue;
                    }
                }

                if (currentState == ParseState.KeyValuePair)
                {
                    var keyValuePair = ReadKeyValuePair(keyParts);

                    if (keyValuePair == null)
                    {
                        latestComment = null;
                        keyParts.Clear();

                        if (currentState != ParseState.None)
                            AddError("Failed to parse key-value pair!");
                        continue;
                    }

                    keyValuePair.Comment = latestComment?.ToString()?.TrimEnd();
                    var inserted = InsertNode(keyValuePair, currentNode, keyParts);
                    latestComment = null;
                    keyParts.Clear();
                    if (inserted)
                        currentState = ParseState.SkipToNextLine;
                    continue;
                }

                if (currentState == ParseState.Table)
                {
                    if (keyParts.Count == 0)
                    {
                        // We have array table
                        if (c == TomlSyntax.TABLE_START_SYMBOL)
                        {
                            // Consume the character
                            ConsumeChar();
                            arrayTable = true;
                        }

                        if (!ReadKeyName(ref keyParts, TomlSyntax.TABLE_END_SYMBOL))
                        {
                            keyParts.Clear();
                            continue;
                        }

                        if (keyParts.Count == 0)
                        {
                            AddError("Table name is emtpy.");
                            arrayTable = false;
                            latestComment = null;
                            keyParts.Clear();
                        }

                        continue;
                    }

                    if (c == TomlSyntax.TABLE_END_SYMBOL)
                    {
                        if (arrayTable)
                        {
                            // Consume the ending bracket so we can peek the next character
                            ConsumeChar();
                            var nextChar = reader.Peek();
                            if (nextChar < 0 || (char)nextChar != TomlSyntax.TABLE_END_SYMBOL)
                            {
                                AddError($"Array table {".".Join(keyParts)} has only one closing bracket.");
                                keyParts.Clear();
                                arrayTable = false;
                                latestComment = null;
                                continue;
                            }
                        }

                        currentNode = CreateTable(rootNode, keyParts, arrayTable);
                        if (currentNode != null)
                        {
                            currentNode.IsInline = false;
                            currentNode.Comment = latestComment?.ToString()?.TrimEnd();
                        }

                        keyParts.Clear();
                        arrayTable = false;
                        latestComment = null;

                        if (currentNode == null)
                        {
                            if (currentState != ParseState.None)
                                AddError("Error creating table array!");
                            // Reset a node to root in order to try and continue parsing
                            currentNode = rootNode;
                            continue;
                        }

                        currentState = ParseState.SkipToNextLine;
                        goto consume_character;
                    }

                    if (keyParts.Count != 0)
                    {
                        AddError($"Unexpected character \"{c}\"");
                        keyParts.Clear();
                        arrayTable = false;
                        latestComment = null;
                    }
                }

                if (currentState == ParseState.SkipToNextLine)
                {
                    if (TomlSyntax.IsWhiteSpace(c) || c == TomlSyntax.NEWLINE_CARRIAGE_RETURN_CHARACTER)
                        goto consume_character;

                    if (c is TomlSyntax.COMMENT_SYMBOL or TomlSyntax.NEWLINE_CHARACTER)
                    {
                        currentState = ParseState.None;
                        AdvanceLine();

                        if (c == TomlSyntax.COMMENT_SYMBOL)
                        {
                            col++;
                            ParseComment();
                            continue;
                        }

                        goto consume_character;
                    }

                    AddError($"Unexpected character \"{c}\" at the end of the line.");
                }

            consume_character:
                reader.Read();
                col++;
            }

            if (currentState != ParseState.None && currentState != ParseState.SkipToNextLine)
                AddError("Unexpected end of file!");

            if (syntaxErrors.Count > 0)
                throw new TomlParseException(rootNode, syntaxErrors);

            return rootNode;
        }

        private bool AddError(string message, bool skipLine = true)
        {
            syntaxErrors.Add(new TomlSyntaxException(message, currentState, line, col));
            // Skip the whole line in hope that it was only a single faulty value (and non-multiline one at that)
            if (skipLine)
            {
                reader.ReadLine();
                AdvanceLine(1);
            }
            currentState = ParseState.None;
            return false;
        }

        private void AdvanceLine(int startCol = 0)
        {
            line++;
            col = startCol;
        }

        private int ConsumeChar()
        {
            col++;
            return reader.Read();
        }

        #region Key-Value pair parsing

        /**
         * Reads a single key-value pair.
         * Assumes the cursor is at the first character that belong to the pair (including possible whitespace).
         * Consumes all characters that belong to the key and the value (ignoring possible trailing whitespace at the end).
         * 
         * Example:
         * foo = "bar"  ==> foo = "bar"
         * ^                           ^
         */
        private TomlNode ReadKeyValuePair(List<string> keyParts)
        {
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                if (TomlSyntax.IsQuoted(c) || TomlSyntax.IsBareKey(c))
                {
                    if (keyParts.Count != 0)
                    {
                        AddError("Encountered extra characters in key definition!");
                        return null;
                    }

                    if (!ReadKeyName(ref keyParts, TomlSyntax.KEY_VALUE_SEPARATOR))
                        return null;

                    continue;
                }

                if (TomlSyntax.IsWhiteSpace(c))
                {
                    ConsumeChar();
                    continue;
                }

                if (c == TomlSyntax.KEY_VALUE_SEPARATOR)
                {
                    ConsumeChar();
                    return ReadValue();
                }

                AddError($"Unexpected character \"{c}\" in key name.");
                return null;
            }

            return null;
        }

        /**
         * Reads a single value.
         * Assumes the cursor is at the first character that belongs to the value (including possible starting whitespace).
         * Consumes all characters belonging to the value (ignoring possible trailing whitespace at the end).
         * 
         * Example:
         * "test"  ==> "test"
         * ^                 ^
         */
        private TomlNode ReadValue(bool skipNewlines = false)
        {
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                if (TomlSyntax.IsWhiteSpace(c))
                {
                    ConsumeChar();
                    continue;
                }

                if (c == TomlSyntax.COMMENT_SYMBOL)
                {
                    AddError("No value found!");
                    return null;
                }

                if (TomlSyntax.IsNewLine(c))
                {
                    if (skipNewlines)
                    {
                        reader.Read();
                        AdvanceLine(1);
                        continue;
                    }

                    AddError("Encountered a newline when expecting a value!");
                    return null;
                }

                if (TomlSyntax.IsQuoted(c))
                {
                    var isMultiline = IsTripleQuote(c, out var excess);

                    // Error occurred in triple quote parsing
                    if (currentState == ParseState.None)
                        return null;

                    var value = isMultiline
                        ? ReadQuotedValueMultiLine(c)
                        : ReadQuotedValueSingleLine(c, excess);

                    if (value is null)
                        return null;

                    return new TomlString
                    {
                        Value = value,
                        IsMultiline = isMultiline,
                        PreferLiteral = c == TomlSyntax.LITERAL_STRING_SYMBOL
                    };
                }

                return c switch
                {
                    TomlSyntax.INLINE_TABLE_START_SYMBOL => ReadInlineTable(),
                    TomlSyntax.ARRAY_START_SYMBOL => ReadArray(),
                    var _ => ReadTomlValue()
                };
            }

            return null;
        }

        /**
         * Reads a single key name.
         * Assumes the cursor is at the first character belonging to the key (with possible trailing whitespace if `skipWhitespace = true`).
         * Consumes all the characters until the `until` character is met (but does not consume the character itself).
         * 
         * Example 1:
         * foo.bar  ==>  foo.bar           (`skipWhitespace = false`, `until = ' '`)
         * ^                    ^
         * 
         * Example 2:
         * [ foo . bar ] ==>  [ foo . bar ]     (`skipWhitespace = true`, `until = ']'`)
         * ^                             ^
         */
        private bool ReadKeyName(ref List<string> parts, char until)
        {
            var buffer = new StringBuilder();
            var quoted = false;
            var prevWasSpace = false;
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                // Reached the final character
                if (c == until) break;

                if (TomlSyntax.IsWhiteSpace(c))
                {
                    prevWasSpace = true;
                    goto consume_character;
                }

                if (buffer.Length == 0) prevWasSpace = false;

                if (c == TomlSyntax.SUBKEY_SEPARATOR)
                {
                    if (buffer.Length == 0 && !quoted)
                        return AddError($"Found an extra subkey separator in {".".Join(parts)}...");

                    parts.Add(buffer.ToString());
                    buffer.Length = 0;
                    quoted = false;
                    prevWasSpace = false;
                    goto consume_character;
                }

                if (prevWasSpace)
                    return AddError("Invalid spacing in key name");

                if (TomlSyntax.IsQuoted(c))
                {
                    if (quoted)

                        return AddError("Expected a subkey separator but got extra data instead!");

                    if (buffer.Length != 0)
                        return AddError("Encountered a quote in the middle of subkey name!");

                    // Consume the quote character and read the key name
                    col++;
                    buffer.Append(ReadQuotedValueSingleLine((char)reader.Read()));
                    quoted = true;
                    continue;
                }

                if (TomlSyntax.IsBareKey(c))
                {
                    buffer.Append(c);
                    goto consume_character;
                }

                // If we see an invalid symbol, let the next parser handle it
                break;

            consume_character:
                reader.Read();
                col++;
            }

            if (buffer.Length == 0 && !quoted)
                return AddError($"Found an extra subkey separator in {".".Join(parts)}...");

            parts.Add(buffer.ToString());

            return true;
        }

        #endregion

        #region Non-string value parsing

        /**
         * Reads the whole raw value until the first non-value character is encountered.
         * Assumes the cursor start position at the first value character and consumes all characters that may be related to the value.
         * Example:
         * 
         * 1_0_0_0  ==>  1_0_0_0
         * ^                    ^
         */
        private string ReadRawValue()
        {
            var result = new StringBuilder();
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;
                if (c == TomlSyntax.COMMENT_SYMBOL || TomlSyntax.IsNewLine(c) || TomlSyntax.IsValueSeparator(c)) break;
                result.Append(c);
                ConsumeChar();
            }

            // Replace trim with manual space counting?
            return result.ToString().Trim();
        }

        /**
         * Reads and parses a non-string, non-composite TOML value.
         * Assumes the cursor at the first character that is related to the value (with possible spaces).
         * Consumes all the characters that are related to the value.
         * 
         * Example
         * 1_0_0_0 # This is a comment
         * <newline>
         *     ==>  1_0_0_0 # This is a comment
         *     ^                                                  ^
         */
        private TomlNode ReadTomlValue()
        {
            var value = ReadRawValue();
            TomlNode node = value switch
            {
                var v when TomlSyntax.IsBoolean(v) => bool.Parse(v),
                var v when TomlSyntax.IsNaN(v) => double.NaN,
                var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity,
                var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity,
                var v when TomlSyntax.IsInteger(v) => long.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR),
                                                                 CultureInfo.InvariantCulture),
                var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR),
                                                                 CultureInfo.InvariantCulture),
                var v when TomlSyntax.IsIntegerWithBase(v, out var numberBase) => new TomlInteger
                {
                    Value = Convert.ToInt64(value.Substring(2).RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), numberBase),
                    IntegerBase = (TomlInteger.Base)numberBase
                },
                var _ => null
            };
            if (node != null) return node;

            // Normalize by removing space separator
            value = value.Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator);
            if (StringUtils.TryParseDateTime<DateTime>(value,
                                             TomlSyntax.RFC3339LocalDateTimeFormats,
                                             DateTimeStyles.AssumeLocal,
                                             DateTime.TryParseExact,
                                             out var dateTimeResult,
                                             out var precision))
                return new TomlDateTimeLocal
                {
                    Value = dateTimeResult,
                    SecondsPrecision = precision
                };

            if (DateTime.TryParseExact(value,
                                       TomlSyntax.LocalDateFormat,
                                       CultureInfo.InvariantCulture,
                                       DateTimeStyles.AssumeLocal,
                                       out dateTimeResult))
                return new TomlDateTimeLocal
                {
                    Value = dateTimeResult,
                    Style = TomlDateTimeLocal.DateTimeStyle.Date
                };

            if (StringUtils.TryParseDateTime(value,
                                             TomlSyntax.RFC3339LocalTimeFormats,
                                             DateTimeStyles.AssumeLocal,
                                             DateTime.TryParseExact,
                                             out dateTimeResult,
                                             out precision))
                return new TomlDateTimeLocal
                {
                    Value = dateTimeResult,
                    Style = TomlDateTimeLocal.DateTimeStyle.Time,
                    SecondsPrecision = precision
                };

            if (StringUtils.TryParseDateTime<DateTimeOffset>(value,
                                                             TomlSyntax.RFC3339Formats,
                                                             DateTimeStyles.None,
                                                             DateTimeOffset.TryParseExact,
                                                             out var dateTimeOffsetResult,
                                                             out precision))
                return new TomlDateTimeOffset
                {
                    Value = dateTimeOffsetResult,
                    SecondsPrecision = precision
                };

            AddError($"Value \"{value}\" is not a valid TOML value!");
            return null;
        }

        /**
         * Reads an array value.
         * Assumes the cursor is at the start of the array definition. Reads all character until the array closing bracket.
         * 
         * Example:
         * [1, 2, 3]  ==>  [1, 2, 3]
         * ^                        ^
         */
        private TomlArray ReadArray()
        {
            // Consume the start of array character
            ConsumeChar();
            var result = new TomlArray();
            TomlNode currentValue = null;
            var expectValue = true;

            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                if (c == TomlSyntax.ARRAY_END_SYMBOL)
                {
                    ConsumeChar();
                    break;
                }

                if (c == TomlSyntax.COMMENT_SYMBOL)
                {
                    reader.ReadLine();
                    AdvanceLine(1);
                    continue;
                }

                if (TomlSyntax.IsWhiteSpace(c) || TomlSyntax.IsNewLine(c))
                {
                    if (TomlSyntax.IsLineBreak(c))
                        AdvanceLine();
                    goto consume_character;
                }

                if (c == TomlSyntax.ITEM_SEPARATOR)
                {
                    if (currentValue == null)
                    {
                        AddError("Encountered multiple value separators");
                        return null;
                    }

                    result.Add(currentValue);
                    currentValue = null;
                    expectValue = true;
                    goto consume_character;
                }

                if (!expectValue)
                {
                    AddError("Missing separator between values");
                    return null;
                }
                currentValue = ReadValue(true);
                if (currentValue == null)
                {
                    if (currentState != ParseState.None)
                        AddError("Failed to determine and parse a value!");
                    return null;
                }
                expectValue = false;

                continue;
            consume_character:
                ConsumeChar();
            }

            if (currentValue != null) result.Add(currentValue);
            return result;
        }

        /**
         * Reads an inline table.
         * Assumes the cursor is at the start of the table definition. Reads all character until the table closing bracket.
         * 
         * Example:
         * { test = "foo", value = 1 }  ==>  { test = "foo", value = 1 }
         * ^                                                            ^
         */
        private TomlNode ReadInlineTable()
        {
            ConsumeChar();
            var result = new TomlTable { IsInline = true };
            TomlNode currentValue = null;
            var separator = false;
            var keyParts = new List<string>();
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                if (c == TomlSyntax.INLINE_TABLE_END_SYMBOL)
                {
                    ConsumeChar();
                    break;
                }

                if (c == TomlSyntax.COMMENT_SYMBOL)
                {
                    AddError("Incomplete inline table definition!");
                    return null;
                }

                if (TomlSyntax.IsNewLine(c))
                {
                    AddError("Inline tables are only allowed to be on single line");
                    return null;
                }

                if (TomlSyntax.IsWhiteSpace(c))
                    goto consume_character;

                if (c == TomlSyntax.ITEM_SEPARATOR)
                {
                    if (currentValue == null)
                    {
                        AddError("Encountered multiple value separators in inline table!");
                        return null;
                    }

                    if (!InsertNode(currentValue, result, keyParts))
                        return null;
                    keyParts.Clear();
                    currentValue = null;
                    separator = true;
                    goto consume_character;
                }

                separator = false;
                currentValue = ReadKeyValuePair(keyParts);
                continue;

            consume_character:
                ConsumeChar();
            }

            if (separator)
            {
                AddError("Trailing commas are not allowed in inline tables.");
                return null;
            }

            if (currentValue != null && !InsertNode(currentValue, result, keyParts))
                return null;

            return result;
        }

        #endregion

        #region String parsing

        /**
         * Checks if the string value a multiline string (i.e. a triple quoted string).
         * Assumes the cursor is at the first quote character. Consumes the least amount of characters needed to determine if the string is multiline.
         * 
         * If the result is false, returns the consumed character through the `excess` variable.
         * 
         * Example 1:
         * """test"""  ==>  """test"""
         * ^                   ^
         * 
         * Example 2:
         * "test"  ==>  "test"         (doesn't return the first quote)
         * ^             ^
         * 
         * Example 3:
         * ""  ==>  ""        (returns the extra `"` through the `excess` variable)
         * ^          ^
         */
        private bool IsTripleQuote(char quote, out char excess)
        {
            // Copypasta, but it's faster...

            int cur;
            // Consume the first quote
            ConsumeChar();
            if ((cur = reader.Peek()) < 0)
            {
                excess = '\0';
                return AddError("Unexpected end of file!");
            }

            if ((char)cur != quote)
            {
                excess = '\0';
                return false;
            }

            // Consume the second quote
            excess = (char)ConsumeChar();
            if ((cur = reader.Peek()) < 0 || (char)cur != quote) return false;

            // Consume the final quote
            ConsumeChar();
            excess = '\0';
            return true;
        }

        /**
         * A convenience method to process a single character within a quote.
         */
        private bool ProcessQuotedValueCharacter(char quote,
                                                 bool isNonLiteral,
                                                 char c,
                                                 StringBuilder sb,
                                                 ref bool escaped)
        {
            if (TomlSyntax.MustBeEscaped(c))
                return AddError($"The character U+{(int)c:X8} must be escaped in a string!");

            if (escaped)
            {
                sb.Append(c);
                escaped = false;
                return false;
            }

            if (c == quote)
            {
                if (!isNonLiteral && reader.Peek() == quote)
                {
                    reader.Read();
                    col++;
                    sb.Append(quote);
                    return false;
                }

                return true;
            }
            if (isNonLiteral && c == TomlSyntax.ESCAPE_SYMBOL)
                escaped = true;
            if (c == TomlSyntax.NEWLINE_CHARACTER)
                return AddError("Encountered newline in single line string!");

            sb.Append(c);
            return false;
        }

        /**
         * Reads a single-line string.
         * Assumes the cursor is at the first character that belongs to the string.
         * Consumes all characters that belong to the string (including the closing quote).
         * 
         * Example:
         * "test"  ==>  "test"
         * ^                 ^
         */
        private string ReadQuotedValueSingleLine(char quote, char initialData = '\0')
        {
            var isNonLiteral = quote == TomlSyntax.BASIC_STRING_SYMBOL;
            var sb = new StringBuilder();
            var escaped = false;

            if (initialData != '\0')
            {
                var shouldReturn =
                    ProcessQuotedValueCharacter(quote, isNonLiteral, initialData, sb, ref escaped);
                if (currentState == ParseState.None) return null;
                if (shouldReturn)
                    if (isNonLiteral)
                    {
                        if (sb.ToString().TryUnescape(out var res, out var ex)) return res;
                        AddError(ex.Message);
                        return null;
                    }
                    else
                        return sb.ToString();
            }

            int cur;
            var readDone = false;
            while ((cur = reader.Read()) >= 0)
            {
                // Consume the character
                col++;
                var c = (char)cur;
                readDone = ProcessQuotedValueCharacter(quote, isNonLiteral, c, sb, ref escaped);
                if (readDone)
                {
                    if (currentState == ParseState.None) return null;
                    break;
                }
            }

            if (!readDone)
            {
                AddError("Unclosed string.");
                return null;
            }

            if (!isNonLiteral) return sb.ToString();
            if (sb.ToString().TryUnescape(out var unescaped, out var unescapedEx)) return unescaped;
            AddError(unescapedEx.Message);
            return null;
        }

        /**
         * Reads a multiline string.
         * Assumes the cursor is at the first character that belongs to the string.
         * Consumes all characters that belong to the string and the three closing quotes.
         * 
         * Example:
         * """test"""  ==>  """test"""
         * ^                       ^
         */
        private string ReadQuotedValueMultiLine(char quote)
        {
            var isBasic = quote == TomlSyntax.BASIC_STRING_SYMBOL;
            var sb = new StringBuilder();
            var escaped = false;
            var skipWhitespace = false;
            var skipWhitespaceLineSkipped = false;
            var quotesEncountered = 0;
            var first = true;
            int cur;
            while ((cur = ConsumeChar()) >= 0)
            {
                var c = (char)cur;
                if (TomlSyntax.MustBeEscaped(c, true))
                {
                    AddError($"The character U+{(int)c:X8} must be escaped!");
                    return null;
                }
                // Trim the first newline
                if (first && TomlSyntax.IsNewLine(c))
                {
                    if (TomlSyntax.IsLineBreak(c))
                        first = false;
                    else
                        AdvanceLine();
                    continue;
                }

                first = false;
                //TODO: Reuse ProcessQuotedValueCharacter
                // Skip the current character if it is going to be escaped later
                if (escaped)
                {
                    sb.Append(c);
                    escaped = false;
                    continue;
                }

                // If we are currently skipping empty spaces, skip
                if (skipWhitespace)
                {
                    if (TomlSyntax.IsEmptySpace(c))
                    {
                        if (TomlSyntax.IsLineBreak(c))
                        {
                            skipWhitespaceLineSkipped = true;
                            AdvanceLine();
                        }
                        continue;
                    }

                    if (!skipWhitespaceLineSkipped)
                    {
                        AddError("Non-whitespace character after trim marker.");
                        return null;
                    }

                    skipWhitespaceLineSkipped = false;
                    skipWhitespace = false;
                }

                // If we encounter an escape sequence...
                if (isBasic && c == TomlSyntax.ESCAPE_SYMBOL)
                {
                    var next = reader.Peek();
                    var nc = (char)next;
                    if (next >= 0)
                    {
                        // ...and the next char is empty space, we must skip all whitespaces
                        if (TomlSyntax.IsEmptySpace(nc))
                        {
                            skipWhitespace = true;
                            continue;
                        }

                        // ...and we have \" or \, skip the character
                        if (nc == quote || nc == TomlSyntax.ESCAPE_SYMBOL) escaped = true;
                    }
                }

                // Count the consecutive quotes
                if (c == quote)
                    quotesEncountered++;
                else
                    quotesEncountered = 0;

                // If the are three quotes, count them as closing quotes
                if (quotesEncountered == 3) break;

                sb.Append(c);
            }

            // TOML actually allows to have five ending quotes like
            // """"" => "" belong to the string + """ is the actual ending
            quotesEncountered = 0;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;
                if (c == quote && ++quotesEncountered < 3)
                {
                    sb.Append(c);
                    ConsumeChar();
                }
                else break;
            }

            // Remove last two quotes (third one wasn't included by default)
            sb.Length -= 2;
            if (!isBasic) return sb.ToString();
            if (sb.ToString().TryUnescape(out var res, out var ex)) return res;
            AddError(ex.Message);
            return null;
        }

        #endregion

        #region Node creation

        private bool InsertNode(TomlNode node, TomlNode root, IList<string> path)
        {
            var latestNode = root;
            if (path.Count > 1)
                for (var index = 0; index < path.Count - 1; index++)
                {
                    var subkey = path[index];
                    if (latestNode.TryGetNode(subkey, out var currentNode))
                    {
                        if (currentNode.HasValue)
                            return AddError($"The key {".".Join(path)} already has a value assigned to it!");
                    }
                    else
                    {
                        currentNode = new TomlTable();
                        latestNode[subkey] = currentNode;
                    }

                    latestNode = currentNode;
                    if (latestNode is TomlTable { IsInline: true })
                        return AddError($"Cannot assign {".".Join(path)} because it will edit an immutable table.");
                }

            if (latestNode.HasKey(path[path.Count - 1]))
                return AddError($"The key {".".Join(path)} is already defined!");
            latestNode[path[path.Count - 1]] = node;
            node.CollapseLevel = path.Count - 1;
            return true;
        }

        private TomlTable CreateTable(TomlNode root, IList<string> path, bool arrayTable)
        {
            if (path.Count == 0) return null;
            var latestNode = root;
            for (var index = 0; index < path.Count; index++)
            {
                var subkey = path[index];

                if (latestNode.TryGetNode(subkey, out var node))
                {
                    if (node.IsArray && arrayTable)
                    {
                        var arr = (TomlArray)node;

                        if (!arr.IsTableArray)
                        {
                            AddError($"The array {".".Join(path)} cannot be redefined as an array table!");
                            return null;
                        }

                        if (index == path.Count - 1)
                        {
                            latestNode = new TomlTable();
                            arr.Add(latestNode);
                            break;
                        }

                        latestNode = arr[arr.ChildrenCount - 1];
                        continue;
                    }

                    if (node is TomlTable { IsInline: true })
                    {
                        AddError($"Cannot create table {".".Join(path)} because it will edit an immutable table.");
                        return null;
                    }

                    if (node.HasValue)
                    {
                        if (!(node is TomlArray { IsTableArray: true } array))
                        {
                            AddError($"The key {".".Join(path)} has a value assigned to it!");
                            return null;
                        }

                        latestNode = array[array.ChildrenCount - 1];
                        continue;
                    }

                    if (index == path.Count - 1)
                    {
                        if (arrayTable && !node.IsArray)
                        {
                            AddError($"The table {".".Join(path)} cannot be redefined as an array table!");
                            return null;
                        }

                        if (node is TomlTable { isImplicit: false })
                        {
                            AddError($"The table {".".Join(path)} is defined multiple times!");
                            return null;
                        }
                    }
                }
                else
                {
                    if (index == path.Count - 1 && arrayTable)
                    {
                        var table = new TomlTable();
                        var arr = new TomlArray
                        {
                            IsTableArray = true
                        };
                        arr.Add(table);
                        latestNode[subkey] = arr;
                        latestNode = table;
                        break;
                    }

                    node = new TomlTable { isImplicit = true };
                    latestNode[subkey] = node;
                }

                latestNode = node;
            }

            var result = (TomlTable)latestNode;
            result.isImplicit = false;
            return result;
        }

        #endregion

        #region Misc parsing

        private string ParseComment()
        {
            ConsumeChar();
            var commentLine = reader.ReadLine()?.Trim() ?? "";
            if (commentLine.Any(ch => TomlSyntax.MustBeEscaped(ch)))
                AddError("Comment must not contain control characters other than tab.", false);
            return commentLine;
        }
        #endregion
    }

    #endregion

    public static class TOML
    {
        public static bool ForceASCII { get; set; } = false;

        public static TomlTable Parse(TextReader reader)
        {
            using var parser = new TOMLParser(reader) { ForceASCII = ForceASCII };
            return parser.Parse();
        }
    }

    #region Exception Types

    public class TomlFormatException : Exception
    {
        public TomlFormatException(string message) : base(message) { }
    }

    public class TomlParseException : Exception
    {
        public TomlParseException(TomlTable parsed, IEnumerable<TomlSyntaxException> exceptions) :
            base("TOML file contains format errors")
        {
            ParsedTable = parsed;
            SyntaxErrors = exceptions;
        }

        public TomlTable ParsedTable { get; }

        public IEnumerable<TomlSyntaxException> SyntaxErrors { get; }
    }

    public class TomlSyntaxException : Exception
    {
        public TomlSyntaxException(string message, TOMLParser.ParseState state, int line, int col) : base(message)
        {
            ParseState = state;
            Line = line;
            Column = col;
        }

        public TOMLParser.ParseState ParseState { get; }

        public int Line { get; }

        public int Column { get; }
    }

    #endregion

    #region Parse utilities

    internal static class TomlSyntax
    {
        #region Type Patterns

        public const string TRUE_VALUE = "true";
        public const string FALSE_VALUE = "false";
        public const string NAN_VALUE = "nan";
        public const string POS_NAN_VALUE = "+nan";
        public const string NEG_NAN_VALUE = "-nan";
        public const string INF_VALUE = "inf";
        public const string POS_INF_VALUE = "+inf";
        public const string NEG_INF_VALUE = "-inf";

        public static bool IsBoolean(string s) => s is TRUE_VALUE or FALSE_VALUE;

        public static bool IsPosInf(string s) => s is INF_VALUE or POS_INF_VALUE;

        public static bool IsNegInf(string s) => s == NEG_INF_VALUE;

        public static bool IsNaN(string s) => s is NAN_VALUE or POS_NAN_VALUE or NEG_NAN_VALUE;

        public static bool IsInteger(string s) => IntegerPattern.IsMatch(s);

        public static bool IsFloat(string s) => FloatPattern.IsMatch(s);

        public static bool IsIntegerWithBase(string s, out int numberBase)
        {
            numberBase = 10;
            var match = BasedIntegerPattern.Match(s);
            if (!match.Success) return false;
            IntegerBases.TryGetValue(match.Groups["base"].Value, out numberBase);
            return true;
        }

        /**
         * A pattern to verify the integer value according to the TOML specification.
         */
        public static readonly Regex IntegerPattern =
            new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)*)$", RegexOptions.Compiled);

        /**
         * A pattern to verify a special 0x, 0o and 0b forms of an integer according to the TOML specification.
         */
        public static readonly Regex BasedIntegerPattern =
            new(@"^0(?<base>x|b|o)(?!_)(_?[0-9A-F])*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        /**
         * A pattern to verify the float value according to the TOML specification.
         */
        public static readonly Regex FloatPattern =
            new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)+)(((e(\+|-)?(?!_)(_?\d)+)?)|(\.(?!_)(_?\d)+(e(\+|-)?(?!_)(_?\d)+)?))$",
                RegexOptions.Compiled | RegexOptions.IgnoreCase);

        /**
         * A helper dictionary to map TOML base codes into the radii.
         */
        public static readonly Dictionary<string, int> IntegerBases = new()
        {
            ["x"] = 16,
            ["o"] = 8,
            ["b"] = 2
        };

        /**
         * A helper dictionary to map non-decimal bases to their TOML identifiers
         */
        public static readonly Dictionary<int, string> BaseIdentifiers = new()
        {
            [2] = "b",
            [8] = "o",
            [16] = "x"
        };

        public const string RFC3339EmptySeparator = " ";
        public const string ISO861Separator = "T";
        public const string ISO861ZeroZone = "+00:00";
        public const string RFC3339ZeroZone = "Z";

        /**
         * Valid date formats with timezone as per RFC3339.
         */
        public static readonly string[] RFC3339Formats =
        {
            "yyyy'-'MM-ddTHH':'mm':'ssK", "yyyy'-'MM-ddTHH':'mm':'ss'.'fK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffK",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffK",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffffK",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffffK"
        };

        /**
         * Valid date formats without timezone (assumes local) as per RFC3339.
         */
        public static readonly string[] RFC3339LocalDateTimeFormats =
        {
            "yyyy'-'MM-ddTHH':'mm':'ss", "yyyy'-'MM-ddTHH':'mm':'ss'.'f", "yyyy'-'MM-ddTHH':'mm':'ss'.'ff",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffff",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffff",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffff"
        };

        /**
         * Valid full date format as per TOML spec.
         */
        public static readonly string LocalDateFormat = "yyyy'-'MM'-'dd";

        /**
         * Valid time formats as per TOML spec.
         */
        public static readonly string[] RFC3339LocalTimeFormats =
        {
            "HH':'mm':'ss", "HH':'mm':'ss'.'f", "HH':'mm':'ss'.'ff", "HH':'mm':'ss'.'fff", "HH':'mm':'ss'.'ffff",
            "HH':'mm':'ss'.'fffff", "HH':'mm':'ss'.'ffffff", "HH':'mm':'ss'.'fffffff"
        };

        #endregion

        #region Character definitions

        public const char ARRAY_END_SYMBOL = ']';
        public const char ITEM_SEPARATOR = ',';
        public const char ARRAY_START_SYMBOL = '[';
        public const char BASIC_STRING_SYMBOL = '\"';
        public const char COMMENT_SYMBOL = '#';
        public const char ESCAPE_SYMBOL = '\\';
        public const char KEY_VALUE_SEPARATOR = '=';
        public const char NEWLINE_CARRIAGE_RETURN_CHARACTER = '\r';
        public const char NEWLINE_CHARACTER = '\n';
        public const char SUBKEY_SEPARATOR = '.';
        public const char TABLE_END_SYMBOL = ']';
        public const char TABLE_START_SYMBOL = '[';
        public const char INLINE_TABLE_START_SYMBOL = '{';
        public const char INLINE_TABLE_END_SYMBOL = '}';
        public const char LITERAL_STRING_SYMBOL = '\'';
        public const char INT_NUMBER_SEPARATOR = '_';

        public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER };

        public static bool IsQuoted(char c) => c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL;

        public static bool IsWhiteSpace(char c) => c is ' ' or '\t';

        public static bool IsNewLine(char c) => c is NEWLINE_CHARACTER or NEWLINE_CARRIAGE_RETURN_CHARACTER;

        public static bool IsLineBreak(char c) => c == NEWLINE_CHARACTER;

        public static bool IsEmptySpace(char c) => IsWhiteSpace(c) || IsNewLine(c);

        public static bool IsBareKey(char c) =>
            c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_' or '-';

        public static bool MustBeEscaped(char c, bool allowNewLines = false)
        {
            var result = c is (>= '\u0000' and <= '\u0008') or '\u000b' or '\u000c' or (>= '\u000e' and <= '\u001f') or '\u007f';
            if (!allowNewLines)
                result |= c is >= '\u000a' and <= '\u000e';
            return result;
        }

        public static bool IsValueSeparator(char c) =>
            c is ITEM_SEPARATOR or ARRAY_END_SYMBOL or INLINE_TABLE_END_SYMBOL;

        #endregion
    }

    internal static class StringUtils
    {
        public static string AsKey(this string key)
        {
            var quote = key == string.Empty || key.Any(c => !TomlSyntax.IsBareKey(c));
            return !quote ? key : $"{TomlSyntax.BASIC_STRING_SYMBOL}{key.Escape()}{TomlSyntax.BASIC_STRING_SYMBOL}";
        }

        public static string Join(this string self, IEnumerable<string> subItems)
        {
            var sb = new StringBuilder();
            var first = true;

            foreach (var subItem in subItems)
            {
                if (!first) sb.Append(self);
                first = false;
                sb.Append(subItem);
            }

            return sb.ToString();
        }

        public delegate bool TryDateParseDelegate<T>(string s, string format, IFormatProvider ci, DateTimeStyles dts, out T dt);

        public static bool TryParseDateTime<T>(string s,
                                               string[] formats,
                                               DateTimeStyles styles,
                                               TryDateParseDelegate<T> parser,
                                               out T dateTime,
                                               out int parsedFormat)
        {
            parsedFormat = 0;
            dateTime = default;
            for (var i = 0; i < formats.Length; i++)
            {
                var format = formats[i];
                if (!parser(s, format, CultureInfo.InvariantCulture, styles, out dateTime)) continue;
                parsedFormat = i;
                return true;
            }

            return false;
        }

        public static void AsComment(this string self, TextWriter tw)
        {
            foreach (var line in self.Split(TomlSyntax.NEWLINE_CHARACTER))
                tw.WriteLine($"{TomlSyntax.COMMENT_SYMBOL} {line.Trim()}");
        }

        public static string RemoveAll(this string txt, char toRemove)
        {
            var sb = new StringBuilder(txt.Length);
            foreach (var c in txt.Where(c => c != toRemove))
                sb.Append(c);
            return sb.ToString();
        }

        public static string Escape(this string txt, bool escapeNewlines = true)
        {
            var stringBuilder = new StringBuilder(txt.Length + 2);
            for (var i = 0; i < txt.Length; i++)
            {
                var c = txt[i];

                static string CodePoint(string txt, ref int i, char c) => char.IsSurrogatePair(txt, i)
                    ? $"\\U{char.ConvertToUtf32(txt, i++):X8}"
                    : $"\\u{(ushort)c:X4}";

                stringBuilder.Append(c switch
                {
                    '\b' => @"\b",
                    '\t' => @"\t",
                    '\n' when escapeNewlines => @"\n",
                    '\f' => @"\f",
                    '\r' when escapeNewlines => @"\r",
                    '\\' => @"\\",
                    '\"' => @"\""",
                    var _ when TomlSyntax.MustBeEscaped(c, !escapeNewlines) || TOML.ForceASCII && c > sbyte.MaxValue =>
                        CodePoint(txt, ref i, c),
                    var _ => c
                });
            }

            return stringBuilder.ToString();
        }

        public static bool TryUnescape(this string txt, out string unescaped, out Exception exception)
        {
            try
            {
                exception = null;
                unescaped = txt.Unescape();
                return true;
            }
            catch (Exception e)
            {
                exception = e;
                unescaped = null;
                return false;
            }
        }

        public static string Unescape(this string txt)
        {
            if (string.IsNullOrEmpty(txt)) return txt;
            var stringBuilder = new StringBuilder(txt.Length);
            for (var i = 0; i < txt.Length;)
            {
                var num = txt.IndexOf('\\', i);
                var next = num + 1;
                if (num < 0 || num == txt.Length - 1) num = txt.Length;
                stringBuilder.Append(txt, i, num - i);
                if (num >= txt.Length) break;
                var c = txt[next];

                static string CodePoint(int next, string txt, ref int num, int size)
                {
                    if (next + size >= txt.Length) throw new Exception("Undefined escape sequence!");
                    num += size;
                    return char.ConvertFromUtf32(Convert.ToInt32(txt.Substring(next + 1, size), 16));
                }

                stringBuilder.Append(c switch
                {
                    'b' => "\b",
                    't' => "\t",
                    'n' => "\n",
                    'f' => "\f",
                    'r' => "\r",
                    '\'' => "\'",
                    '\"' => "\"",
                    '\\' => "\\",
                    'u' => CodePoint(next, txt, ref num, 4),
                    'U' => CodePoint(next, txt, ref num, 8),
                    var _ => throw new Exception("Undefined escape sequence!")
                });
                i = num + 2;
            }

            return stringBuilder.ToString();
        }
    }

    #endregion
}

```

--------------------------------------------------------------------------------
/UnityMcpBridge/Editor/External/Tommy.cs:
--------------------------------------------------------------------------------

```csharp
#region LICENSE

/*
 * MIT License
 * 
 * Copyright (c) 2020 Denis Zhidkikh
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace MCPForUnity.External.Tommy
{
    #region TOML Nodes

    public abstract class TomlNode : IEnumerable
    {
        public virtual bool HasValue { get; } = false;
        public virtual bool IsArray { get; } = false;
        public virtual bool IsTable { get; } = false;
        public virtual bool IsString { get; } = false;
        public virtual bool IsInteger { get; } = false;
        public virtual bool IsFloat { get; } = false;
        public bool IsDateTime => IsDateTimeLocal || IsDateTimeOffset;
        public virtual bool IsDateTimeLocal { get; } = false;
        public virtual bool IsDateTimeOffset { get; } = false;
        public virtual bool IsBoolean { get; } = false;
        public virtual string Comment { get; set; }
        public virtual int CollapseLevel { get; set; }

        public virtual TomlTable AsTable => this as TomlTable;
        public virtual TomlString AsString => this as TomlString;
        public virtual TomlInteger AsInteger => this as TomlInteger;
        public virtual TomlFloat AsFloat => this as TomlFloat;
        public virtual TomlBoolean AsBoolean => this as TomlBoolean;
        public virtual TomlDateTimeLocal AsDateTimeLocal => this as TomlDateTimeLocal;
        public virtual TomlDateTimeOffset AsDateTimeOffset => this as TomlDateTimeOffset;
        public virtual TomlDateTime AsDateTime => this as TomlDateTime;
        public virtual TomlArray AsArray => this as TomlArray;

        public virtual int ChildrenCount => 0;

        public virtual TomlNode this[string key]
        {
            get => null;
            set { }
        }

        public virtual TomlNode this[int index]
        {
            get => null;
            set { }
        }

        public virtual IEnumerable<TomlNode> Children
        {
            get { yield break; }
        }

        public virtual IEnumerable<string> Keys
        {
            get { yield break; }
        }

        public IEnumerator GetEnumerator() => Children.GetEnumerator();

        public virtual bool TryGetNode(string key, out TomlNode node)
        {
            node = null;
            return false;
        }

        public virtual bool HasKey(string key) => false;

        public virtual bool HasItemAt(int index) => false;

        public virtual void Add(string key, TomlNode node) { }

        public virtual void Add(TomlNode node) { }

        public virtual void Delete(TomlNode node) { }

        public virtual void Delete(string key) { }

        public virtual void Delete(int index) { }

        public virtual void AddRange(IEnumerable<TomlNode> nodes)
        {
            foreach (var tomlNode in nodes) Add(tomlNode);
        }

        public virtual void WriteTo(TextWriter tw, string name = null) => tw.WriteLine(ToInlineToml());

        public virtual string ToInlineToml() => ToString();

        #region Native type to TOML cast

        public static implicit operator TomlNode(string value) => new TomlString { Value = value };

        public static implicit operator TomlNode(bool value) => new TomlBoolean { Value = value };

        public static implicit operator TomlNode(long value) => new TomlInteger { Value = value };

        public static implicit operator TomlNode(float value) => new TomlFloat { Value = value };

        public static implicit operator TomlNode(double value) => new TomlFloat { Value = value };

        public static implicit operator TomlNode(DateTime value) => new TomlDateTimeLocal { Value = value };

        public static implicit operator TomlNode(DateTimeOffset value) => new TomlDateTimeOffset { Value = value };

        public static implicit operator TomlNode(TomlNode[] nodes)
        {
            var result = new TomlArray();
            result.AddRange(nodes);
            return result;
        }

        #endregion

        #region TOML to native type cast

        public static implicit operator string(TomlNode value) => value.ToString();

        public static implicit operator int(TomlNode value) => (int)value.AsInteger.Value;

        public static implicit operator long(TomlNode value) => value.AsInteger.Value;

        public static implicit operator float(TomlNode value) => (float)value.AsFloat.Value;

        public static implicit operator double(TomlNode value) => value.AsFloat.Value;

        public static implicit operator bool(TomlNode value) => value.AsBoolean.Value;

        public static implicit operator DateTime(TomlNode value) => value.AsDateTimeLocal.Value;

        public static implicit operator DateTimeOffset(TomlNode value) => value.AsDateTimeOffset.Value;

        #endregion
    }

    public class TomlString : TomlNode
    {
        public override bool HasValue { get; } = true;
        public override bool IsString { get; } = true;
        public bool IsMultiline { get; set; }
        public bool MultilineTrimFirstLine { get; set; }
        public bool PreferLiteral { get; set; }

        public string Value { get; set; }

        public override string ToString() => Value;

        public override string ToInlineToml()
        {
            // Automatically convert literal to non-literal if there are too many literal string symbols
            if (Value.IndexOf(new string(TomlSyntax.LITERAL_STRING_SYMBOL, IsMultiline ? 3 : 1), StringComparison.Ordinal) != -1 && PreferLiteral) PreferLiteral = false;
            var quotes = new string(PreferLiteral ? TomlSyntax.LITERAL_STRING_SYMBOL : TomlSyntax.BASIC_STRING_SYMBOL,
                                    IsMultiline ? 3 : 1);
            var result = PreferLiteral ? Value : Value.Escape(!IsMultiline);
            if (IsMultiline)
                result = result.Replace("\r\n", "\n").Replace("\n", Environment.NewLine);
            if (IsMultiline && (MultilineTrimFirstLine || !MultilineTrimFirstLine && result.StartsWith(Environment.NewLine)))
                result = $"{Environment.NewLine}{result}";
            return $"{quotes}{result}{quotes}";
        }
    }

    public class TomlInteger : TomlNode
    {
        public enum Base
        {
            Binary = 2,
            Octal = 8,
            Decimal = 10,
            Hexadecimal = 16
        }

        public override bool IsInteger { get; } = true;
        public override bool HasValue { get; } = true;
        public Base IntegerBase { get; set; } = Base.Decimal;

        public long Value { get; set; }

        public override string ToString() => Value.ToString();

        public override string ToInlineToml() =>
            IntegerBase != Base.Decimal
                ? $"0{TomlSyntax.BaseIdentifiers[(int)IntegerBase]}{Convert.ToString(Value, (int)IntegerBase)}"
                : Value.ToString(CultureInfo.InvariantCulture);
    }

    public class TomlFloat : TomlNode, IFormattable
    {
        public override bool IsFloat { get; } = true;
        public override bool HasValue { get; } = true;

        public double Value { get; set; }

        public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);

        public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider);

        public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);

        public override string ToInlineToml() =>
            Value switch
            {
                var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE,
                var v when double.IsPositiveInfinity(v) => TomlSyntax.INF_VALUE,
                var v when double.IsNegativeInfinity(v) => TomlSyntax.NEG_INF_VALUE,
                var v => v.ToString("G", CultureInfo.InvariantCulture).ToLowerInvariant()
            };
    }

    public class TomlBoolean : TomlNode
    {
        public override bool IsBoolean { get; } = true;
        public override bool HasValue { get; } = true;

        public bool Value { get; set; }

        public override string ToString() => Value.ToString();

        public override string ToInlineToml() => Value ? TomlSyntax.TRUE_VALUE : TomlSyntax.FALSE_VALUE;
    }

    public class TomlDateTime : TomlNode, IFormattable
    {
        public int SecondsPrecision { get; set; }
        public override bool HasValue { get; } = true;
        public virtual string ToString(string format, IFormatProvider formatProvider) => string.Empty;
        public virtual string ToString(IFormatProvider formatProvider) => string.Empty;
        protected virtual string ToInlineTomlInternal() => string.Empty;

        public override string ToInlineToml() => ToInlineTomlInternal()
                                                .Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator)
                                                .Replace(TomlSyntax.ISO861ZeroZone, TomlSyntax.RFC3339ZeroZone);
    }

    public class TomlDateTimeOffset : TomlDateTime
    {
        public override bool IsDateTimeOffset { get; } = true;
        public DateTimeOffset Value { get; set; }

        public override string ToString() => Value.ToString(CultureInfo.CurrentCulture);
        public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);

        public override string ToString(string format, IFormatProvider formatProvider) =>
            Value.ToString(format, formatProvider);

        protected override string ToInlineTomlInternal() => Value.ToString(TomlSyntax.RFC3339Formats[SecondsPrecision]);
    }

    public class TomlDateTimeLocal : TomlDateTime
    {
        public enum DateTimeStyle
        {
            Date,
            Time,
            DateTime
        }

        public override bool IsDateTimeLocal { get; } = true;
        public DateTimeStyle Style { get; set; } = DateTimeStyle.DateTime;
        public DateTime Value { get; set; }

        public override string ToString() => Value.ToString(CultureInfo.CurrentCulture);

        public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider);

        public override string ToString(string format, IFormatProvider formatProvider) =>
            Value.ToString(format, formatProvider);

        public override string ToInlineToml() =>
            Style switch
            {
                DateTimeStyle.Date => Value.ToString(TomlSyntax.LocalDateFormat),
                DateTimeStyle.Time => Value.ToString(TomlSyntax.RFC3339LocalTimeFormats[SecondsPrecision]),
                var _ => Value.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision])
            };
    }

    public class TomlArray : TomlNode
    {
        private List<TomlNode> values;

        public override bool HasValue { get; } = true;
        public override bool IsArray { get; } = true;
        public bool IsMultiline { get; set; }
        public bool IsTableArray { get; set; }
        public List<TomlNode> RawArray => values ??= new List<TomlNode>();

        public override TomlNode this[int index]
        {
            get
            {
                if (index < RawArray.Count) return RawArray[index];
                var lazy = new TomlLazy(this);
                this[index] = lazy;
                return lazy;
            }
            set
            {
                if (index == RawArray.Count)
                    RawArray.Add(value);
                else
                    RawArray[index] = value;
            }
        }

        public override int ChildrenCount => RawArray.Count;

        public override IEnumerable<TomlNode> Children => RawArray.AsEnumerable();

        public override void Add(TomlNode node) => RawArray.Add(node);

        public override void AddRange(IEnumerable<TomlNode> nodes) => RawArray.AddRange(nodes);

        public override void Delete(TomlNode node) => RawArray.Remove(node);

        public override void Delete(int index) => RawArray.RemoveAt(index);

        public override string ToString() => ToString(false);

        public string ToString(bool multiline)
        {
            var sb = new StringBuilder();
            sb.Append(TomlSyntax.ARRAY_START_SYMBOL);
            if (ChildrenCount != 0)
            {
                var arrayStart = multiline ? $"{Environment.NewLine}  " : " ";
                var arraySeparator = multiline ? $"{TomlSyntax.ITEM_SEPARATOR}{Environment.NewLine}  " : $"{TomlSyntax.ITEM_SEPARATOR} ";
                var arrayEnd = multiline ? Environment.NewLine : " ";
                sb.Append(arrayStart)
                  .Append(arraySeparator.Join(RawArray.Select(n => n.ToInlineToml())))
                  .Append(arrayEnd);
            }
            sb.Append(TomlSyntax.ARRAY_END_SYMBOL);
            return sb.ToString();
        }

        public override void WriteTo(TextWriter tw, string name = null)
        {
            // If it's a normal array, write it as usual
            if (!IsTableArray)
            {
                tw.WriteLine(ToString(IsMultiline));
                return;
            }

            if (!(Comment is null))
            {
                tw.WriteLine();
                Comment.AsComment(tw);
            }
            tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
            tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
            tw.Write(name);
            tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
            tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
            tw.WriteLine();

            var first = true;

            foreach (var tomlNode in RawArray)
            {
                if (!(tomlNode is TomlTable tbl))
                    throw new TomlFormatException("The array is marked as array table but contains non-table nodes!");

                // Ensure it's parsed as a section
                tbl.IsInline = false;

                if (!first)
                {
                    tw.WriteLine();

                    Comment?.AsComment(tw);
                    tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
                    tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
                    tw.Write(name);
                    tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
                    tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
                    tw.WriteLine();
                }

                first = false;

                // Don't write section since it's already written here
                tbl.WriteTo(tw, name, false);
            }
        }
    }

    public class TomlTable : TomlNode
    {
        private Dictionary<string, TomlNode> children;
        internal bool isImplicit;

        public override bool HasValue { get; } = false;
        public override bool IsTable { get; } = true;
        public bool IsInline { get; set; }
        public Dictionary<string, TomlNode> RawTable => children ??= new Dictionary<string, TomlNode>();

        public override TomlNode this[string key]
        {
            get
            {
                if (RawTable.TryGetValue(key, out var result)) return result;
                var lazy = new TomlLazy(this);
                RawTable[key] = lazy;
                return lazy;
            }
            set => RawTable[key] = value;
        }

        public override int ChildrenCount => RawTable.Count;
        public override IEnumerable<TomlNode> Children => RawTable.Select(kv => kv.Value);
        public override IEnumerable<string> Keys => RawTable.Select(kv => kv.Key);
        public override bool HasKey(string key) => RawTable.ContainsKey(key);
        public override void Add(string key, TomlNode node) => RawTable.Add(key, node);
        public override bool TryGetNode(string key, out TomlNode node) => RawTable.TryGetValue(key, out node);
        public override void Delete(TomlNode node) => RawTable.Remove(RawTable.First(kv => kv.Value == node).Key);
        public override void Delete(string key) => RawTable.Remove(key);

        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.Append(TomlSyntax.INLINE_TABLE_START_SYMBOL);

            if (ChildrenCount != 0)
            {
                var collapsed = CollectCollapsedItems(normalizeOrder: false);

                if (collapsed.Count != 0)
                    sb.Append(' ')
                      .Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(collapsed.Select(n =>
                                                                       $"{n.Key} {TomlSyntax.KEY_VALUE_SEPARATOR} {n.Value.ToInlineToml()}")));
                sb.Append(' ');
            }

            sb.Append(TomlSyntax.INLINE_TABLE_END_SYMBOL);
            return sb.ToString();
        }

        private LinkedList<KeyValuePair<string, TomlNode>> CollectCollapsedItems(string prefix = "", int level = 0, bool normalizeOrder = true)
        {
            var nodes = new LinkedList<KeyValuePair<string, TomlNode>>();
            var postNodes = normalizeOrder ? new LinkedList<KeyValuePair<string, TomlNode>>() : nodes;

            foreach (var keyValuePair in RawTable)
            {
                var node = keyValuePair.Value;
                var key = keyValuePair.Key.AsKey();

                if (node is TomlTable tbl)
                {
                    var subnodes = tbl.CollectCollapsedItems($"{prefix}{key}.", level + 1, normalizeOrder);
                    // Write main table first before writing collapsed items
                    if (subnodes.Count == 0 && node.CollapseLevel == level)
                    {
                        postNodes.AddLast(new KeyValuePair<string, TomlNode>($"{prefix}{key}", node));
                    }
                    foreach (var kv in subnodes)
                        postNodes.AddLast(kv);
                }
                else if (node.CollapseLevel == level)
                    nodes.AddLast(new KeyValuePair<string, TomlNode>($"{prefix}{key}", node));
            }

            if (normalizeOrder)
                foreach (var kv in postNodes)
                    nodes.AddLast(kv);

            return nodes;
        }

        public override void WriteTo(TextWriter tw, string name = null) => WriteTo(tw, name, true);

        internal void WriteTo(TextWriter tw, string name, bool writeSectionName)
        {
            // The table is inline table
            if (IsInline && name != null)
            {
                tw.WriteLine(ToInlineToml());
                return;
            }

            var collapsedItems = CollectCollapsedItems();

            if (collapsedItems.Count == 0)
                return;

            var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } or TomlArray { IsTableArray: true });

            Comment?.AsComment(tw);

            if (name != null && (hasRealValues || Comment != null) && writeSectionName)
            {
                tw.Write(TomlSyntax.ARRAY_START_SYMBOL);
                tw.Write(name);
                tw.Write(TomlSyntax.ARRAY_END_SYMBOL);
                tw.WriteLine();
            }
            else if (Comment != null) // Add some spacing between the first node and the comment
            {
                tw.WriteLine();
            }

            var namePrefix = name == null ? "" : $"{name}.";
            var first = true;

            foreach (var collapsedItem in collapsedItems)
            {
                var key = collapsedItem.Key;
                if (collapsedItem.Value is TomlArray { IsTableArray: true } or TomlTable { IsInline: false })
                {
                    if (!first) tw.WriteLine();
                    first = false;
                    collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}");
                    continue;
                }
                first = false;

                collapsedItem.Value.Comment?.AsComment(tw);
                tw.Write(key);
                tw.Write(' ');
                tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR);
                tw.Write(' ');

                collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}");
            }
        }
    }

    internal class TomlLazy : TomlNode
    {
        private readonly TomlNode parent;
        private TomlNode replacement;

        public TomlLazy(TomlNode parent) => this.parent = parent;

        public override TomlNode this[int index]
        {
            get => Set<TomlArray>()[index];
            set => Set<TomlArray>()[index] = value;
        }

        public override TomlNode this[string key]
        {
            get => Set<TomlTable>()[key];
            set => Set<TomlTable>()[key] = value;
        }

        public override void Add(TomlNode node) => Set<TomlArray>().Add(node);

        public override void Add(string key, TomlNode node) => Set<TomlTable>().Add(key, node);

        public override void AddRange(IEnumerable<TomlNode> nodes) => Set<TomlArray>().AddRange(nodes);

        private TomlNode Set<T>() where T : TomlNode, new()
        {
            if (replacement != null) return replacement;

            var newNode = new T
            {
                Comment = Comment
            };

            if (parent.IsTable)
            {
                var key = parent.Keys.FirstOrDefault(s => parent.TryGetNode(s, out var node) && node.Equals(this));
                if (key == null) return default(T);

                parent[key] = newNode;
            }
            else if (parent.IsArray)
            {
                var index = parent.Children.TakeWhile(child => child != this).Count();
                if (index == parent.ChildrenCount) return default(T);
                parent[index] = newNode;
            }
            else
            {
                return default(T);
            }

            replacement = newNode;
            return newNode;
        }
    }

    #endregion

    #region Parser

    public class TOMLParser : IDisposable
    {
        public enum ParseState
        {
            None,
            KeyValuePair,
            SkipToNextLine,
            Table
        }

        private readonly TextReader reader;
        private ParseState currentState;
        private int line, col;
        private List<TomlSyntaxException> syntaxErrors;

        public TOMLParser(TextReader reader)
        {
            this.reader = reader;
            line = col = 0;
        }

        public bool ForceASCII { get; set; }

        public void Dispose() => reader?.Dispose();

        public TomlTable Parse()
        {
            syntaxErrors = new List<TomlSyntaxException>();
            line = col = 1;
            var rootNode = new TomlTable();
            var currentNode = rootNode;
            currentState = ParseState.None;
            var keyParts = new List<string>();
            var arrayTable = false;
            StringBuilder latestComment = null;
            var firstComment = true;

            int currentChar;
            while ((currentChar = reader.Peek()) >= 0)
            {
                var c = (char)currentChar;

                if (currentState == ParseState.None)
                {
                    // Skip white space
                    if (TomlSyntax.IsWhiteSpace(c)) goto consume_character;

                    if (TomlSyntax.IsNewLine(c))
                    {
                        // Check if there are any comments and so far no items being declared
                        if (latestComment != null && firstComment)
                        {
                            rootNode.Comment = latestComment.ToString().TrimEnd();
                            latestComment = null;
                            firstComment = false;
                        }

                        if (TomlSyntax.IsLineBreak(c))
                            AdvanceLine();

                        goto consume_character;
                    }

                    // Start of a comment; ignore until newline
                    if (c == TomlSyntax.COMMENT_SYMBOL)
                    {
                        latestComment ??= new StringBuilder();
                        latestComment.AppendLine(ParseComment());
                        AdvanceLine(1);
                        continue;
                    }

                    // Encountered a non-comment value. The comment must belong to it (ignore possible newlines)!
                    firstComment = false;

                    if (c == TomlSyntax.TABLE_START_SYMBOL)
                    {
                        currentState = ParseState.Table;
                        goto consume_character;
                    }

                    if (TomlSyntax.IsBareKey(c) || TomlSyntax.IsQuoted(c))
                    {
                        currentState = ParseState.KeyValuePair;
                    }
                    else
                    {
                        AddError($"Unexpected character \"{c}\"");
                        continue;
                    }
                }

                if (currentState == ParseState.KeyValuePair)
                {
                    var keyValuePair = ReadKeyValuePair(keyParts);

                    if (keyValuePair == null)
                    {
                        latestComment = null;
                        keyParts.Clear();

                        if (currentState != ParseState.None)
                            AddError("Failed to parse key-value pair!");
                        continue;
                    }

                    keyValuePair.Comment = latestComment?.ToString()?.TrimEnd();
                    var inserted = InsertNode(keyValuePair, currentNode, keyParts);
                    latestComment = null;
                    keyParts.Clear();
                    if (inserted)
                        currentState = ParseState.SkipToNextLine;
                    continue;
                }

                if (currentState == ParseState.Table)
                {
                    if (keyParts.Count == 0)
                    {
                        // We have array table
                        if (c == TomlSyntax.TABLE_START_SYMBOL)
                        {
                            // Consume the character
                            ConsumeChar();
                            arrayTable = true;
                        }

                        if (!ReadKeyName(ref keyParts, TomlSyntax.TABLE_END_SYMBOL))
                        {
                            keyParts.Clear();
                            continue;
                        }

                        if (keyParts.Count == 0)
                        {
                            AddError("Table name is emtpy.");
                            arrayTable = false;
                            latestComment = null;
                            keyParts.Clear();
                        }

                        continue;
                    }

                    if (c == TomlSyntax.TABLE_END_SYMBOL)
                    {
                        if (arrayTable)
                        {
                            // Consume the ending bracket so we can peek the next character
                            ConsumeChar();
                            var nextChar = reader.Peek();
                            if (nextChar < 0 || (char)nextChar != TomlSyntax.TABLE_END_SYMBOL)
                            {
                                AddError($"Array table {".".Join(keyParts)} has only one closing bracket.");
                                keyParts.Clear();
                                arrayTable = false;
                                latestComment = null;
                                continue;
                            }
                        }

                        currentNode = CreateTable(rootNode, keyParts, arrayTable);
                        if (currentNode != null)
                        {
                            currentNode.IsInline = false;
                            currentNode.Comment = latestComment?.ToString()?.TrimEnd();
                        }

                        keyParts.Clear();
                        arrayTable = false;
                        latestComment = null;

                        if (currentNode == null)
                        {
                            if (currentState != ParseState.None)
                                AddError("Error creating table array!");
                            // Reset a node to root in order to try and continue parsing
                            currentNode = rootNode;
                            continue;
                        }

                        currentState = ParseState.SkipToNextLine;
                        goto consume_character;
                    }

                    if (keyParts.Count != 0)
                    {
                        AddError($"Unexpected character \"{c}\"");
                        keyParts.Clear();
                        arrayTable = false;
                        latestComment = null;
                    }
                }

                if (currentState == ParseState.SkipToNextLine)
                {
                    if (TomlSyntax.IsWhiteSpace(c) || c == TomlSyntax.NEWLINE_CARRIAGE_RETURN_CHARACTER)
                        goto consume_character;

                    if (c is TomlSyntax.COMMENT_SYMBOL or TomlSyntax.NEWLINE_CHARACTER)
                    {
                        currentState = ParseState.None;
                        AdvanceLine();

                        if (c == TomlSyntax.COMMENT_SYMBOL)
                        {
                            col++;
                            ParseComment();
                            continue;
                        }

                        goto consume_character;
                    }

                    AddError($"Unexpected character \"{c}\" at the end of the line.");
                }

            consume_character:
                reader.Read();
                col++;
            }

            if (currentState != ParseState.None && currentState != ParseState.SkipToNextLine)
                AddError("Unexpected end of file!");

            if (syntaxErrors.Count > 0)
                throw new TomlParseException(rootNode, syntaxErrors);

            return rootNode;
        }

        private bool AddError(string message, bool skipLine = true)
        {
            syntaxErrors.Add(new TomlSyntaxException(message, currentState, line, col));
            // Skip the whole line in hope that it was only a single faulty value (and non-multiline one at that)
            if (skipLine)
            {
                reader.ReadLine();
                AdvanceLine(1);
            }
            currentState = ParseState.None;
            return false;
        }

        private void AdvanceLine(int startCol = 0)
        {
            line++;
            col = startCol;
        }

        private int ConsumeChar()
        {
            col++;
            return reader.Read();
        }

        #region Key-Value pair parsing

        /**
         * Reads a single key-value pair.
         * Assumes the cursor is at the first character that belong to the pair (including possible whitespace).
         * Consumes all characters that belong to the key and the value (ignoring possible trailing whitespace at the end).
         * 
         * Example:
         * foo = "bar"  ==> foo = "bar"
         * ^                           ^
         */
        private TomlNode ReadKeyValuePair(List<string> keyParts)
        {
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                if (TomlSyntax.IsQuoted(c) || TomlSyntax.IsBareKey(c))
                {
                    if (keyParts.Count != 0)
                    {
                        AddError("Encountered extra characters in key definition!");
                        return null;
                    }

                    if (!ReadKeyName(ref keyParts, TomlSyntax.KEY_VALUE_SEPARATOR))
                        return null;

                    continue;
                }

                if (TomlSyntax.IsWhiteSpace(c))
                {
                    ConsumeChar();
                    continue;
                }

                if (c == TomlSyntax.KEY_VALUE_SEPARATOR)
                {
                    ConsumeChar();
                    return ReadValue();
                }

                AddError($"Unexpected character \"{c}\" in key name.");
                return null;
            }

            return null;
        }

        /**
         * Reads a single value.
         * Assumes the cursor is at the first character that belongs to the value (including possible starting whitespace).
         * Consumes all characters belonging to the value (ignoring possible trailing whitespace at the end).
         * 
         * Example:
         * "test"  ==> "test"
         * ^                 ^
         */
        private TomlNode ReadValue(bool skipNewlines = false)
        {
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                if (TomlSyntax.IsWhiteSpace(c))
                {
                    ConsumeChar();
                    continue;
                }

                if (c == TomlSyntax.COMMENT_SYMBOL)
                {
                    AddError("No value found!");
                    return null;
                }

                if (TomlSyntax.IsNewLine(c))
                {
                    if (skipNewlines)
                    {
                        reader.Read();
                        AdvanceLine(1);
                        continue;
                    }

                    AddError("Encountered a newline when expecting a value!");
                    return null;
                }

                if (TomlSyntax.IsQuoted(c))
                {
                    var isMultiline = IsTripleQuote(c, out var excess);

                    // Error occurred in triple quote parsing
                    if (currentState == ParseState.None)
                        return null;

                    var value = isMultiline
                        ? ReadQuotedValueMultiLine(c)
                        : ReadQuotedValueSingleLine(c, excess);

                    if (value is null)
                        return null;

                    return new TomlString
                    {
                        Value = value,
                        IsMultiline = isMultiline,
                        PreferLiteral = c == TomlSyntax.LITERAL_STRING_SYMBOL
                    };
                }

                return c switch
                {
                    TomlSyntax.INLINE_TABLE_START_SYMBOL => ReadInlineTable(),
                    TomlSyntax.ARRAY_START_SYMBOL => ReadArray(),
                    var _ => ReadTomlValue()
                };
            }

            return null;
        }

        /**
         * Reads a single key name.
         * Assumes the cursor is at the first character belonging to the key (with possible trailing whitespace if `skipWhitespace = true`).
         * Consumes all the characters until the `until` character is met (but does not consume the character itself).
         * 
         * Example 1:
         * foo.bar  ==>  foo.bar           (`skipWhitespace = false`, `until = ' '`)
         * ^                    ^
         * 
         * Example 2:
         * [ foo . bar ] ==>  [ foo . bar ]     (`skipWhitespace = true`, `until = ']'`)
         * ^                             ^
         */
        private bool ReadKeyName(ref List<string> parts, char until)
        {
            var buffer = new StringBuilder();
            var quoted = false;
            var prevWasSpace = false;
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                // Reached the final character
                if (c == until) break;

                if (TomlSyntax.IsWhiteSpace(c))
                {
                    prevWasSpace = true;
                    goto consume_character;
                }

                if (buffer.Length == 0) prevWasSpace = false;

                if (c == TomlSyntax.SUBKEY_SEPARATOR)
                {
                    if (buffer.Length == 0 && !quoted)
                        return AddError($"Found an extra subkey separator in {".".Join(parts)}...");

                    parts.Add(buffer.ToString());
                    buffer.Length = 0;
                    quoted = false;
                    prevWasSpace = false;
                    goto consume_character;
                }

                if (prevWasSpace)
                    return AddError("Invalid spacing in key name");

                if (TomlSyntax.IsQuoted(c))
                {
                    if (quoted)

                        return AddError("Expected a subkey separator but got extra data instead!");

                    if (buffer.Length != 0)
                        return AddError("Encountered a quote in the middle of subkey name!");

                    // Consume the quote character and read the key name
                    col++;
                    buffer.Append(ReadQuotedValueSingleLine((char)reader.Read()));
                    quoted = true;
                    continue;
                }

                if (TomlSyntax.IsBareKey(c))
                {
                    buffer.Append(c);
                    goto consume_character;
                }

                // If we see an invalid symbol, let the next parser handle it
                break;

            consume_character:
                reader.Read();
                col++;
            }

            if (buffer.Length == 0 && !quoted)
                return AddError($"Found an extra subkey separator in {".".Join(parts)}...");

            parts.Add(buffer.ToString());

            return true;
        }

        #endregion

        #region Non-string value parsing

        /**
         * Reads the whole raw value until the first non-value character is encountered.
         * Assumes the cursor start position at the first value character and consumes all characters that may be related to the value.
         * Example:
         * 
         * 1_0_0_0  ==>  1_0_0_0
         * ^                    ^
         */
        private string ReadRawValue()
        {
            var result = new StringBuilder();
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;
                if (c == TomlSyntax.COMMENT_SYMBOL || TomlSyntax.IsNewLine(c) || TomlSyntax.IsValueSeparator(c)) break;
                result.Append(c);
                ConsumeChar();
            }

            // Replace trim with manual space counting?
            return result.ToString().Trim();
        }

        /**
         * Reads and parses a non-string, non-composite TOML value.
         * Assumes the cursor at the first character that is related to the value (with possible spaces).
         * Consumes all the characters that are related to the value.
         * 
         * Example
         * 1_0_0_0 # This is a comment
         * <newline>
         *     ==>  1_0_0_0 # This is a comment
         *     ^                                                  ^
         */
        private TomlNode ReadTomlValue()
        {
            var value = ReadRawValue();
            TomlNode node = value switch
            {
                var v when TomlSyntax.IsBoolean(v) => bool.Parse(v),
                var v when TomlSyntax.IsNaN(v) => double.NaN,
                var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity,
                var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity,
                var v when TomlSyntax.IsInteger(v) => long.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR),
                                                                 CultureInfo.InvariantCulture),
                var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR),
                                                                 CultureInfo.InvariantCulture),
                var v when TomlSyntax.IsIntegerWithBase(v, out var numberBase) => new TomlInteger
                {
                    Value = Convert.ToInt64(value.Substring(2).RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), numberBase),
                    IntegerBase = (TomlInteger.Base)numberBase
                },
                var _ => null
            };
            if (node != null) return node;

            // Normalize by removing space separator
            value = value.Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator);
            if (StringUtils.TryParseDateTime<DateTime>(value,
                                             TomlSyntax.RFC3339LocalDateTimeFormats,
                                             DateTimeStyles.AssumeLocal,
                                             DateTime.TryParseExact,
                                             out var dateTimeResult,
                                             out var precision))
                return new TomlDateTimeLocal
                {
                    Value = dateTimeResult,
                    SecondsPrecision = precision
                };

            if (DateTime.TryParseExact(value,
                                       TomlSyntax.LocalDateFormat,
                                       CultureInfo.InvariantCulture,
                                       DateTimeStyles.AssumeLocal,
                                       out dateTimeResult))
                return new TomlDateTimeLocal
                {
                    Value = dateTimeResult,
                    Style = TomlDateTimeLocal.DateTimeStyle.Date
                };

            if (StringUtils.TryParseDateTime(value,
                                             TomlSyntax.RFC3339LocalTimeFormats,
                                             DateTimeStyles.AssumeLocal,
                                             DateTime.TryParseExact,
                                             out dateTimeResult,
                                             out precision))
                return new TomlDateTimeLocal
                {
                    Value = dateTimeResult,
                    Style = TomlDateTimeLocal.DateTimeStyle.Time,
                    SecondsPrecision = precision
                };

            if (StringUtils.TryParseDateTime<DateTimeOffset>(value,
                                                             TomlSyntax.RFC3339Formats,
                                                             DateTimeStyles.None,
                                                             DateTimeOffset.TryParseExact,
                                                             out var dateTimeOffsetResult,
                                                             out precision))
                return new TomlDateTimeOffset
                {
                    Value = dateTimeOffsetResult,
                    SecondsPrecision = precision
                };

            AddError($"Value \"{value}\" is not a valid TOML value!");
            return null;
        }

        /**
         * Reads an array value.
         * Assumes the cursor is at the start of the array definition. Reads all character until the array closing bracket.
         * 
         * Example:
         * [1, 2, 3]  ==>  [1, 2, 3]
         * ^                        ^
         */
        private TomlArray ReadArray()
        {
            // Consume the start of array character
            ConsumeChar();
            var result = new TomlArray();
            TomlNode currentValue = null;
            var expectValue = true;

            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                if (c == TomlSyntax.ARRAY_END_SYMBOL)
                {
                    ConsumeChar();
                    break;
                }

                if (c == TomlSyntax.COMMENT_SYMBOL)
                {
                    reader.ReadLine();
                    AdvanceLine(1);
                    continue;
                }

                if (TomlSyntax.IsWhiteSpace(c) || TomlSyntax.IsNewLine(c))
                {
                    if (TomlSyntax.IsLineBreak(c))
                        AdvanceLine();
                    goto consume_character;
                }

                if (c == TomlSyntax.ITEM_SEPARATOR)
                {
                    if (currentValue == null)
                    {
                        AddError("Encountered multiple value separators");
                        return null;
                    }

                    result.Add(currentValue);
                    currentValue = null;
                    expectValue = true;
                    goto consume_character;
                }

                if (!expectValue)
                {
                    AddError("Missing separator between values");
                    return null;
                }
                currentValue = ReadValue(true);
                if (currentValue == null)
                {
                    if (currentState != ParseState.None)
                        AddError("Failed to determine and parse a value!");
                    return null;
                }
                expectValue = false;

                continue;
            consume_character:
                ConsumeChar();
            }

            if (currentValue != null) result.Add(currentValue);
            return result;
        }

        /**
         * Reads an inline table.
         * Assumes the cursor is at the start of the table definition. Reads all character until the table closing bracket.
         * 
         * Example:
         * { test = "foo", value = 1 }  ==>  { test = "foo", value = 1 }
         * ^                                                            ^
         */
        private TomlNode ReadInlineTable()
        {
            ConsumeChar();
            var result = new TomlTable { IsInline = true };
            TomlNode currentValue = null;
            var separator = false;
            var keyParts = new List<string>();
            int cur;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;

                if (c == TomlSyntax.INLINE_TABLE_END_SYMBOL)
                {
                    ConsumeChar();
                    break;
                }

                if (c == TomlSyntax.COMMENT_SYMBOL)
                {
                    AddError("Incomplete inline table definition!");
                    return null;
                }

                if (TomlSyntax.IsNewLine(c))
                {
                    AddError("Inline tables are only allowed to be on single line");
                    return null;
                }

                if (TomlSyntax.IsWhiteSpace(c))
                    goto consume_character;

                if (c == TomlSyntax.ITEM_SEPARATOR)
                {
                    if (currentValue == null)
                    {
                        AddError("Encountered multiple value separators in inline table!");
                        return null;
                    }

                    if (!InsertNode(currentValue, result, keyParts))
                        return null;
                    keyParts.Clear();
                    currentValue = null;
                    separator = true;
                    goto consume_character;
                }

                separator = false;
                currentValue = ReadKeyValuePair(keyParts);
                continue;

            consume_character:
                ConsumeChar();
            }

            if (separator)
            {
                AddError("Trailing commas are not allowed in inline tables.");
                return null;
            }

            if (currentValue != null && !InsertNode(currentValue, result, keyParts))
                return null;

            return result;
        }

        #endregion

        #region String parsing

        /**
         * Checks if the string value a multiline string (i.e. a triple quoted string).
         * Assumes the cursor is at the first quote character. Consumes the least amount of characters needed to determine if the string is multiline.
         * 
         * If the result is false, returns the consumed character through the `excess` variable.
         * 
         * Example 1:
         * """test"""  ==>  """test"""
         * ^                   ^
         * 
         * Example 2:
         * "test"  ==>  "test"         (doesn't return the first quote)
         * ^             ^
         * 
         * Example 3:
         * ""  ==>  ""        (returns the extra `"` through the `excess` variable)
         * ^          ^
         */
        private bool IsTripleQuote(char quote, out char excess)
        {
            // Copypasta, but it's faster...

            int cur;
            // Consume the first quote
            ConsumeChar();
            if ((cur = reader.Peek()) < 0)
            {
                excess = '\0';
                return AddError("Unexpected end of file!");
            }

            if ((char)cur != quote)
            {
                excess = '\0';
                return false;
            }

            // Consume the second quote
            excess = (char)ConsumeChar();
            if ((cur = reader.Peek()) < 0 || (char)cur != quote) return false;

            // Consume the final quote
            ConsumeChar();
            excess = '\0';
            return true;
        }

        /**
         * A convenience method to process a single character within a quote.
         */
        private bool ProcessQuotedValueCharacter(char quote,
                                                 bool isNonLiteral,
                                                 char c,
                                                 StringBuilder sb,
                                                 ref bool escaped)
        {
            if (TomlSyntax.MustBeEscaped(c))
                return AddError($"The character U+{(int)c:X8} must be escaped in a string!");

            if (escaped)
            {
                sb.Append(c);
                escaped = false;
                return false;
            }

            if (c == quote)
            {
                if (!isNonLiteral && reader.Peek() == quote)
                {
                    reader.Read();
                    col++;
                    sb.Append(quote);
                    return false;
                }

                return true;
            }
            if (isNonLiteral && c == TomlSyntax.ESCAPE_SYMBOL)
                escaped = true;
            if (c == TomlSyntax.NEWLINE_CHARACTER)
                return AddError("Encountered newline in single line string!");

            sb.Append(c);
            return false;
        }

        /**
         * Reads a single-line string.
         * Assumes the cursor is at the first character that belongs to the string.
         * Consumes all characters that belong to the string (including the closing quote).
         * 
         * Example:
         * "test"  ==>  "test"
         * ^                 ^
         */
        private string ReadQuotedValueSingleLine(char quote, char initialData = '\0')
        {
            var isNonLiteral = quote == TomlSyntax.BASIC_STRING_SYMBOL;
            var sb = new StringBuilder();
            var escaped = false;

            if (initialData != '\0')
            {
                var shouldReturn =
                    ProcessQuotedValueCharacter(quote, isNonLiteral, initialData, sb, ref escaped);
                if (currentState == ParseState.None) return null;
                if (shouldReturn)
                    if (isNonLiteral)
                    {
                        if (sb.ToString().TryUnescape(out var res, out var ex)) return res;
                        AddError(ex.Message);
                        return null;
                    }
                    else
                        return sb.ToString();
            }

            int cur;
            var readDone = false;
            while ((cur = reader.Read()) >= 0)
            {
                // Consume the character
                col++;
                var c = (char)cur;
                readDone = ProcessQuotedValueCharacter(quote, isNonLiteral, c, sb, ref escaped);
                if (readDone)
                {
                    if (currentState == ParseState.None) return null;
                    break;
                }
            }

            if (!readDone)
            {
                AddError("Unclosed string.");
                return null;
            }

            if (!isNonLiteral) return sb.ToString();
            if (sb.ToString().TryUnescape(out var unescaped, out var unescapedEx)) return unescaped;
            AddError(unescapedEx.Message);
            return null;
        }

        /**
         * Reads a multiline string.
         * Assumes the cursor is at the first character that belongs to the string.
         * Consumes all characters that belong to the string and the three closing quotes.
         * 
         * Example:
         * """test"""  ==>  """test"""
         * ^                       ^
         */
        private string ReadQuotedValueMultiLine(char quote)
        {
            var isBasic = quote == TomlSyntax.BASIC_STRING_SYMBOL;
            var sb = new StringBuilder();
            var escaped = false;
            var skipWhitespace = false;
            var skipWhitespaceLineSkipped = false;
            var quotesEncountered = 0;
            var first = true;
            int cur;
            while ((cur = ConsumeChar()) >= 0)
            {
                var c = (char)cur;
                if (TomlSyntax.MustBeEscaped(c, true))
                {
                    AddError($"The character U+{(int)c:X8} must be escaped!");
                    return null;
                }
                // Trim the first newline
                if (first && TomlSyntax.IsNewLine(c))
                {
                    if (TomlSyntax.IsLineBreak(c))
                        first = false;
                    else
                        AdvanceLine();
                    continue;
                }

                first = false;
                //TODO: Reuse ProcessQuotedValueCharacter
                // Skip the current character if it is going to be escaped later
                if (escaped)
                {
                    sb.Append(c);
                    escaped = false;
                    continue;
                }

                // If we are currently skipping empty spaces, skip
                if (skipWhitespace)
                {
                    if (TomlSyntax.IsEmptySpace(c))
                    {
                        if (TomlSyntax.IsLineBreak(c))
                        {
                            skipWhitespaceLineSkipped = true;
                            AdvanceLine();
                        }
                        continue;
                    }

                    if (!skipWhitespaceLineSkipped)
                    {
                        AddError("Non-whitespace character after trim marker.");
                        return null;
                    }

                    skipWhitespaceLineSkipped = false;
                    skipWhitespace = false;
                }

                // If we encounter an escape sequence...
                if (isBasic && c == TomlSyntax.ESCAPE_SYMBOL)
                {
                    var next = reader.Peek();
                    var nc = (char)next;
                    if (next >= 0)
                    {
                        // ...and the next char is empty space, we must skip all whitespaces
                        if (TomlSyntax.IsEmptySpace(nc))
                        {
                            skipWhitespace = true;
                            continue;
                        }

                        // ...and we have \" or \, skip the character
                        if (nc == quote || nc == TomlSyntax.ESCAPE_SYMBOL) escaped = true;
                    }
                }

                // Count the consecutive quotes
                if (c == quote)
                    quotesEncountered++;
                else
                    quotesEncountered = 0;

                // If the are three quotes, count them as closing quotes
                if (quotesEncountered == 3) break;

                sb.Append(c);
            }

            // TOML actually allows to have five ending quotes like
            // """"" => "" belong to the string + """ is the actual ending
            quotesEncountered = 0;
            while ((cur = reader.Peek()) >= 0)
            {
                var c = (char)cur;
                if (c == quote && ++quotesEncountered < 3)
                {
                    sb.Append(c);
                    ConsumeChar();
                }
                else break;
            }

            // Remove last two quotes (third one wasn't included by default)
            sb.Length -= 2;
            if (!isBasic) return sb.ToString();
            if (sb.ToString().TryUnescape(out var res, out var ex)) return res;
            AddError(ex.Message);
            return null;
        }

        #endregion

        #region Node creation

        private bool InsertNode(TomlNode node, TomlNode root, IList<string> path)
        {
            var latestNode = root;
            if (path.Count > 1)
                for (var index = 0; index < path.Count - 1; index++)
                {
                    var subkey = path[index];
                    if (latestNode.TryGetNode(subkey, out var currentNode))
                    {
                        if (currentNode.HasValue)
                            return AddError($"The key {".".Join(path)} already has a value assigned to it!");
                    }
                    else
                    {
                        currentNode = new TomlTable();
                        latestNode[subkey] = currentNode;
                    }

                    latestNode = currentNode;
                    if (latestNode is TomlTable { IsInline: true })
                        return AddError($"Cannot assign {".".Join(path)} because it will edit an immutable table.");
                }

            if (latestNode.HasKey(path[path.Count - 1]))
                return AddError($"The key {".".Join(path)} is already defined!");
            latestNode[path[path.Count - 1]] = node;
            node.CollapseLevel = path.Count - 1;
            return true;
        }

        private TomlTable CreateTable(TomlNode root, IList<string> path, bool arrayTable)
        {
            if (path.Count == 0) return null;
            var latestNode = root;
            for (var index = 0; index < path.Count; index++)
            {
                var subkey = path[index];

                if (latestNode.TryGetNode(subkey, out var node))
                {
                    if (node.IsArray && arrayTable)
                    {
                        var arr = (TomlArray)node;

                        if (!arr.IsTableArray)
                        {
                            AddError($"The array {".".Join(path)} cannot be redefined as an array table!");
                            return null;
                        }

                        if (index == path.Count - 1)
                        {
                            latestNode = new TomlTable();
                            arr.Add(latestNode);
                            break;
                        }

                        latestNode = arr[arr.ChildrenCount - 1];
                        continue;
                    }

                    if (node is TomlTable { IsInline: true })
                    {
                        AddError($"Cannot create table {".".Join(path)} because it will edit an immutable table.");
                        return null;
                    }

                    if (node.HasValue)
                    {
                        if (!(node is TomlArray { IsTableArray: true } array))
                        {
                            AddError($"The key {".".Join(path)} has a value assigned to it!");
                            return null;
                        }

                        latestNode = array[array.ChildrenCount - 1];
                        continue;
                    }

                    if (index == path.Count - 1)
                    {
                        if (arrayTable && !node.IsArray)
                        {
                            AddError($"The table {".".Join(path)} cannot be redefined as an array table!");
                            return null;
                        }

                        if (node is TomlTable { isImplicit: false })
                        {
                            AddError($"The table {".".Join(path)} is defined multiple times!");
                            return null;
                        }
                    }
                }
                else
                {
                    if (index == path.Count - 1 && arrayTable)
                    {
                        var table = new TomlTable();
                        var arr = new TomlArray
                        {
                            IsTableArray = true
                        };
                        arr.Add(table);
                        latestNode[subkey] = arr;
                        latestNode = table;
                        break;
                    }

                    node = new TomlTable { isImplicit = true };
                    latestNode[subkey] = node;
                }

                latestNode = node;
            }

            var result = (TomlTable)latestNode;
            result.isImplicit = false;
            return result;
        }

        #endregion

        #region Misc parsing

        private string ParseComment()
        {
            ConsumeChar();
            var commentLine = reader.ReadLine()?.Trim() ?? "";
            if (commentLine.Any(ch => TomlSyntax.MustBeEscaped(ch)))
                AddError("Comment must not contain control characters other than tab.", false);
            return commentLine;
        }
        #endregion
    }

    #endregion

    public static class TOML
    {
        public static bool ForceASCII { get; set; } = false;

        public static TomlTable Parse(TextReader reader)
        {
            using var parser = new TOMLParser(reader) { ForceASCII = ForceASCII };
            return parser.Parse();
        }
    }

    #region Exception Types

    public class TomlFormatException : Exception
    {
        public TomlFormatException(string message) : base(message) { }
    }

    public class TomlParseException : Exception
    {
        public TomlParseException(TomlTable parsed, IEnumerable<TomlSyntaxException> exceptions) :
            base("TOML file contains format errors")
        {
            ParsedTable = parsed;
            SyntaxErrors = exceptions;
        }

        public TomlTable ParsedTable { get; }

        public IEnumerable<TomlSyntaxException> SyntaxErrors { get; }
    }

    public class TomlSyntaxException : Exception
    {
        public TomlSyntaxException(string message, TOMLParser.ParseState state, int line, int col) : base(message)
        {
            ParseState = state;
            Line = line;
            Column = col;
        }

        public TOMLParser.ParseState ParseState { get; }

        public int Line { get; }

        public int Column { get; }
    }

    #endregion

    #region Parse utilities

    internal static class TomlSyntax
    {
        #region Type Patterns

        public const string TRUE_VALUE = "true";
        public const string FALSE_VALUE = "false";
        public const string NAN_VALUE = "nan";
        public const string POS_NAN_VALUE = "+nan";
        public const string NEG_NAN_VALUE = "-nan";
        public const string INF_VALUE = "inf";
        public const string POS_INF_VALUE = "+inf";
        public const string NEG_INF_VALUE = "-inf";

        public static bool IsBoolean(string s) => s is TRUE_VALUE or FALSE_VALUE;

        public static bool IsPosInf(string s) => s is INF_VALUE or POS_INF_VALUE;

        public static bool IsNegInf(string s) => s == NEG_INF_VALUE;

        public static bool IsNaN(string s) => s is NAN_VALUE or POS_NAN_VALUE or NEG_NAN_VALUE;

        public static bool IsInteger(string s) => IntegerPattern.IsMatch(s);

        public static bool IsFloat(string s) => FloatPattern.IsMatch(s);

        public static bool IsIntegerWithBase(string s, out int numberBase)
        {
            numberBase = 10;
            var match = BasedIntegerPattern.Match(s);
            if (!match.Success) return false;
            IntegerBases.TryGetValue(match.Groups["base"].Value, out numberBase);
            return true;
        }

        /**
         * A pattern to verify the integer value according to the TOML specification.
         */
        public static readonly Regex IntegerPattern =
            new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)*)$", RegexOptions.Compiled);

        /**
         * A pattern to verify a special 0x, 0o and 0b forms of an integer according to the TOML specification.
         */
        public static readonly Regex BasedIntegerPattern =
            new(@"^0(?<base>x|b|o)(?!_)(_?[0-9A-F])*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        /**
         * A pattern to verify the float value according to the TOML specification.
         */
        public static readonly Regex FloatPattern =
            new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)+)(((e(\+|-)?(?!_)(_?\d)+)?)|(\.(?!_)(_?\d)+(e(\+|-)?(?!_)(_?\d)+)?))$",
                RegexOptions.Compiled | RegexOptions.IgnoreCase);

        /**
         * A helper dictionary to map TOML base codes into the radii.
         */
        public static readonly Dictionary<string, int> IntegerBases = new()
        {
            ["x"] = 16,
            ["o"] = 8,
            ["b"] = 2
        };

        /**
         * A helper dictionary to map non-decimal bases to their TOML identifiers
         */
        public static readonly Dictionary<int, string> BaseIdentifiers = new()
        {
            [2] = "b",
            [8] = "o",
            [16] = "x"
        };

        public const string RFC3339EmptySeparator = " ";
        public const string ISO861Separator = "T";
        public const string ISO861ZeroZone = "+00:00";
        public const string RFC3339ZeroZone = "Z";

        /**
         * Valid date formats with timezone as per RFC3339.
         */
        public static readonly string[] RFC3339Formats =
        {
            "yyyy'-'MM-ddTHH':'mm':'ssK", "yyyy'-'MM-ddTHH':'mm':'ss'.'fK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffK",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffK",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffffK",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffffK"
        };

        /**
         * Valid date formats without timezone (assumes local) as per RFC3339.
         */
        public static readonly string[] RFC3339LocalDateTimeFormats =
        {
            "yyyy'-'MM-ddTHH':'mm':'ss", "yyyy'-'MM-ddTHH':'mm':'ss'.'f", "yyyy'-'MM-ddTHH':'mm':'ss'.'ff",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffff",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffff",
            "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffff"
        };

        /**
         * Valid full date format as per TOML spec.
         */
        public static readonly string LocalDateFormat = "yyyy'-'MM'-'dd";

        /**
         * Valid time formats as per TOML spec.
         */
        public static readonly string[] RFC3339LocalTimeFormats =
        {
            "HH':'mm':'ss", "HH':'mm':'ss'.'f", "HH':'mm':'ss'.'ff", "HH':'mm':'ss'.'fff", "HH':'mm':'ss'.'ffff",
            "HH':'mm':'ss'.'fffff", "HH':'mm':'ss'.'ffffff", "HH':'mm':'ss'.'fffffff"
        };

        #endregion

        #region Character definitions

        public const char ARRAY_END_SYMBOL = ']';
        public const char ITEM_SEPARATOR = ',';
        public const char ARRAY_START_SYMBOL = '[';
        public const char BASIC_STRING_SYMBOL = '\"';
        public const char COMMENT_SYMBOL = '#';
        public const char ESCAPE_SYMBOL = '\\';
        public const char KEY_VALUE_SEPARATOR = '=';
        public const char NEWLINE_CARRIAGE_RETURN_CHARACTER = '\r';
        public const char NEWLINE_CHARACTER = '\n';
        public const char SUBKEY_SEPARATOR = '.';
        public const char TABLE_END_SYMBOL = ']';
        public const char TABLE_START_SYMBOL = '[';
        public const char INLINE_TABLE_START_SYMBOL = '{';
        public const char INLINE_TABLE_END_SYMBOL = '}';
        public const char LITERAL_STRING_SYMBOL = '\'';
        public const char INT_NUMBER_SEPARATOR = '_';

        public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER };

        public static bool IsQuoted(char c) => c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL;

        public static bool IsWhiteSpace(char c) => c is ' ' or '\t';

        public static bool IsNewLine(char c) => c is NEWLINE_CHARACTER or NEWLINE_CARRIAGE_RETURN_CHARACTER;

        public static bool IsLineBreak(char c) => c == NEWLINE_CHARACTER;

        public static bool IsEmptySpace(char c) => IsWhiteSpace(c) || IsNewLine(c);

        public static bool IsBareKey(char c) =>
            c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_' or '-';

        public static bool MustBeEscaped(char c, bool allowNewLines = false)
        {
            var result = c is (>= '\u0000' and <= '\u0008') or '\u000b' or '\u000c' or (>= '\u000e' and <= '\u001f') or '\u007f';
            if (!allowNewLines)
                result |= c is >= '\u000a' and <= '\u000e';
            return result;
        }

        public static bool IsValueSeparator(char c) =>
            c is ITEM_SEPARATOR or ARRAY_END_SYMBOL or INLINE_TABLE_END_SYMBOL;

        #endregion
    }

    internal static class StringUtils
    {
        public static string AsKey(this string key)
        {
            var quote = key == string.Empty || key.Any(c => !TomlSyntax.IsBareKey(c));
            return !quote ? key : $"{TomlSyntax.BASIC_STRING_SYMBOL}{key.Escape()}{TomlSyntax.BASIC_STRING_SYMBOL}";
        }

        public static string Join(this string self, IEnumerable<string> subItems)
        {
            var sb = new StringBuilder();
            var first = true;

            foreach (var subItem in subItems)
            {
                if (!first) sb.Append(self);
                first = false;
                sb.Append(subItem);
            }

            return sb.ToString();
        }

        public delegate bool TryDateParseDelegate<T>(string s, string format, IFormatProvider ci, DateTimeStyles dts, out T dt);

        public static bool TryParseDateTime<T>(string s,
                                               string[] formats,
                                               DateTimeStyles styles,
                                               TryDateParseDelegate<T> parser,
                                               out T dateTime,
                                               out int parsedFormat)
        {
            parsedFormat = 0;
            dateTime = default;
            for (var i = 0; i < formats.Length; i++)
            {
                var format = formats[i];
                if (!parser(s, format, CultureInfo.InvariantCulture, styles, out dateTime)) continue;
                parsedFormat = i;
                return true;
            }

            return false;
        }

        public static void AsComment(this string self, TextWriter tw)
        {
            foreach (var line in self.Split(TomlSyntax.NEWLINE_CHARACTER))
                tw.WriteLine($"{TomlSyntax.COMMENT_SYMBOL} {line.Trim()}");
        }

        public static string RemoveAll(this string txt, char toRemove)
        {
            var sb = new StringBuilder(txt.Length);
            foreach (var c in txt.Where(c => c != toRemove))
                sb.Append(c);
            return sb.ToString();
        }

        public static string Escape(this string txt, bool escapeNewlines = true)
        {
            var stringBuilder = new StringBuilder(txt.Length + 2);
            for (var i = 0; i < txt.Length; i++)
            {
                var c = txt[i];

                static string CodePoint(string txt, ref int i, char c) => char.IsSurrogatePair(txt, i)
                    ? $"\\U{char.ConvertToUtf32(txt, i++):X8}"
                    : $"\\u{(ushort)c:X4}";

                stringBuilder.Append(c switch
                {
                    '\b' => @"\b",
                    '\t' => @"\t",
                    '\n' when escapeNewlines => @"\n",
                    '\f' => @"\f",
                    '\r' when escapeNewlines => @"\r",
                    '\\' => @"\\",
                    '\"' => @"\""",
                    var _ when TomlSyntax.MustBeEscaped(c, !escapeNewlines) || TOML.ForceASCII && c > sbyte.MaxValue =>
                        CodePoint(txt, ref i, c),
                    var _ => c
                });
            }

            return stringBuilder.ToString();
        }

        public static bool TryUnescape(this string txt, out string unescaped, out Exception exception)
        {
            try
            {
                exception = null;
                unescaped = txt.Unescape();
                return true;
            }
            catch (Exception e)
            {
                exception = e;
                unescaped = null;
                return false;
            }
        }

        public static string Unescape(this string txt)
        {
            if (string.IsNullOrEmpty(txt)) return txt;
            var stringBuilder = new StringBuilder(txt.Length);
            for (var i = 0; i < txt.Length;)
            {
                var num = txt.IndexOf('\\', i);
                var next = num + 1;
                if (num < 0 || num == txt.Length - 1) num = txt.Length;
                stringBuilder.Append(txt, i, num - i);
                if (num >= txt.Length) break;
                var c = txt[next];

                static string CodePoint(int next, string txt, ref int num, int size)
                {
                    if (next + size >= txt.Length) throw new Exception("Undefined escape sequence!");
                    num += size;
                    return char.ConvertFromUtf32(Convert.ToInt32(txt.Substring(next + 1, size), 16));
                }

                stringBuilder.Append(c switch
                {
                    'b' => "\b",
                    't' => "\t",
                    'n' => "\n",
                    'f' => "\f",
                    'r' => "\r",
                    '\'' => "\'",
                    '\"' => "\"",
                    '\\' => "\\",
                    'u' => CodePoint(next, txt, ref num, 4),
                    'U' => CodePoint(next, txt, ref num, 8),
                    var _ => throw new Exception("Undefined escape sequence!")
                });
                i = num + 2;
            }

            return stringBuilder.ToString();
        }
    }

    #endregion
}

```
Page 10/13FirstPrevNextLast