#ifndef GRAPH_VIEWER_H
#define GRAPH_VIEWER_H


const char graph_viewer_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html lang="ko"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>그래프 뷰어 - CANFD Logger</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;font-size:13px}
nav{background:#161b22;border-bottom:1px solid #30363d;padding:8px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
nav a{color:#58a6ff;text-decoration:none;padding:4px 10px;border-radius:6px;font-size:12px}
nav a:hover{background:#21262d} nav a.active{background:#1f6feb;color:#fff}
.title{color:#e6edf3;font-weight:700;font-size:15px}
.container{padding:12px;max-width:1000px}
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px;margin-bottom:10px}
.card h3{font-size:12px;color:#8b949e;margin-bottom:12px;text-transform:uppercase}
.btn{padding:5px 12px;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600}
.btn-blue{background:#1f6feb;color:#fff} .btn-gray{background:#21262d;color:#c9d1d9;border:1px solid #30363d}
input[type=text],input[type=file]{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 8px;border-radius:6px;font-size:12px}
.row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;margin-bottom:8px}
.field{margin-bottom:4px} label{display:block;font-size:11px;color:#8b949e;margin-bottom:3px}
canvas{width:100%;height:300px;background:#0d1117;border:1px solid #30363d;border-radius:6px}
.info{font-size:11px;color:#8b949e;margin-top:6px}
.stat{display:inline-block;margin-right:16px;color:#8b949e}
.stat b{color:#e6edf3}
</style>
</head><body>
<nav>
  <span class="title">⚡ CANFD Logger</span>
  <a href="/">대시보드</a><a href="/transmit">송신</a>
  <a href="/graph">그래프</a><a href="/graph-view" class="active">뷰어</a>
  <a href="/settings">설정</a>
</nav>
<div class="container">
  <div class="card">
    <h3>BIN 파일 분석 뷰어 (CAN FD)</h3>
    <div class="row">
      <div class="field">
        <label>BIN 파일 선택 (로컬)</label>
        <input type="file" id="binFile" accept=".bin" onchange="loadBin()">
      </div>
      <div class="field" style="margin-bottom:4px">
        <button class="btn btn-gray" onclick="document.getElementById('dlUrl').style.display='block'">URL로 열기</button>
      </div>
    </div>
    <div id="dlUrl" style="display:none;margin-bottom:8px">
      <label>다운로드 URL (SD 파일명)</label>
      <div class="row">
        <input type="text" id="urlInput" placeholder="예: CANFD_20250430_120000.bin" style="width:260px">
        <button class="btn btn-blue" onclick="loadUrl()">불러오기</button>
      </div>
    </div>
    <div id="fileInfo" class="info"></div>
  </div>

  <div class="card" id="analyzeCard" style="display:none">
    <h3>신호 설정</h3>
    <div class="row">
      <div class="field"><label>CAN ID (hex)</label><input type="text" id="vSigId" value="0x123" style="width:90px"></div>
      <div class="field"><label>시작 비트</label><input type="text" id="vSigStart" value="0" style="width:60px"></div>
      <div class="field"><label>비트 길이</label><input type="text" id="vSigLen" value="8" style="width:60px"></div>
      <div class="field"><label>배율</label><input type="text" id="vSigScale" value="1" style="width:60px"></div>
      <div class="field"><label>이름</label><input type="text" id="vSigName" value="Signal 1" style="width:90px"></div>
      <div class="field" style="margin-bottom:4px"><button class="btn btn-blue" onclick="addSig()">+ 추가</button></div>
    </div>
    <div id="vSigList" style="display:flex;gap:8px;flex-wrap:wrap;margin:8px 0"></div>
    <canvas id="vChart"></canvas>
    <div style="margin-top:8px">
      <span class="stat">시간 범위: <b id="timeRange">-</b></span>
      <span class="stat">총 메시지: <b id="totalMsgs">0</b></span>
      <span class="stat">FD 메시지: <b id="fdMsgs">0</b></span>
    </div>
    <div style="margin-top:8px;display:flex;gap:8px">
      <button class="btn btn-gray" onclick="clearSigs()">신호 초기화</button>
      <button class="btn btn-blue" onclick="exportCSV()">CSV 내보내기</button>
    </div>
  </div>
</div>

<script>
const COLORS=['#58a6ff','#3fb950','#d29922','#f85149','#a371f7','#ffa657','#79c0ff','#56d364'];
const RECORD_SIZE=79; // sizeof(CANFDLog): 8+4+1+1+1+64
let records=[], signals=[];

function parseBin(buf){
  records=[];
  let n=Math.floor(buf.byteLength/RECORD_SIZE);
  let dv=new DataView(buf);
  for(let i=0;i<n;i++){
    let off=i*RECORD_SIZE;
    let tsLo=dv.getUint32(off,true), tsHi=dv.getUint32(off+4,true);
    let ts=tsHi*0x100000000+tsLo;  // uint64 → JS Number (μs, precision ok for logging)
    let id=dv.getUint32(off+8,true);
    let dlc=dv.getUint8(off+12);
    let len=dv.getUint8(off+13);
    let flags=dv.getUint8(off+14);
    let data=new Uint8Array(buf,off+15,Math.min(len,64));
    records.push({ts,id,dlc,len,flags,data:Array.from(data)});
  }
  let total=records.length;
  let fdCnt=records.filter(r=>(r.flags&0x20)!==0).length;
  document.getElementById('totalMsgs').textContent=total.toLocaleString();
  document.getElementById('fdMsgs').textContent=fdCnt.toLocaleString();
  if(total>0){
    let t0=records[0].ts, t1=records[total-1].ts;
    document.getElementById('timeRange').textContent=((t1-t0)/1e6).toFixed(3)+'s';
    document.getElementById('fileInfo').textContent=
      `총 ${total.toLocaleString()} 메시지 | FD: ${fdCnt.toLocaleString()} | 시간: ${((t1-t0)/1e6).toFixed(3)}s`;
  }
  document.getElementById('analyzeCard').style.display='block';
  draw();
}

function loadBin(){
  let f=document.getElementById('binFile').files[0]; if(!f) return;
  document.getElementById('fileInfo').textContent='읽는 중...';
  let r=new FileReader();
  r.onload=e=>parseBin(e.target.result);
  r.readAsArrayBuffer(f);
}

function loadUrl(){
  let fn=document.getElementById('urlInput').value.trim(); if(!fn) return;
  document.getElementById('fileInfo').textContent='다운로드 중...';
  fetch('/download?file='+fn).then(r=>r.arrayBuffer()).then(parseBin)
    .catch(e=>{ document.getElementById('fileInfo').textContent='다운로드 실패: '+e; });
}

function extractBits(data,start,len){
  let v=0;
  for(let i=0;i<len;i++){
    let bit=start+i,b=Math.floor(bit/8),bit2=7-(bit%8);
    if(b<data.length&&(data[b]&(1<<bit2))) v|=(1<<(len-1-i));
  }
  return v;
}

function addSig(){
  if(signals.length>=8) return alert('최대 8개');
  let id=parseInt(document.getElementById('vSigId').value,16);
  signals.push({id,startBit:parseInt(document.getElementById('vSigStart').value)||0,
    bitLen:parseInt(document.getElementById('vSigLen').value)||8,
    scale:parseFloat(document.getElementById('vSigScale').value)||1,
    name:document.getElementById('vSigName').value||'Signal',
    color:COLORS[signals.length%COLORS.length]});
  renderSigList(); draw();
}
function clearSigs(){signals=[];renderSigList();draw();}
function removeSig(i){signals.splice(i,1);renderSigList();draw();}
function renderSigList(){
  let h='';
  signals.forEach((s,i)=>h+=`<span style="background:#21262d;border:1px solid #30363d;border-radius:12px;padding:3px 10px;font-size:11px;display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:50%;background:${s.color};display:inline-block"></span>${s.name}<span onclick="removeSig(${i})" style="cursor:pointer;color:#8b949e">✕</span></span>`);
  document.getElementById('vSigList').innerHTML=h;
}

const cv=document.getElementById('vChart');
const ctx=cv.getContext('2d');
function draw(){
  let W=cv.offsetWidth, H=cv.offsetHeight;
  cv.width=W; cv.height=H;
  ctx.fillStyle='#0d1117';ctx.fillRect(0,0,W,H);
  if(!records.length||!signals.length){
    ctx.fillStyle='#8b949e';ctx.font='13px sans-serif';ctx.textAlign='center';
    ctx.fillText(records.length?'신호를 추가하세요.':'BIN 파일을 로드하세요.',W/2,H/2); return;
  }
  // 각 신호별 포인트 계산
  let seriesList=signals.map(s=>{
    let pts=records.filter(r=>r.id===s.id).map(r=>({x:r.ts,y:extractBits(r.data,s.startBit,s.bitLen)*s.scale}));
    return {s,pts};
  }).filter(s=>s.pts.length>0);
  if(!seriesList.length) return;
  let allPts=seriesList.flatMap(s=>s.pts);
  let minX=Math.min(...allPts.map(p=>p.x)), maxX=Math.max(...allPts.map(p=>p.x));
  let minY=Math.min(...allPts.map(p=>p.y)), maxY=Math.max(...allPts.map(p=>p.y));
  if(minY===maxY){minY-=1;maxY+=1;}
  let pad=35;
  function toX(x){return pad+(x-minX)/(maxX-minX||1)*(W-pad*2);}
  function toY(y){return H-pad-(y-minY)/(maxY-minY)*(H-pad*2);}
  // 격자
  ctx.strokeStyle='#21262d';ctx.lineWidth=1;
  for(let i=0;i<=4;i++){let y=pad+i*(H-pad*2)/4;ctx.beginPath();ctx.moveTo(pad,y);ctx.lineTo(W-pad,y);ctx.stroke();}
  ctx.fillStyle='#8b949e';ctx.font='10px sans-serif';ctx.textAlign='right';
  for(let i=0;i<=4;i++){let y=pad+i*(H-pad*2)/4;let v=(maxY-(maxY-minY)*i/4).toFixed(2);ctx.fillText(v,pad-4,y+4);}
  // X축 레이블
  ctx.textAlign='center';
  for(let i=0;i<=4;i++){let x=pad+i*(W-pad*2)/4;let t=((minX+(maxX-minX)*i/4-minX)/1e6).toFixed(2)+'s';ctx.fillText(t,x,H-4);}
  // 신호 그리기
  seriesList.forEach(({s,pts})=>{
    ctx.strokeStyle=s.color;ctx.lineWidth=1.5;ctx.beginPath();
    pts.forEach((p,i)=>{let x=toX(p.x),y=toY(p.y);i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});
    ctx.stroke();
    ctx.fillStyle=s.color;ctx.font='11px monospace';ctx.textAlign='left';
    let last=pts[pts.length-1];
    ctx.fillText(s.name+': '+last.y.toFixed(2),toX(last.x)+4,toY(last.y)-4);
  });
}

function exportCSV(){
  if(!records.length) return;
  let cols=['Time_us','ID','DLC','Len','FD','BRS','EXT'];
  signals.forEach(s=>cols.push(s.name));
  let rows=[cols.join(',')];
  records.forEach(r=>{
    let row=[r.ts,(r.id>>>0).toString(16).toUpperCase(),r.dlc,r.len,
      (r.flags&0x20)?1:0,(r.flags&0x10)?1:0,(r.flags&0x80)?1:0];
    signals.forEach(s=>{
      if(s.id===r.id) row.push((extractBits(r.data,s.startBit,s.bitLen)*s.scale).toFixed(4));
      else row.push('');
    });
    rows.push(row.join(','));
  });
  let blob=new Blob([rows.join('\n')],{type:'text/csv'});
  let a=document.createElement('a');a.href=URL.createObjectURL(blob);
  a.download='canfd_export.csv';a.click();
}
</script>
</body></html>
)rawliteral";

#endif
