// Edge rendering: oneway/twoway/energy/cracked/dotted/wavy between two nodes.
// Also handles multi-edge offset between same pair so they don't overlap.

// Intersection: find point on circle (cx,cy,r) on the line from (cx,cy) toward (tx,ty)
function edgePointOnCircle(cx,cy,tx,ty,r){
  const dx = tx-cx, dy = ty-cy;
  const L = Math.hypot(dx,dy) || 1;
  return { x: cx + dx/L*r, y: cy + dy/L*r };
}

// Build a quadratic-bezier path between two points with a perpendicular offset.
function buildCurve(p1, p2, offset){
  const mx = (p1.x+p2.x)/2, my = (p1.y+p2.y)/2;
  const dx = p2.x-p1.x, dy = p2.y-p1.y;
  const L = Math.hypot(dx,dy) || 1;
  const nx = -dy/L, ny = dx/L;
  const cx = mx + nx*offset;
  const cy = my + ny*offset;
  return { d: `M ${p1.x} ${p1.y} Q ${cx} ${cy} ${p2.x} ${p2.y}`, cx, cy };
}

// Sample points along a quadratic bezier
function sampleQuad(p1, c, p2, n=60){
  const pts=[];
  for(let i=0;i<=n;i++){
    const t=i/n;
    const it=1-t;
    const x = it*it*p1.x + 2*it*t*c.x + t*t*p2.x;
    const y = it*it*p1.y + 2*it*t*c.y + t*t*p2.y;
    pts.push({x,y,t});
  }
  return pts;
}

// Convert a polyline into an SVG path d
function polyToPath(pts){
  return pts.map((p,i)=> (i?'L':'M') + p.x.toFixed(1) + ' ' + p.y.toFixed(1)).join(' ');
}

// Build a "wavy" path along the curve
function wavePathAlong(p1, c, p2, amp=7, freq=0.09){
  const samples = sampleQuad(p1,c,p2,120);
  const out=[];
  for(let i=0;i<samples.length;i++){
    const s = samples[i];
    // tangent
    const prev = samples[Math.max(0,i-1)];
    const nx = -(s.y - prev.y), ny = (s.x - prev.x);
    const L = Math.hypot(nx,ny) || 1;

    // 波が両端（矢印の根本）で綺麗に中央へ収束するように、両端15%で振幅をフェードアウトする
    const t = i / (samples.length - 1);
    const fade = Math.min(1, t / 0.15, (1 - t) / 0.15);

    const off = Math.sin(i*freq*Math.PI) * amp * fade;
    out.push({x: s.x + nx/L*off, y: s.y + ny/L*off});
  }
  return polyToPath(out);
}

// Build a zig-zag "crack" path along the curve
function crackPathAlong(p1, c, p2, amp=10){
  const samples = sampleQuad(p1,c,p2,14);
  const out=[];
  for(let i=0;i<samples.length;i++){
    const s = samples[i];
    if (i===0 || i===samples.length-1){ out.push({x:s.x,y:s.y}); continue; }
    const prev = samples[i-1];
    const nx = -(s.y - prev.y), ny = (s.x - prev.x);
    const L = Math.hypot(nx,ny) || 1;
    const off = (i%2===0?1:-1) * amp * (1 - Math.abs(i - samples.length/2)/(samples.length/2)*0.3);
    out.push({x: s.x + nx/L*off, y: s.y + ny/L*off});
  }
  return polyToPath(out);
}

// The label pill has a fixed dark navy background (#1C1F2B). If the arrow's color
// is itself dark (e.g. user picks black, dark blue, navy), the label text would
// merge into the background. We keep the user's hue but lift its luminance so
// the text stays legible while still reading as "the same color" as the arrow.
function liftDarkColorForDarkPill(hex){
  if (!hex || typeof hex !== 'string') return '#E9C46A';
  const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
  if (!m) return hex;
  const r = parseInt(m[1].slice(0,2), 16);
  const g = parseInt(m[1].slice(2,4), 16);
  const b = parseInt(m[1].slice(4,6), 16);
  // Relative luminance (sRGB, simplified — no gamma decoding; close enough here)
  const lum = (0.2126*r + 0.7152*g + 0.0722*b) / 255;
  const THRESHOLD = 0.5; // below this we start lifting toward white
  const MAX_MIX   = 0.7; // pure black ends up at ~70% white
  if (lum >= THRESHOLD) return hex;
  const mix = ((THRESHOLD - lum) / THRESHOLD) * MAX_MIX;
  const blend = c => Math.round(c + (255 - c) * mix);
  const toHex = c => blend(c).toString(16).padStart(2, '0');
  return '#' + toHex(r) + toHex(g) + toHex(b);
}

// Pill-shaped label whose width is measured from the rendered <text> via getBBox,
// so long labels (especially CJK) don't visually spill past the white rounded rect.
function EdgeLabelBox({ text, x, y, color, selected, onSelect, onEdit, onDragStart }){
  const textRef = React.useRef(null);
  const [w, setW] = React.useState(() => Math.max(36, text.length * 12 + 24));
  React.useLayoutEffect(()=>{
    if (textRef.current) {
      try {
        const bb = textRef.current.getBBox();
        const next = Math.max(36, bb.width + 20);
        if (Math.abs(next - w) > 0.5) setW(next);
      } catch(_){}
    }
  }, [text]);
  return (
    <g
      transform={`translate(${x},${y})`}
      className={`edge-label-g${selected?' selected':''}`}
      onClick={(e)=>{e.stopPropagation(); onSelect && onSelect(e.shiftKey);}}
      onDoubleClick={(e)=>{e.stopPropagation(); onEdit && onEdit();}}
      onPointerDown={(e)=>{
        e.stopPropagation();
        if (e.shiftKey) return; // click handler will process shift+click (swap z-order)
        onSelect && onSelect(false);
        if(onDragStart) onDragStart(e);
      }}
      style={{cursor:'pointer'}}
    >
      <rect
        x={-w/2} y={-14} width={w} height={28} rx={14}
        fill="#1C1F2B"
        stroke={selected ? '#E9C46A' : '#3D4258'}
        strokeWidth={2}
      />
      <text
        ref={textRef}
        x={0} y={1}
        textAnchor="middle"
        dominantBaseline="middle"
        fontFamily='"Noto Sans JP","Orbitron",system-ui,sans-serif'
        fontWeight={800}
        fontSize={13}
        fill={liftDarkColorForDarkPill(color)}
      >{text}</text>
    </g>
  );
}

function Edge({ edge, nodes, offset, selected, onSelect, onEdit, onDragStart }){
  const a = nodes[edge.from];
  const b = nodes[edge.to];
  if (!a || !b) return null;

  const rel = RELATIONS.find(r => r.id === edge.relation) || RELATIONS[0];
  const color = edge.color || rel.color;
  const style = edge.style || 'oneway';
  // Thickness: 'normal' (default) or 'thick'. Scales the base stroke, the flow
  // overlay stroke (via CSS .edge-flow.thick) and the arrowhead polygon size.
  const isThick = edge.thickness === 'thick';
  const stroke = isThick ? 10 : 4;

  // Shrink the path to the node edge. For thick arrows the arrowhead tip is
  // pushed forward by a larger tipOffset (to contain the bigger line cap), so
  // we also shrink the path MORE so the final tip position stays consistent
  // with normal arrows (= tip only grazes into the card by ~3px).
  // Normal: tipOffset=6, nr=48, tip lands ~42 from node center (card edge ~45).
  // Thick : tipOffset=11, nr=53, tip lands ~42 from center (same depth).
  const tipOffset = Math.max(6, stroke * 1.1);
  const nr = 48 + Math.max(0, tipOffset - 6);

  const p1full = { x:a.x, y:a.y };
  const p2full = { x:b.x, y:b.y };
  const actualOffset = edge.customOffset !== undefined ? edge.customOffset : offset;
  const { cx, cy } = buildCurve(p1full, p2full, actualOffset);

  // shrink endpoints to node boundary using control point direction
  const p1 = edgePointOnCircle(a.x,a.y, cx, cy, nr);
  const p2 = edgePointOnCircle(b.x,b.y, cx, cy, nr);

  const curve = buildCurve(p1, p2, actualOffset * 0.6);

  let pathD = curve.d;
  let dash = '';
  if (style === 'dotted') dash = '0.1 12';
  if (style === 'wavy') pathD = wavePathAlong(p1, {x:curve.cx,y:curve.cy}, p2, 6, 0.12);
  if (style === 'cracked') pathD = crackPathAlong(p1, {x:curve.cx,y:curve.cy}, p2, 10);

  // Build arrowhead polygons instead of SVG markers (html-to-image breaks markers)
  const arrowSize = isThick ? 32 : 18;
  const needEnd = (style === 'oneway' || style === 'twoway' || style === 'dotted' || style === 'wavy');
  const needStart = (style === 'twoway');

  const endArrow = needEnd ? buildArrowhead(p1, {x:curve.cx,y:curve.cy}, p2, 1.0, arrowSize, stroke) : null;
  const startArrow = needStart ? buildArrowhead(p1, {x:curve.cx,y:curve.cy}, p2, 0.0, arrowSize, stroke) : null;

  // label position: mid of curve
  const labelX = 0.25*p1.x + 0.5*curve.cx + 0.25*p2.x;
  const labelY = 0.25*p1.y + 0.5*curve.cy + 0.25*p2.y;

  const labelText = edge.label ?? rel.label;
  const stamp = edge.stamp || '';

  return (
    <g className={`edge-group${selected?' selected':''}`}>
      <path className="edge-hit" d={curve.d}
            data-edge-id={edge.id}
            onClick={(e)=>onSelect(edge.id, e.shiftKey)}
            onDoubleClick={()=>onEdit(edge.id)}
            onPointerDown={(e)=>{
              if(onDragStart && !e.shiftKey) onDragStart(edge.id, e);
            }}/>
      <path
        className="edge-path"
        d={pathD}
        stroke={color}
        strokeWidth={stroke}
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeDasharray={dash || undefined}
        fill="none"
      />
      {/* Flow overlay: bright pulse that travels along the curve from source → target.
          Gives every arrow a clear directional flow. */}
      <path
        className={`edge-flow ${style}${isThick ? ' thick' : ''}`}
        d={pathD}
        stroke={color}
        fill="none"
        style={{filter:`drop-shadow(0 0 ${isThick ? 16 : 8}px ${color})`}}
      />
      {/* Two-way arrows get a second flow going the opposite direction so the
          pulse appears to emanate from BOTH endpoints — both sides "push" toward each other. */}
      {style === 'twoway' && (
        <path
          className={`edge-flow-rev${isThick ? ' thick' : ''}`}
          d={pathD}
          stroke={color}
          fill="none"
          style={{filter:`drop-shadow(0 0 ${isThick ? 16 : 8}px ${color})`}}
        />
      )}

      {endArrow && <polygon points={endArrow} fill={color} />}
      {startArrow && <polygon points={startArrow} fill={color} />}
    </g>
  );
}

// Renders ONLY the label (pill) for an edge. Used by Canvas to paint all labels
// in a separate SVG pass AFTER every edge path has been drawn, so a later edge
// crossing over an earlier edge's label doesn't cover the pill background.
function EdgeLabel({ edge, nodes, offset, selected, onSelect, onEdit, onDragStart }){
  const a = nodes[edge.from];
  const b = nodes[edge.to];
  if (!a || !b) return null;

  // Must match the nr used in Edge() above — including the thick-arrow bump —
  // otherwise labels drift off the midpoint of the real drawn path.
  const stroke = edge.thickness === 'thick' ? 10 : 4;
  const tipOffset = Math.max(6, stroke * 1.1);
  const nr = 48 + Math.max(0, tipOffset - 6);
  const p1full = { x:a.x, y:a.y };
  const p2full = { x:b.x, y:b.y };
  const actualOffset = edge.customOffset !== undefined ? edge.customOffset : offset;
  const { cx, cy } = buildCurve(p1full, p2full, actualOffset);
  const p1 = edgePointOnCircle(a.x,a.y, cx, cy, nr);
  const p2 = edgePointOnCircle(b.x,b.y, cx, cy, nr);
  const curve = buildCurve(p1, p2, actualOffset * 0.6);

  const rel = RELATIONS.find(r => r.id === edge.relation) || RELATIONS[0];
  const color = edge.color || rel.color;
  const labelX = 0.25*p1.x + 0.5*curve.cx + 0.25*p2.x;
  const labelY = 0.25*p1.y + 0.5*curve.cy + 0.25*p2.y;
  const labelText = edge.label ?? rel.label;
  const stamp = edge.stamp || '';
  if (!labelText && !stamp) return null;

  return (
    <EdgeLabelBox
      text={(stamp ? stamp + ' ' : '') + (labelText || '')}
      x={labelX} y={labelY}
      color={color}
      selected={selected}
      onSelect={(shiftKey)=>onSelect(edge.id, shiftKey)}
      onEdit={()=>onEdit(edge.id)}
      onDragStart={onDragStart ? (e)=>onDragStart(edge.id, e) : null}
    />
  );
}

// Build an arrowhead polygon at a point along a quadratic bezier.
// tPos: 0.0 = start, 1.0 = end
// strokeW: the base stroke width of the path the arrow terminates — we push the
// arrow tip forward enough that the path's round line-cap (radius = strokeW/2)
// sits fully inside the triangle instead of peeking past its narrow tip.
function buildArrowhead(p1, c, p2, tPos, size, strokeW){
  size = size || 10;
  strokeW = strokeW || 4;
  // Sample the tangent near the target point
  const eps = 0.02;
  let tA, tB;
  if (tPos >= 1.0) { tA = 1.0 - eps; tB = 1.0; }
  else { tA = 0.0; tB = eps; }

  const evalBez = (t) => {
    const it = 1 - t;
    return { x: it*it*p1.x + 2*it*t*c.x + t*t*p2.x,
             y: it*it*p1.y + 2*it*t*c.y + t*t*p2.y };
  };

  const pA = evalBez(tA);
  const pB = evalBez(tB);
  // tangent direction (pointing from A toward B)
  let dx = pB.x - pA.x, dy = pB.y - pA.y;
  const L = Math.hypot(dx, dy) || 1;
  dx /= L; dy /= L;

  // For start arrow, reverse direction
  if (tPos < 0.5) { dx = -dx; dy = -dy; }

  const tip = evalBez(tPos);
  // Two base points perpendicular to the tangent
  const nx = -dy, ny = dx;
  const halfW = size * 0.6; // Slightly wider base (0.6 instead of 0.5)
  const backDist = size;

  // Push the arrow tip forward so the path's round line-cap (radius = strokeW/2)
  // fits inside the triangle. Triangle width at distance d from the tip is
  // `1.2 * d` (given halfW=0.6*size), so we need d ≥ strokeW / 1.2 to fully
  // contain a cap of diameter strokeW. Add a small safety margin.
  const tipOffset = Math.max(6, strokeW * 1.1);
  tip.x += dx * tipOffset;
  tip.y += dy * tipOffset;

  const bx = tip.x - dx * backDist;
  const by = tip.y - dy * backDist;

  const x1 = bx + nx * halfW;
  const y1 = by + ny * halfW;
  const x2 = bx - nx * halfW;
  const y2 = by - ny * halfW;

  return `${tip.x},${tip.y} ${x1},${y1} ${x2},${y2}`;
}

window.Edge = Edge;
window.EdgeLabel = EdgeLabel;
