#include "ChronoWebServer.h"
#include <WebServer.h>
#include <ProjectileProfile.h>
#include <ChronoReadingManager.h>
#include <FileManager.h>
#include <ChronoProfile.h>
#include <GunProfile.h>
#include <Update.h>
#include "Display.h"
#include <nvs_flash.h>

static const char *SSID_KEY = "wifi_ssid";
static const char *PASSWORD_KEY = "wifi_password";
static Preferences preferences;

String getWifiSsid() {
    String ssid = "";
    if (preferences.begin("wifi", true)) {
        ssid = preferences.getString(SSID_KEY, "");
        preferences.end();
    }
    return ssid;
}

String getWifiPassword() {
    String password = "";
    if (preferences.begin("wifi", true)) {
        password = preferences.getString(PASSWORD_KEY, "");
        preferences.end();
    }
    return password;
}

void saveWifiCredentials(const String& ssid, const String& password) {
    preferences.begin("wifi", false);
    preferences.putString(SSID_KEY, ssid);
    preferences.putString(PASSWORD_KEY, password);
    preferences.end();
}

WebServer _server(80);
void (*testModeCallback)();

String escapeJsonString(const char* src, int maxLen) {
  String out;
  out.reserve(maxLen * 2);
  for (int i = 0; i < maxLen; i++) {
    char c = src[i];
    if (c == '\0') break;
    if (c == '"') { out += "\\\""; }
    else if (c == '\\') { out += "\\\\"; }
    else if ((unsigned char)c < 0x20) { continue; }
    else { out += c; }
  }
  return out;
}

// Constructor implementation
ChronoWebServer::ChronoWebServer() {}
void ChronoWebServer::_handleRoot() {
    String temp="<html><head><title>ChronoDisplay</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><style>:root{--bg:#f5f5f5;--text:#333;--primary:#2196F3;--secondary:#e3f2fd;--surface:#fff;--border:#ddd;--shadow:0 2px 4px rgba(0,0,0,0.1);--accent:#4CAF50;--danger:#f44336}[data-theme='dark']{--bg:#1a1a2e;--text:#e0e0e0;--primary:#4a9eff;--secondary:#16213e;--surface:#16213e;--border:#2d3748;--danger:#e53935}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;margin:0}header{background:var(--surface);padding:12px 20px;display:flex;justify-content:space-between;align-items:center;box-shadow:var(--shadow);position:sticky;top:0;z-index:100}header h1{font-size:1.4rem;font-weight:600;color:var(--primary);margin:0}.theme-toggle{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:6px 14px;border-radius:16px;cursor:pointer;font-size:0.85rem;display:flex;align-items:center;gap:6px}.theme-toggle:hover{background:var(--secondary);border-color:var(--primary)}.tabs{display:flex;background:var(--surface);border-bottom:1px solid var(--border);padding:0 20px;flex-wrap:wrap}.tab{padding:12px 20px;cursor:pointer;border:none;border-bottom:3px solid transparent;background:none;color:var(--text);font-size:0.95rem;font-weight:500}.tab:hover{background:var(--secondary)}.tab.active{border-bottom-color:var(--primary);color:var(--primary)}.tab-content{display:none;padding:20px}.tab-content.active{display:block}.container{max-width:1200px;margin:0 auto}.card{background:var(--surface);border-radius:12px;padding:20px;margin-bottom:20px;box-shadow:var(--shadow);position:relative}.loading-overlay{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.8);display:flex;align-items:center;justify-content:center;border-radius:12px;z-index:10;opacity:0;pointer-events:none;transition:opacity 0.2s}.loading-overlay.active{opacity:1;pointer-events:auto}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.loading-spinner{width:40px;height:40px;border:4px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:spin 1s linear infinite}.card h2{font-size:1.15rem;margin-bottom:12px;color:var(--text);border-bottom:2px solid var(--border);padding-bottom:10px}.metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px}.metric-card{text-align:center;padding:12px;border-radius:10px;background:var(--surface);box-shadow:var(--shadow)}.metric-value{font-size:1.4rem;font-weight:700;color:var(--primary);margin-bottom:3px}.metric-label{font-size:0.75rem;color:var(--text);opacity:0.8}.data-table{width:100%;border-collapse:collapse;background:var(--surface);border-radius:10px;overflow:hidden}.data-table th,.data-table td{padding:8px 10px;border-bottom:1px solid var(--border);text-align:left;font-size:0.85rem}.data-table th{background:var(--secondary);font-weight:600}.data-table tr:hover{background:var(--secondary)}.btn{padding:8px 14px;border:none;border-radius:6px;font-size:0.85rem;cursor:pointer}.btn-primary{background:var(--primary);color:#fff}.btn-primary:hover{background:#1976D2}.btn-danger{background:var(--danger);color:#fff}.btn-danger:hover{background:#d32f2f}.btn-success{background:var(--accent);color:#fff}.btn-success:hover{background:#388E3C}.form-group{margin-bottom:10px}.form-group label{display:block;margin-bottom:4px;font-size:0.8rem;color:var(--text)}.form-group input,.form-group select{width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font-size:0.85rem}.control-panel{display:flex;gap:8px;flex-wrap:wrap}.status-bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--surface);border-radius:8px;margin-bottom:12px;font-size:0.8rem}.status-bar strong{color:var(--primary)}.empty-state{text-align:center;padding:30px 15px;opacity:0.7}.empty-state p{margin-top:6px}.add-btn{position:absolute;top:15px;right:15px}.pagination{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:12px;padding:12px 0;border-top:1px solid var(--border)}.pagination select{padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font-size:0.85rem}.pagination .page-info{font-size:0.85rem;opacity:0.8}.pagination button{padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);cursor:pointer;font-size:0.85rem}.pagination button:disabled{opacity:0.5;cursor:not-allowed}.pagination button:hover:not(:disabled){background:var(--secondary)}.reading-history-controls{position:absolute;top:15px;right:15px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;background:var(--surface);padding:8px 12px;border-radius:8px;border:1px solid var(--border)}</style></head><body><header><h1>ChronoDisplay</h1><button class='theme-toggle' onclick='toggleTheme()' id='themeToggle'>Dark</button></header><div class='tabs'><button class='tab active' data-tab='live' onclick='switchTab(this.getAttribute(\"data-tab\"))'>Live Data</button><button class='tab' data-tab='gun-profiles' onclick='switchTab(this.getAttribute(\"data-tab\"))'>Gun Profiles</button><button class='tab' data-tab='projectile-profiles' onclick='switchTab(this.getAttribute(\"data-tab\"))'>Projectile Profiles</button><button class='tab' data-tab='settings' onclick='switchTab(this.getAttribute(\"data-tab\"))'>Settings</button></div><div class='container'><div id='live' class='tab-content active'><div class='card'><h2>Latest Reading</h2><div class='metric-grid'><div class='metric-card'><div class='metric-value' id='latestFPS'>--</div><div class='metric-label'>FPS</div></div><div class='metric-card'><div class='metric-value' id='latestFPE'>--</div><div class='metric-label'>FPE</div></div><div class='metric-card'><div class='metric-value' id='shotCount'>0</div><div class='metric-label'>Shots</div></div><div class='metric-card'><div class='metric-value' id='avgFPS'>--</div><div class='metric-label'>Avg</div></div><div class='metric-card'><div class='metric-value' id='lowFPS'>--</div><div class='metric-label'>Low</div></div><div class='metric-card'><div class='metric-value' id='highFPS'>--</div><div class='metric-label'>High</div></div></div></div><div class='card'><h2>Reading History</h2><div class='reading-history-controls'><label style='display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.9rem;'><input type='checkbox' id='pollingToggle' onchange='togglePolling(this.checked)'><span>Auto-refresh</span></label><select id='pollingInterval' style='padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font-size:0.9rem;' onchange='updatePollingInterval(this.value)'><option value='5'>5 seconds</option><option value='10' selected>10 seconds</option><option value='15'>15 seconds</option><option value='30'>30 seconds</option></select><span id='pollingStatus' style='font-size:0.8rem;opacity:0.7'>Polling: OFF</span></div><div style='overflow-x:auto;'><table class='data-table' id='readingHistory'><thead><tr><th>Shot</th><th>FPS</th><th>FPE</th><th>Gun</th><th>Power</th><th>Projectile</th><th>Cal</th><th>Wt</th></tr></thead><tbody id='historyBody'></tbody></table></div><div id='emptyHistory' class='empty-state' style='display:none'><p>No readings yet</p></div><div class='pagination'><label style='display:flex;align-items:center;gap:6px;font-size:0.85rem;white-space:nowrap;'><span>Per page:</span><select id='itemsPerPage'><option value='10' selected>10</option><option value='25'>25</option><option value='50'>50</option></select></label><div class='page-info'><span id='currentPageInfo'>Showing 0-0 of 0</span></div><div><button id='prevPage' onclick='prevPage()' disabled>Previous</button><button id='nextPage' onclick='nextPage()' disabled>Next</button></div></div></div></div></div><div id='gun-profiles' class='tab-content'><div class='card'><div class='loading-overlay' id='gunProfileLoading'><div class='loading-spinner'></div></div><h2>Gun Profiles</h2><button class='btn btn-primary add-btn' onclick='addGunProfile()'>Add</button><div style='overflow-x:auto;'><table class='data-table'><thead><tr><th>Name</th><th>Type</th><th></th></tr></thead><tbody id='gunProfilesBody'></tbody></table></div><div id='emptyGunProfiles' class='empty-state' style='display:none'><p>No profiles</p></div></div></div><div id='projectile-profiles' class='tab-content'><div class='card'><div class='loading-overlay' id='projectileProfileLoading'><div class='loading-spinner'></div></div><h2>Projectile Profiles</h2><button class='btn btn-primary add-btn' onclick='addProjectileProfile()'>Add</button><div style='overflow-x:auto;'><table class='data-table'><thead><tr><th>Name</th><th>Type</th><th>Cal</th><th>Wt</th><th></th></tr></thead><tbody id='projectileProfilesBody'></tbody></table></div><div id='emptyProjectileProfiles' class='empty-state' style='display:none'><p>No profiles</p></div></div></div><div id='settings' class='tab-content'><div class='card'><h2>WiFi</h2><div class='status-bar'><div>Mode: <strong id='wifiMode'>?</strong></div><div>WiFi: <strong id='wifiSsid'>--</strong></div></div><div id='wifiFormContainer' style='margin-top:10px;'><div class='form-group'><label>SSID</label><input type='text' id='wifiSsidInput' placeholder='Enter SSID'></div><div class='form-group'><label>Password</label><input type='password' id='wifiPasswordInput' placeholder='Enter password'></div><button class='btn btn-primary' onclick='saveWifi()'>Save</button></div><p style='margin-top:10px;font-size:0.75rem;opacity:0.7'>After saving, device will restart.</p><button class='btn btn-danger' onclick='clearWifi()' style='margin-top:6px;'>Clear WiFi</button></div><div class='card'><h2>OTA Update</h2><div class='status-bar'><div>Version: <strong id='currentVersion'>?</strong></div></div><div class='form-group'><label>Firmware File</label><input type='file' id='otaFile' accept='.bin'></div><button class='btn btn-success' onclick='doOta()'>OTA Upgrade</button></div><div class='card'><h2>Control</h2><div class='control-panel'><button class='btn btn-primary' onclick='exportData()'>Export Data</button><button class='btn btn-danger' onclick='resetData()'>Reset Data</button><button class='btn btn-danger' onclick='formatProfiles()'>Delete Profiles</button><button class='btn btn-primary' onclick='toggleTestMode()'>Test Mode</button></div></div></div></div><script>var currentPage=1;var itemsPerPage=10;var totalReadings=0;var allReadings=[];function toggleTheme(){const t=document.getElementById('themeToggle'),d=document.documentElement,c=d.getAttribute('data-theme'),n=c==='dark'?'light':'dark';d.setAttribute('data-theme',n);t.textContent=n==='dark'?'Light':'Dark';localStorage.setItem('chronoTheme',n)}function showLoadingOverlay(id){var el=document.getElementById(id);if(el)el.classList.add('active')}function hideLoadingOverlay(id){var el=document.getElementById(id);if(el)el.classList.remove('active')}function switchTab(e){console.log('switchTab:',e);var tabs=document.querySelectorAll('.tab');console.log('tabs:',tabs.length);var contents=document.querySelectorAll('.tab-content');console.log('contents:',contents.length);for(var i=0;i<tabs.length;i++){tabs[i].classList.remove('active');contents[i].classList.remove('active')}for(var i=0;i<tabs.length;i++){if(tabs[i].getAttribute('data-tab')===e){tabs[i].classList.add('active');break}}var content=document.getElementById(e);content.classList.add('active');console.log('switchTab done');if(e==='gun-profiles'){loadGunProfiles(null)}if(e==='projectile-profiles'){loadProjectileProfiles(null)}}function addGunProfile(){window.location.href='/gunprofile'}function addProjectileProfile(){window.location.href='/projectileprofile'}function setProfile(t,e){var n=new URLSearchParams();n.append('id',e);var r=t==='gun'?'/gunprofile/set':'/projectileprofile/set';var overlayId=t==='gun'?'gunProfileLoading':'projectileProfileLoading';showLoadingOverlay(overlayId);fetch(r,{method:'POST',body:n}).then(r=>r.text()).then(e=>{console.log(e);hideLoadingOverlay(overlayId);t==='gun'?loadGunProfiles(null):loadProjectileProfiles(null)}).catch(err=>{hideLoadingOverlay(overlayId);console.error('Error:',err)})}function exportData(){var e=document.createElement('a');e.href='/export';e.setAttribute('download','data.csv');document.body.appendChild(e);e.click();document.body.removeChild(e)}function resetData(){if(confirm('Reset all data?')){fetch('/reset',{method:'GET'}).then(r=>r.text()).then(d=>{alert('Data reset')})}}function formatProfiles(){if(confirm('Delete all profiles?')){fetch('/format',{method:'GET'}).then(r=>r.text()).then(d=>{alert('Profiles deleted')})}}function toggleTestMode(){fetch('/testmode',{method:'GET'})}function doOta(){var e=document.getElementById('otaFile');if(!e.files.length){alert('Select firmware file');return}var n=new FormData();n.append('otaFile',e.files[0]);var r=new XMLHttpRequest();r.open('POST','/ota',true);r.onload=function(){r.status>=200&&r.status<300&&(window.location.href='/')};r.onerror=function(){console.error('Error')};r.send(n)}function saveWifi(){var e=document.getElementById('wifiSsidInput').value,n=document.getElementById('wifiPasswordInput').value;if(!e){alert('Enter SSID');return}var r=new URLSearchParams();r.append('ssid',e);r.append('password',n);fetch('/wifisave',{method:'POST',body:r}).then(r=>r.text()).then(d=>{alert('WiFi saved!');window.location.href='/'})}function clearWifi(){if(confirm('Clear WiFi?')){fetch('/clearwifi',{method:'POST',body:new URLSearchParams()}).then(r=>r.text()).then(d=>{alert('WiFi cleared')})}}function loadInitData(){fetch('/initdata').then(r=>r.json()).then(data=>{document.getElementById('currentVersion').textContent=data.version.version;var ssid=data.wifiSsid||'--';document.getElementById('wifiSsid').textContent=ssid;var mode=data.wifiMode;if(mode==='AP'){document.getElementById('wifiMode').textContent='AP Mode';document.getElementById('wifiFormContainer').style.display='block'}else if(mode==='STA'){document.getElementById('wifiMode').textContent='STA Mode';document.getElementById('wifiFormContainer').style.display='block'}else{document.getElementById('wifiMode').textContent='?'}}).catch(err=>{console.error('Error loading init data:',err)});var t=localStorage.getItem('chronoTheme')||'light';document.documentElement.setAttribute('data-theme',t);document.getElementById('themeToggle').textContent=t==='dark'?'Light':'Dark'}function loadLatestData(){console.log('loadLatestData');var tbody=document.getElementById('historyBody');var empty=document.getElementById('emptyHistory');fetch('/readings').then(r=>r.json()).then(data=>{allReadings=data;totalReadings=data.length;document.getElementById('shotCount').textContent=totalReadings;if(totalReadings>0){var latest=allReadings[0];document.getElementById('latestFPS').textContent=latest.fps;document.getElementById('latestFPE').textContent=latest.fpe;var fpsValues=allReadings.map(function(r){return r.fps});var avg=fpsValues.reduce(function(a,b){return a+b},0)/fpsValues.length;var low=Math.min.apply(null,fpsValues);var high=Math.max.apply(null,fpsValues);document.getElementById('avgFPS').textContent=avg.toFixed(0);document.getElementById('lowFPS').textContent=low;document.getElementById('highFPS').textContent=high}else{document.getElementById('latestFPS').textContent='---';document.getElementById('latestFPE').textContent='---';document.getElementById('avgFPS').textContent='---';document.getElementById('lowFPS').textContent='---';document.getElementById('highFPS').textContent='---'}currentPage=1;itemsPerPage=parseInt(document.getElementById('itemsPerPage').value);renderHistory();console.log('Loaded',totalReadings,'readings')}).catch(err=>{console.error('Error loading readings:',err);document.getElementById('latestFPS').textContent='---';document.getElementById('latestFPE').textContent='---';document.getElementById('avgFPS').textContent='---';document.getElementById('lowFPS').textContent='---';document.getElementById('highFPS').textContent='---';empty.style.display='block'})}function renderHistory(){var tbody=document.getElementById('historyBody');var empty=document.getElementById('emptyHistory');var startIndex=(currentPage-1)*itemsPerPage;var endIndex=Math.min(startIndex+itemsPerPage,totalReadings);var paginatedData=allReadings.slice(startIndex,endIndex);empty.style.display='none';tbody.innerHTML='';if(totalReadings===0){empty.style.display='block';updatePaginationControls(0,0);return}paginatedData.forEach(function(r){var row='<tr><td>'+r.shot+'</td><td>'+r.fps+'</td><td>'+r.fpe+'</td><td>'+r.gun+'</td><td>'+r.power+'</td><td>'+r.projectile+'</td><td>'+r.cal+'</td><td>'+r.weight+'</td></tr>';tbody.innerHTML+=row});updatePaginationControls(startIndex+1,endIndex);console.log('Rendered page',currentPage,'of',Math.ceil(totalReadings/itemsPerPage))}function updatePaginationControls(start,end){document.getElementById('currentPageInfo').textContent='Showing '+start+'-'+end+' of '+totalReadings;document.getElementById('prevPage').disabled=currentPage<=1;document.getElementById('nextPage').disabled=end>=totalReadings}function prevPage(){if(currentPage>1){currentPage--;renderHistory()}}function nextPage(){var totalPages=Math.ceil(totalReadings/itemsPerPage);if(currentPage<totalPages){currentPage++;renderHistory()}}function loadGunProfiles(currentData){console.log('loadGunProfiles');var tbody=document.getElementById('gunProfilesBody');var empty=document.getElementById('emptyGunProfiles');showLoadingOverlay('gunProfileLoading');fetch('/gunprofile/get').then(r=>r.json()).then(data=>{hideLoadingOverlay('gunProfileLoading');var profiles=data.profiles||[];var current=data.current||{id:-1,name:''};empty.style.display='none';tbody.innerHTML='';if(profiles.length===0){empty.style.display='block';return}profiles.forEach(function(p){var isSelected=(current.name && p.id===current.id)?'style=\\'background:var(--secondary);font-weight:bold;\\'':'';var row='<tr '+isSelected+'><td onclick=\\'setProfile(\"gun\",'+p.id+')\\' style=\\'cursor:pointer;\\'>'+p.name+'</td><td onclick=\\'setProfile(\"gun\",'+p.id+')\\' style=\\'cursor:pointer;\\'>'+p.type+'</td><td><button class=\\'remove-btn\\' onclick=\\'removeGunProfile('+p.id+')\\'>Remove</button></td></tr>';tbody.innerHTML+=row});console.log('Loaded',profiles.length,'gun profiles')}).catch(err=>{hideLoadingOverlay('gunProfileLoading');console.error('Error loading gun profiles:',err);empty.style.display='block'})}function loadProjectileProfiles(currentData){console.log('loadProjectileProfiles');var tbody=document.getElementById('projectileProfilesBody');var empty=document.getElementById('emptyProjectileProfiles');showLoadingOverlay('projectileProfileLoading');fetch('/projectileprofile/get').then(r=>r.json()).then(data=>{hideLoadingOverlay('projectileProfileLoading');var profiles=data.profiles||[];var current=data.current||{id:-1,name:''};empty.style.display='none';tbody.innerHTML='';if(profiles.length===0){empty.style.display='block';return}profiles.forEach(function(p){var isSelected=(current.name && p.id===current.id)?'style=\\'background:var(--secondary);font-weight:bold;\\'':'';var row='<tr '+isSelected+'><td onclick=\\'setProfile(\"projectile\",'+p.id+')\\' style=\\'cursor:pointer;\\'>'+p.name+'</td><td onclick=\\'setProfile(\"projectile\",'+p.id+')\\' style=\\'cursor:pointer;\\'>'+p.type+'</td><td onclick=\\'setProfile(\"projectile\",'+p.id+')\\' style=\\'cursor:pointer;\\'>'+p.cal+'</td><td onclick=\\'setProfile(\"projectile\",'+p.id+')\\' style=\\'cursor:pointer;\\'>'+p.weight+'</td><td><button class=\\'remove-btn\\' onclick=\\'removeProjectileProfile('+p.id+')\\'>Remove</button></td></tr>';tbody.innerHTML+=row});console.log('Loaded',profiles.length,'projectile profiles')}).catch(err=>{hideLoadingOverlay('projectileProfileLoading');console.error('Error loading projectile profiles:',err);empty.style.display='block'})}function removeGunProfile(id){if(confirm('Remove this profile?')){var n=new URLSearchParams();n.append('id',id);showLoadingOverlay('gunProfileLoading');fetch('/gunprofile/remove',{method:'POST',body:n}).then(r=>r.text()).then(d=>{console.log(d);hideLoadingOverlay('gunProfileLoading');loadGunProfiles(null);}).catch(e=>{hideLoadingOverlay('gunProfileLoading');console.error('Error:',e)})}}function removeProjectileProfile(id){if(confirm('Remove this profile?')){var n=new URLSearchParams();n.append('id',id);showLoadingOverlay('projectileProfileLoading');fetch('/projectileprofile/remove',{method:'POST',body:n}).then(r=>r.text()).then(d=>{console.log(d);hideLoadingOverlay('projectileProfileLoading');loadProjectileProfiles(null);}).catch(e=>{hideLoadingOverlay('projectileProfileLoading');console.error('Error:',e)})}}var pollingInterval;var pollingEnabled=false;var pollingSeconds=10;function startPolling(){if(pollingEnabled){clearInterval(pollingInterval);pollingInterval=setInterval(function(){console.log('Auto-refresh...');loadLatestData()},pollingSeconds*1000);document.getElementById('pollingStatus').textContent='Polling: ON ('+pollingSeconds+'s)';console.log('Started polling every '+pollingSeconds+' seconds');}}function stopPolling(){clearInterval(pollingInterval);document.getElementById('pollingStatus').textContent='Polling: OFF';console.log('Stopped polling');}function togglePolling(enabled){pollingEnabled=enabled;if(enabled){startPolling()}else{stopPolling()}}function updatePollingInterval(seconds){pollingSeconds=parseInt(seconds);if(pollingEnabled){startPolling()}}document.addEventListener('DOMContentLoaded',function(){console.log('DOMContentLoaded');loadInitData();loadLatestData();document.getElementById('itemsPerPage').addEventListener('change',function(){itemsPerPage=parseInt(this.value);currentPage=1;renderHistory()})})</script></body></html>";
    _server.send(200, "text/html", temp);
}

void ChronoWebServer::_handleGunProfile(){
  String temp="<html>\
  <head>\
    <title>Gun Profile</title>\
    <style>\
      body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
    </style>\
  </head>\
  <body>\
  <script>\
      function addProfile() {\
        var name=document.getElementById('name').value;\
        var type=document.getElementById('type').value;\
        const formData = new URLSearchParams();\
        formData.append('name', name);\
        formData.append('type', type);\
        fetch('/gunprofile/add', {\
          method: 'POST',\
          body: formData\
        })\
        .then(response => response.text())\
        .then(data => {console.log(data); window.location.href = '/';})\
        .catch(error => console.error('Error:', error));\
      }\
    </script>\
  <h1>Add Gun Profile</h1>\
  <div>\
    <h3>Name</h3>\
    <input id='name' style='width:200px;'></input>\
    <h3>Type</h3>\
    <select id='type'>\
    <option value='0'>Bow or Airsoft</option>\
    <option value='1'>CO2 Pistol</option>\
    <option value='2'>Air Pistol</option>\
    <option value='3'>Air Gun UK</option>\
    <option value='4'>FAC</option>\
    </select>\
    <div style='margin-top:50px;'><button onclick='addProfile()'>Add Profile</button></div>\
  </div>\
  </body>\
  </html>";
  _server.send(200, "text/html", temp);
}
void ChronoWebServer::_handleAddGunProfile(){
  if (_server.method() == HTTP_POST){
    String name="Default";
    GunProfile::GunProfileType type=GunProfile::PROFILE_TYPE_AIR_GUN_FAC;
    float caliber=0.0;
    float weight=0.0;
    if (_server.hasArg("name")){name=_server.arg("name");}
    if (_server.hasArg("type")){
      String st=_server.arg("type");
      if (strcmp(st.c_str(),"0")==0){type=GunProfile::PROFILE_TYPE_BOW_AIRSOFT;}
      else if (strcmp(st.c_str(),"1")==0){type=GunProfile::PROFILE_TYPE_CO2_PISTOL;}
      else if (strcmp(st.c_str(),"2")==0){type=GunProfile::PROFILE_TYPE_AIR_PISTOL;}
      else if (strcmp(st.c_str(),"3")==0){type=GunProfile::PROFILE_TYPE_AIR_GUN_UK;}
      else if (strcmp(st.c_str(),"4")==0){type=GunProfile::PROFILE_TYPE_AIR_GUN_FAC;}
    }
    ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
    GunProfile p(name,type);
    chronoReadings.addGunProfile(p);
  }

  _server.send(200, "text/html", "ok");
}
void ChronoWebServer::_handleSetGunProfile(){
  if (_server.method() == HTTP_POST){
    if (_server.hasArg("id")){
      int id=_server.arg("id").toInt();
      ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
      chronoReadings.setGunProfile(id);

      //update profile with chronograph
      int type=chronoReadings.getProfile().getGunProfile().getPowerLevel();
      ChronoBLE::changeGunType(type);

      //update display
      Display::updateProfile(chronoReadings.getProfile());
      Display::refreshDisplay();
    }
  }
  _server.send(200, "text/html", "ok");
}
void ChronoWebServer::_handleRemoveGunProfile(){
  if (_server.method() == HTTP_POST){
    if (_server.hasArg("id")){
      int id=_server.arg("id").toInt();
      ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
      chronoReadings.removeGunProfile(id);
    }
  }
  _server.send(200, "text/html", "ok");
}
void ChronoWebServer::_handleGetGunProfiles(){
  ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
  LinkedList<GunProfile>& gunProfiles=chronoReadings.getGunProfiles();
  int count=gunProfiles.size();

  GunProfile gp=chronoReadings.getProfile().getGunProfile();
  int currentId=0;
  for(int i=0;i<count;i++){
    GunProfile g=gunProfiles.get(i);
    if(g.getName()==gp.getName() && g.getPowerLevel()==gp.getPowerLevel()){
      currentId=i;
      break;
    }
  }

  String json;
  json.reserve(2048);
  json += "{\"profiles\":[";
  for (int i=0;i<count;i++){
    GunProfile p=gunProfiles.get(i);
    if (i>0) json+=",";
    json += "{\"id\":";
    json += i;
    json += ",\"name\":\"";
    json += escapeJsonString(p.getName().c_str(), 49);
    json += "\",\"type\":";
    json += p.getPowerLevel();
    json += "}";
  }
  json += "],\"current\":{\"id\":";
  json += currentId;
  json += ",\"name\":\"";
  json += escapeJsonString(gp.getName().c_str(), 49);
  json += "\",\"type\":";
  json += gp.getPowerLevel();
  json += "}}";

  _server.send(200, "application/json", json);
}
void ChronoWebServer::_handleGetProjectileProfiles(){
  ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
  LinkedList<ProjectileProfile>& projectileProfiles=chronoReadings.getProjectileProfiles();
  int count=projectileProfiles.size();

  ProjectileProfile pp=chronoReadings.getProfile().getProjectileProfile();
  int currentId=0;
  for(int i=0;i<count;i++){
    ProjectileProfile p=projectileProfiles.get(i);
    if(p.getName()==pp.getName() && p.getType()==pp.getType() && p.getCaliber()==pp.getCaliber() && p.getWeight()==pp.getWeight()){
      currentId=i;
      break;
    }
  }

  String json;
  json.reserve(2048);
  json += "{\"profiles\":[";
  for (int i=0;i<count;i++){
    ProjectileProfile p=projectileProfiles.get(i);
    if (i>0) json+=",";
    json += "{\"id\":";
    json += i;
    json += ",\"name\":\"";
    json += escapeJsonString(p.getName().c_str(), 49);
    json += "\",\"type\":\"";
    json += escapeJsonString(p.getType().c_str(), 9);
    json += "\",\"cal\":\"";
    json += String(p.getCaliber(), 4);
    json += "\",\"weight\":\"";
    json += String(p.getWeight(), 4);
    json += "\"}";
  }
  json += "],\"current\":{\"id\":";
  json += currentId;
  json += ",\"name\":\"";
  json += escapeJsonString(pp.getName().c_str(), 49);
  json += "\",\"type\":\"";
  json += escapeJsonString(pp.getType().c_str(), 9);
  json += "\",\"cal\":\"";
  json += String(pp.getCaliber(), 4);
  json += "\",\"weight\":\"";
  json += String(pp.getWeight(), 4);
  json += "\"}}";

  _server.send(200, "application/json", json);
}
void ChronoWebServer::_handleGetCurrentGunProfile(){
  ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
  GunProfile p=chronoReadings.getProfile().getGunProfile();
  LinkedList<GunProfile>& gunProfiles=chronoReadings.getGunProfiles();
  int id=0;
  for(int i=0;i<gunProfiles.size();i++){
    GunProfile gp=gunProfiles.get(i);
    if(gp.getName()==p.getName() && gp.getPowerLevel()==p.getPowerLevel()){
      id=i;
      break;
    }
  }
  String json;
  json += "{\"id\":";
  json += id;
  json += ",\"name\":\"";
  json += escapeJsonString(p.getName().c_str(), 49);
  json += "\",\"type\":";
  json += p.getPowerLevel();
  json += "}";
  _server.send(200, "application/json", json);
}
void ChronoWebServer::_handleGetCurrentProjectileProfile(){
  ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
  ProjectileProfile p=chronoReadings.getProfile().getProjectileProfile();
  LinkedList<ProjectileProfile>& projectileProfiles=chronoReadings.getProjectileProfiles();
  int id=0;
  for(int i=0;i<projectileProfiles.size();i++){
    ProjectileProfile pp=projectileProfiles.get(i);
    if(pp.getName()==p.getName() && pp.getType()==p.getType() && pp.getCaliber()==p.getCaliber() && pp.getWeight()==p.getWeight()){
      id=i;
      break;
    }
  }
  String json;
  json += "{\"id\":";
  json += id;
  json += ",\"name\":\"";
  json += escapeJsonString(p.getName().c_str(), 49);
  json += "\",\"type\":\"";
  json += escapeJsonString(p.getType().c_str(), 9);
  json += "\",\"cal\":\"";
  json += String(p.getCaliber(), 4);
  json += "\",\"weight\":\"";
  json += String(p.getWeight(), 4);
  json += "\"}";
  _server.send(200, "application/json", json);
}
void ChronoWebServer::_handleGetReadings(){
  ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
  LinkedList<ChronoReading>& readings=chronoReadings.getChronographReadings();
  int count=readings.size();

  String json;
  json.reserve(4096);
  json += "[";
  for (int i=count-1;i>=0;i--){
    ChronoReading r=readings.get(i);
    if (count-1-i>0) json+=",";
    json += "{\"shot\":";
    json += (count-i);
    json += ",\"fps\":";
    json += r.getFPS();
    json += ",\"fpe\":";
    json += r.getFPE();
    json += ",\"gun\":\"";
    json += escapeJsonString(r.getProfile().getGunProfile().getName().c_str(), 49);
    json += "\",\"power\":";
    json += r.getProfile().getGunProfile().getPowerLevel();
    json += ",\"projectile\":\"";
    json += escapeJsonString(r.getProfile().getProjectileProfile().getName().c_str(), 49);
    json += "\",\"cal\":\"";
    json += String(r.getProfile().getProjectileProfile().getCaliber(), 4);
    json += "\",\"weight\":\"";
    json += String(r.getProfile().getProjectileProfile().getWeight(), 4);
    json += "\"}";
  }
  json += "]";

  _server.send(200, "application/json", json);
}
void ChronoWebServer::_handleProjectileProfile(){
  String temp="<html>\
  <head>\
    <title>Projectile Profile</title>\
    <style>\
      body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
    </style>\
  </head>\
  <body>\
  <script>\
      function addProfile() {\
        var name=document.getElementById('name').value;\
        var type=document.getElementById('type').value;\
        var cal=document.getElementById('caliber').value;\
        var weight=document.getElementById('weight').value;\
        const formData = new URLSearchParams();\
        formData.append('name', name);\
        formData.append('type', type);\
        formData.append('cal', cal);\
        formData.append('weight', weight);\
        fetch('/projectileprofile/add', {\
          method: 'POST',\
          body: formData\
        })\
        .then(response => response.text())\
        .then(data => {console.log(data); window.location.href = '/';})\
        .catch(error => console.error('Error:', error));\
      }\
    </script>\
  <h1>Add Projectile Profile</h1>\
  <div>\
    <h3>Name</h3>\
    <input id='name' style='width:200px;'></input>\
    <h3>Type</h3>\
    <select id='type'>\
    <option value='pellet'>Pellet</option>\
    <option value='slug'>Slug</option>\
    </select>\
    <h3>Caliber</h3>\
    <input id='caliber' style='width:50px;'></input>\
    <h3>Weight</h3>\
    <input id='weight' style='width:50px;'></input>\
    <div style='margin-top:50px;'><button onclick='addProfile()'>Add Profile</button></div>\
  </div>\
  </body>\
  </html>";
  _server.send(200, "text/html", temp);
}
void ChronoWebServer::_handleSetProjectileProfile(){
  if (_server.method() == HTTP_POST){
    if (_server.hasArg("id")){
      int id=_server.arg("id").toInt();
      ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
      chronoReadings.setProjectileProfile(id);
      Display::updateProfile(chronoReadings.getProfile());
      Display::refreshDisplay();
    }
  }
  _server.send(200, "text/html", "ok");
}
void ChronoWebServer::_handleAddProjectileProfile(){
  String message = "Got request...\n\n";
  message += "URI: ";
  message += _server.uri();
  message += "\nMethod: ";
  message += (_server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += _server.args();
  message += "\n";
  
  for (uint8_t i = 0; i < _server.args(); i++) {
    message += " " + _server.argName(i) + ": " + _server.arg(i) + "\n";
  }
  Serial.println(message);

  if (_server.method() == HTTP_POST){
    String name="";
    ProjectileProfile::ProjectileType type=ProjectileProfile::PELLET;
    float caliber=0.0;
    float weight=0.0;
    if (_server.hasArg("name")){name=_server.arg("name");}
    if (_server.hasArg("type")){
      String st=_server.arg("type");
      if (strcmp(st.c_str(),"slug")==0){
        type=ProjectileProfile::SLUG;
      }
    }
    if (_server.hasArg("cal")){caliber=_server.arg("cal").toFloat();}
    if (_server.hasArg("weight")){weight=_server.arg("weight").toFloat();}
    ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
    ProjectileProfile pp(name,type,caliber,weight);
    chronoReadings.addProjectileProfile(pp);
  }

  _server.send(200, "text/html", "ok");
}
void ChronoWebServer::_handleRemoveProjectileProfile(){
  if (_server.method() == HTTP_POST){
    if (_server.hasArg("id")){
      int id=_server.arg("id").toInt();
      ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
      chronoReadings.removeProjectileProfile(id);
    }
  }
  _server.send(200, "text/html", "ok");
}
void ChronoWebServer::_handleExport(){
  
  ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
  LinkedList<ChronoReading>& readings=chronoReadings.getChronographReadings();
  int count=readings.size();
  Serial.print("exporting ");
  Serial.print(count);
  Serial.println(" readings to csv...");

  //return csv data
  String csvData = "Shot #,FPS,FPE,Gun,Power,Projectile,Type,Caliber,Weight\n";
  for (int i=0; i<count;i++){
    ChronoReading r=readings.get(i);
    String shotNum=String(i+1);
    String fps=String(r.getFPS());
    String fpe=String(r.getFPE());
    String gun=r.getProfile().getGunProfile().getName();
    String power=String(r.getProfile().getGunProfile().getPowerLevel());
    String projectile=r.getProfile().getProjectileProfile().getName();
    String ptype=r.getProfile().getProjectileProfile().getType();
    String cal=String(r.getProfile().getProjectileProfile().getCaliber());
    String weight=String(r.getProfile().getProjectileProfile().getWeight());
    csvData += shotNum + ",";
    csvData += fps + ",";
    csvData += fpe + ",";
    csvData += gun + ",";
    csvData += power + ",";
    csvData += projectile + ",";
    csvData += ptype + ",";
    csvData += cal + ",";
    csvData += weight + "\n";
  }

  _server.sendHeader("Content-Disposition", "attachment; filename=data.csv");
  _server.send(200, "text/csv", csvData);


  //_server.send(200, "text/html", "ok");
}
void ChronoWebServer::_handleReset(){
  ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
  chronoReadings.reset();
  Display::updateEntireDisplay();
  Display::refreshDisplay();
  _server.send(200, "text/html", "ok");
}
void ChronoWebServer::_handleFormat(){
  Serial.println("formating disk, erasing all data...");
  FileManager& files = FileManager::getInstance();
  files.formatDisk();
  _server.send(200, "text/html", "ok");
}
void ChronoWebServer::setTestModeCallback(void (*pFunc)()){
  testModeCallback=pFunc;
}
void ChronoWebServer::_handleTestMode(){
  Serial.println("toggling test mode....");
  testModeCallback();
  _server.send(200, "text/html", "ok");
}
void handleOtaUpdate() {
  size_t fsize = UPDATE_SIZE_UNKNOWN;
  if (_server.hasArg("size")) {
    fsize = _server.arg("size").toInt();
  }
  HTTPUpload &upload = _server.upload();
  if (upload.status == UPLOAD_FILE_START) {
   
    Serial.printf("Receiving Update: %s, Size: %d\n", upload.filename.c_str(), fsize);
    if (!Update.begin(fsize)) {
      //otaDone = 0;
      //Update.printError(Serial);
    }
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
    //  Update.printError(Serial);
    } else {
    //  otaDone = 100 * Update.progress() / Update.size();
    }
  } else if (upload.status == UPLOAD_FILE_END) {
    if (Update.end(true)) {
      Serial.printf("Update Success: %u bytes\nRebooting...\n", upload.totalSize);
    } else {
      Serial.printf("%s\n", Update.errorString());
     // otaDone = 0;
    }
  }
}
void handleOtaUpdateEnd() {
  _server.sendHeader("Connection", "close");
  if (Update.hasError()) {
    _server.send(502, "text/plain", Update.errorString());
  } else {
    _server.sendHeader("Refresh", "10");
    _server.sendHeader("Location", "/");
    _server.send(307);
    delay(500);
    ESP.restart();
  }
}
void setupSoftAp(const char* ssid, const char* password) {

  WiFi.mode(WIFI_AP);
  IPAddress local_ip(192, 168, 4, 1);
  IPAddress gateway(192, 168, 4, 1);
  IPAddress subnet(255, 255, 255, 0);

  WiFi.softAPConfig(local_ip, gateway, subnet);
  delay(100);
  
  WiFi.softAP(ssid, password);//, password);
  delay(100);

  Serial.print("AP started: ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.softAPIP());
}

bool attemptStaConnection(const String& ssid, const String& password, int timeoutMs) {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid.c_str(), password.c_str());

  int attempts = 0;
  int maxAttempts = timeoutMs / 500;

  while (WiFi.status() != WL_CONNECTED && attempts < maxAttempts) {
    delay(500);
    Serial.print(".");
    attempts++;
  }

  return WiFi.status() == WL_CONNECTED;
}

void ChronoWebServer::init(){
  // Initialize NVS flash and erase WiFi config partition
  esp_err_t err = nvs_flash_init();
  if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    Serial.println("NVS partition was truncated, erasing...");
    ESP_ERROR_CHECK(nvs_flash_erase());
    err = nvs_flash_init();
  }
  ESP_ERROR_CHECK(err);

  // Erase WiFi storage in NVS
  nvs_handle_t handle;
  err = nvs_open("storage", NVS_READWRITE, &handle);
  if (err == ESP_OK) {
    nvs_erase_all(handle);
    nvs_commit(handle);
    nvs_close(handle);
    Serial.println("WiFi NVS storage erased");
  } else {
    Serial.println("No WiFi NVS storage to erase");
  }

  WiFi.disconnect(true, true);
  WiFi.mode(WIFI_OFF);
  delay(100);

  // Try to load saved WiFi credentials
  String savedSsid = getWifiSsid();
  String savedPassword = getWifiPassword();

  Serial.print("Saved SSID from Preferences: '");
  Serial.print(savedSsid);
  Serial.println("'");
  Serial.print("Saved Password length: ");
  Serial.println(savedPassword.length());

  bool connected = false;

  if (savedSsid.length() > 0) {
    Serial.print("Attempting to connect to saved WiFi: ");
    Serial.println(savedSsid);
    connected = attemptStaConnection(savedSsid, savedPassword, 7000);

    if (connected) {
      Serial.println("");
      Serial.print("Connected to ");
      Serial.println(savedSsid);
      Serial.print("IP address: ");
      Serial.println(WiFi.localIP());
    } else {
      Serial.println("");
      Serial.println("Failed to connect to saved WiFi - starting AP mode");
      WiFi.disconnect(true, true);
      WiFi.mode(WIFI_OFF);
      delay(100);
    }
  } else {
    Serial.println("No saved WiFi credentials - starting AP mode");
  }

  if (!connected) {
    setupSoftAp("ChronoDisplay", "12345678");

    if (MDNS.begin("esp32")) {
      Serial.println("MDNS responder started");
    }
  }

  _server.on("/gunprofile/add", _handleAddGunProfile);
  _server.on("/gunprofile/set", _handleSetGunProfile);
  _server.on("/gunprofile/remove", _handleRemoveGunProfile);
  _server.on("/gunprofile/get", _handleGetGunProfiles);
  _server.on("/gunprofile/current", _handleGetCurrentGunProfile);
  _server.on("/gunprofile", _handleGunProfile);
  _server.on("/projectileprofile/set", _handleSetProjectileProfile);
  _server.on("/projectileprofile/add", _handleAddProjectileProfile);
  _server.on("/projectileprofile/remove", _handleRemoveProjectileProfile);
  _server.on("/projectileprofile/get", _handleGetProjectileProfiles);
  _server.on("/projectileprofile/current", _handleGetCurrentProjectileProfile);
  _server.on("/projectileprofile", _handleProjectileProfile);
  _server.on("/readings", _handleGetReadings);
  _server.on("/export", _handleExport);
  _server.on("/reset", _handleReset);
  _server.on("/format", _handleFormat);
  _server.on("/testmode", _handleTestMode);
  _server.on("/ota", HTTP_POST, [](){handleOtaUpdateEnd();}, [](){handleOtaUpdate();});
  _server.on("/wifi", HTTP_GET, _handleWifi);
  _server.on("/wifisave", HTTP_POST, _handleWifiSave);
  _server.on("/clearwifi", HTTP_POST, _handleClearWifi);
  _server.on("/wifimode", HTTP_GET, _handleWifiMode);
  _server.on("/version", HTTP_GET, _handleVersion);
  _server.on("/initdata", HTTP_GET, _handleInitData);
  _server.on("/", _handleRoot);
  _server.begin();

}

void ChronoWebServer::process(){
  _server.handleClient();
}

void ChronoWebServer::_handleVersion() {
  String versionJson = String("{'version':'") + CHRONO_VERSION + "','major':" + String(CHRONO_VERSION_MAJOR) + ",'minor':" + String(CHRONO_VERSION_MINOR) + ",'patch':" + String(CHRONO_VERSION_PATCH) + "}";
  _server.send(200, "application/json", versionJson);
}

void ChronoWebServer::_handleWifi() {
    String ssid = getWifiSsid();
    _server.send(200, "text/plain", ssid);
}

void ChronoWebServer::_handleWifiSave() {
    if (_server.method() == HTTP_POST) {
        String ssid = "";
        String password = "";
        if (_server.hasArg("ssid")) {
            ssid = _server.arg("ssid");
        }
        if (_server.hasArg("password")) {
            password = _server.arg("password");
        }
        saveWifiCredentials(ssid, password);
        _server.send(200, "text/html", "ok");
        delay(1000);
        ESP.restart();
    }
}

void ChronoWebServer::_handleClearWifi() {
    if (_server.method() == HTTP_POST) {
        preferences.begin("wifi", false);
        preferences.clear();
        preferences.end();
        _server.send(200, "text/html", "ok");
        delay(1000);
        ESP.restart();
    }
}

void ChronoWebServer::_handleWifiMode() {
    String mode;
    if (WiFi.getMode() == WIFI_STA) {
        mode = "STA";
    } else if (WiFi.getMode() == WIFI_AP) {
        mode = "AP";
    } else if (WiFi.getMode() == WIFI_AP_STA) {
        mode = "AP+STA";
    } else {
        mode = "NONE";
    }
    _server.send(200, "text/plain", mode);
}
void ChronoWebServer::_handleInitData(){
  String json;
  json.reserve(4096);

  json += "{\"version\":{\"version\":\"";
  json += CHRONO_VERSION;
  json += "\",\"major\":";
  json += CHRONO_VERSION_MAJOR;
  json += ",\"minor\":";
  json += CHRONO_VERSION_MINOR;
  json += ",\"patch\":";
  json += CHRONO_VERSION_PATCH;
  json += "},\"wifiSsid\":\"";
  String ssid = getWifiSsid();
  json += escapeJsonString(ssid.c_str(), ssid.length());
  json += "\",\"wifiMode\":\"";
  if (WiFi.getMode() == WIFI_STA) json += "STA";
  else if (WiFi.getMode() == WIFI_AP) json += "AP";
  else if (WiFi.getMode() == WIFI_AP_STA) json += "AP+STA";
  else json += "NONE";
  json += "\",\"gunProfiles\":[";

  ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
  LinkedList<GunProfile>& gunProfiles = chronoReadings.getGunProfiles();
  int gunCount = gunProfiles.size();
  for (int i = 0; i < gunCount; i++) {
    GunProfile p = gunProfiles.get(i);
    if (i > 0) json += ",";
    json += "{\"id\":";
    json += i;
    json += ",\"name\":\"";
    json += escapeJsonString(p.getName().c_str(), 49);
    json += "\",\"type\":";
    json += p.getPowerLevel();
    json += "}";
  }
  json += "],\"projectileProfiles\":[";

  LinkedList<ProjectileProfile>& projectileProfiles = chronoReadings.getProjectileProfiles();
  int projCount = projectileProfiles.size();
  for (int i = 0; i < projCount; i++) {
    ProjectileProfile p = projectileProfiles.get(i);
    if (i > 0) json += ",";
    json += "{\"id\":";
    json += i;
    json += ",\"name\":\"";
    json += escapeJsonString(p.getName().c_str(), 49);
    json += "\",\"type\":\"";
    json += escapeJsonString(p.getType().c_str(), 9);
    json += "\",\"cal\":\"";
    json += String(p.getCaliber(), 4);
    json += "\",\"weight\":\"";
    json += String(p.getWeight(), 4);
    json += "\"}";
  }
  json += "],\"currentGunProfile\":{";

  GunProfile gp = chronoReadings.getProfile().getGunProfile();
  int gunId = 0;
  for (int i = 0; i < gunCount; i++) {
    GunProfile g = gunProfiles.get(i);
    if (g.getName() == gp.getName() && g.getPowerLevel() == gp.getPowerLevel()) {
      gunId = i;
      break;
    }
  }
  json += "\"id\":";
  json += gunId;
  json += ",\"name\":\"";
  json += escapeJsonString(gp.getName().c_str(), 49);
  json += "\",\"type\":";
  json += gp.getPowerLevel();
  json += "},\"currentProjectileProfile\":{";

  ProjectileProfile pp = chronoReadings.getProfile().getProjectileProfile();
  int projId = 0;
  for (int i = 0; i < projCount; i++) {
    ProjectileProfile p = projectileProfiles.get(i);
    if (p.getName() == pp.getName() && p.getType() == pp.getType() && p.getCaliber() == pp.getCaliber() && p.getWeight() == pp.getWeight()) {
      projId = i;
      break;
    }
  }
  json += "\"id\":";
  json += projId;
  json += ",\"name\":\"";
  json += escapeJsonString(pp.getName().c_str(), 49);
  json += "\",\"type\":\"";
  json += escapeJsonString(pp.getType().c_str(), 9);
  json += "\",\"cal\":\"";
  json += String(pp.getCaliber(), 4);
  json += "\",\"weight\":\"";
  json += String(pp.getWeight(), 4);
  json += "\"}";
  json += "}";

  _server.send(200, "application/json", json);
}