/**
 * @file    plc_config.js
 * @brief   PLC 逻辑配置页 — 信号面板 + 逻辑图查看(当前/备份) + 在线编辑
 * @details Canvas 可视化 + XML 解析/生成, 11 种门类型.
 *          从原工程迁移, 按现工程 UI 风格重构.
 *
 *          布局: 左侧信号面板(datacenter_out.xml) | 右侧 Canvas
 *          功能: 查看(当前/备份切换) | 编辑(可选基础: 当前/备份/空白)
 */
var PlcConfigPage = (function() {
	"use strict";

	/* ========== Canvas roundRect polyfill ========== */
	if (!CanvasRenderingContext2D.prototype.roundRect) {
		CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
			if (typeof r === 'number') r = { tl: r, tr: r, br: r, bl: r };
			else if (Array.isArray(r)) r = { tl: r[0]||0, tr: r[1]||r[0]||0, br: r[2]||r[0]||0, bl: r[3]||r[0]||0 };
			this.beginPath();
			this.moveTo(x + r.tl, y);
			this.lineTo(x + w - r.tr, y);
			this.quadraticCurveTo(x + w, y, x + w, y + r.tr);
			this.lineTo(x + w, y + h - r.br);
			this.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h);
			this.lineTo(x + r.bl, y + h);
			this.quadraticCurveTo(x, y + h, x, y + h - r.bl);
			this.lineTo(x, y + r.tl);
			this.quadraticCurveTo(x, y, x + r.tl, y);
			this.closePath();
		};
	}

	/* ========== 节点类型常量 ========== */
	var NT_OUTPUT=0, NT_INPUT=1, NT_OR=2, NT_AND=3, NT_NOT=4;
	var NT_62P=5, NT_62D=6, NT_RISING=7, NT_SR=8, NT_RS=9, NT_FALLING=10, NT_TOGGLE=11, NT_MX_ADD=50, NT_MX_SUB=51, NT_MX_MAX=52, NT_MX_MIN=53, NT_MX_MUL=54, NT_MX_DIV=55, NT_MX_ABS=56, NT_MX_CMP=57;

	var NODE_INFO = {
		0:  { name:'输出点',   icon:'📤', color:'#4CAF50', inPins:1, outPins:0, inMax:1,  outMax:0 },
		1:  { name:'输入点',   icon:'📥', color:'#2196F3', inPins:0, outPins:1, inMax:0,  outMax:99 },
		2:  { name:'或门 OR',  icon:'🔶', color:'#FF9800', inPins:2, outPins:1, inMax:10, outMax:10 },
		3:  { name:'与门 AND', icon:'🔷', color:'#9C27B0', inPins:2, outPins:1, inMax:10, outMax:10 },
		4:  { name:'非门 NOT', icon:'🔺', color:'#f44336', inPins:1, outPins:1, inMax:1,  outMax:10 },
		5:  { name:'62p延时',  icon:'⏱️', color:'#00BCD4', inPins:1, outPins:1, inMax:1,  outMax:10 },
		6:  { name:'62d延时',  icon:'⏱️', color:'#009688', inPins:1, outPins:1, inMax:1,  outMax:10 },
		7:  { name:'上升沿',   icon:'⬆️', color:'#E91E63', inPins:1, outPins:1, inMax:1,  outMax:10 },
		8:  { name:'SR触发器', icon:'🔒', color:'#795548', inPins:2, outPins:1, inMax:2,  outMax:10 },
		9:  { name:'RS触发器', icon:'🔒', color:'#607D8B', inPins:2, outPins:1, inMax:2,  outMax:10 },
		10: { name:'下降沿',   icon:'⬇️', color:'#FF5722', inPins:1, outPins:1, inMax:1,  outMax:10 },
		11: { name:'翻转元件', icon:'🔄', color:'#CDDC39', inPins:1, outPins:1, inMax:1,  outMax:10 },
		50: { name:'加法元件',   icon:'➕', color:'#E040FB', inPins:2, outPins:1, inMax:2, outMax:10 },
		51: { name:'减法元件',   icon:'➖', color:'#00E5FF', inPins:2, outPins:1, inMax:2, outMax:10 },
		52: { name:'最大值元件', icon:'⬆️', color:'#FFD600', inPins:2, outPins:1, inMax:2, outMax:10 },
		53: { name:'最小值元件', icon:'⬇️', color:'#76FF03', inPins:2, outPins:1, inMax:2, outMax:10 },
		54: { name:'乘法元件',   icon:'✖️', color:'#FF6D00', inPins:2, outPins:1, inMax:2, outMax:10 },
		55: { name:'除法元件',   icon:'➗', color:'#D50000', inPins:2, outPins:1, inMax:2, outMax:10 },
		56: { name:'绝对值元件', icon:'📏', color:'#00C853', inPins:1, outPins:1, inMax:1, outMax:10 },
			57: { name:'比较元件', icon:'⚖️', color:'#FF1744', inPins:2, outPins:1, inMax:2, outMax:10 }
	};

	/* ========== 全局状态 ========== */
	var mode = 'view';               // 'view' | 'edit'
	var dcOutSignals = [];           // [{no, saddr, desc}]
	var logicGraphs = [];            // parsed graphs
	var loadedFiles = {};            // {path: content}

	/* ======== 编辑器状态 ======== */
	var editNodes = [];
	var editLinks = [];
	var editKey = '', editName = '';
	var editSelectedNode = -1, editSelectedLink = -1;
	var editWiring = null;
	var editDragNode = null, editDragOffX = 0, editDragOffY = 0;
	var editDragCanvas = null;
	var editPanX = 0, editPanY = 0, editScale = 1.0;
	var editNextGateIdx = {};
	var editLastClickTime = 0, editLastClickNode = -1;

	/* ======== 查看器状态 ======== */
	var viewPanX = 0, viewPanY = 0, viewScale = 1.0;
	var viewDrag = null, viewAutoFit = true;

	/* ========== 工具函数 ========== */
	function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
	function escXml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
	function findSignalByNo(no) { for (var i=0;i<dcOutSignals.length;i++) if(dcOutSignals[i].no===no) return dcOutSignals[i]; return null; }
	function getNodeDisplayLabel(node) {
		var info = NODE_INFO[node.type] || {};
		if (node.type===NT_INPUT||node.type===NT_OUTPUT) {
			var pfx = node.type===NT_INPUT?'输入':'输出', sig=findSignalByNo(node.id);
			return pfx+' #'+node.id+(sig?' ("'+sig.saddr+'", "'+sig.desc+'")':'');
		}
		return info.icon+' '+info.name+' #'+(node.id&0xFF);
	}
	function getNodeInPins(node) {
		if (node.inPins!==undefined&&node.inPins>0) return node.inPins;
		return (NODE_INFO[node.type]||{}).inPins||0;
	}
	function getNodeSize(node) {
		var isIO=(node.type===NT_OUTPUT||node.type===NT_INPUT),isSR=(node.type===NT_SR||node.type===NT_RS);
		var inP=getNodeInPins(node);
		return {w:isIO?250:170, h:isIO?80:(isSR?86:(66+Math.max(0,inP-2)*14))};
	}
	function showToast(msg, type) {
		type=type||'info';
		var t=document.getElementById('plc-cfg-toast');
		if(!t){t=document.createElement('div');t.id='plc-cfg-toast';t.style.cssText='position:fixed;top:80px;left:50%;transform:translateX(-50%);padding:10px 24px;border-radius:6px;z-index:99999;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,0.3);transition:opacity 0.3s;pointer-events:none;color:#fff';document.body.appendChild(t);}
		t.style.background=type==='err'?'#f44336':type==='ok'?'#4CAF50':'#333';
		t.textContent=msg;t.style.opacity='1';clearTimeout(t._timer);t._timer=setTimeout(function(){t.style.opacity='0';},2000);
	}

	/* ========== WebSocket 文件请求 ========== */
	function requestFile(path,cb){if(loadedFiles[path]){cb(loadedFiles[path]);return;}if(!PlcConfigPage._pending)PlcConfigPage._pending={};PlcConfigPage._pending[path]=cb;if(!WsClient.send({type:'get_file',path:path})){if(!PlcConfigPage._retryList)PlcConfigPage._retryList=[];PlcConfigPage._retryList.push(path);}}

	/* ========== 主渲染 ========== */
	function render(container) {
		container.innerHTML = ''
			+ '<div class="card" style="margin-bottom:12px"><div class="card-header"><span>🔌 PLC 逻辑配置</span>'
			+ '<div class="btn-group" style="margin-left:auto">'
			+ '<button class="btn btn-sm '+(mode==='view'?'btn-primary':'btn-outline')+'" id="plc-btn-view">👁️ 查看</button>'
			+ '<button class="btn btn-sm '+(mode==='edit'?'btn-primary':'btn-outline')+'" id="plc-btn-edit">✏️ 编辑</button>'
			+ '</div></div></div>'
			+ '<div id="plc-main-area" style="display:flex;gap:12px;height:calc(100vh - 200px)"></div>';
		document.getElementById('plc-btn-view').onclick = function(){mode='view';editWiring=null;render(container);};
		document.getElementById('plc-btn-edit').onclick = function(){mode='edit';editWiring=null;render(container);};
		var main=document.getElementById('plc-main-area');if(!main)return;
		if(mode==='view')renderViewMode(main);else renderEditMode(main);
		if(dcOutSignals.length===0)loadDcOutSignals(function(){refreshSigPanel();});else refreshSigPanel();
	}

	/* ================================================================
	 * 信号面板
	 * ================================================================ */
	function renderSigPanel() {
		return '<div class="card" style="width:230px;flex-shrink:0;display:flex;flex-direction:column;overflow:hidden">'
			+ '<div class="card-header"><span>📋 信号 ('+dcOutSignals.length+')</span>'
			+ '<button class="btn btn-sm btn-outline" id="plc-sig-reload" style="margin-left:auto;font-size:11px">🔄</button></div>'
			+ '<input type="text" id="plc-sig-search" placeholder="搜索 saddr/desc..." style="margin:8px;padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
			+ '<div style="flex:1;overflow-y:auto;font-size:12px;padding:0 8px" id="plc-sig-list"></div>'
			+ '</div>';
	}

	function renderOpPanel() {
		var ops=[{t:NT_AND,n:'与门',i:'🔷',c:'#9C27B0'},{t:NT_OR,n:'或门',i:'🔶',c:'#FF9800'},{t:NT_NOT,n:'非门',i:'🔺',c:'#f44336'},{t:NT_62P,n:'62P',i:'⏱️',c:'#00BCD4'},{t:NT_62D,n:'62D',i:'⏱️',c:'#009688'},{t:NT_RISING,n:'上升沿',i:'⬆️',c:'#E91E63'},{t:NT_FALLING,n:'下降沿',i:'⬇️',c:'#FF5722'},{t:NT_TOGGLE,n:'翻转',i:'🔄',c:'#CDDC39'},{t:NT_SR,n:'SR',i:'🔒',c:'#795548'},{t:NT_RS,n:'RS',i:'🔒',c:'#607D8B'},{t:NT_MX_ADD,n:'加法',i:'➕',c:'#E040FB'},{t:NT_MX_SUB,n:'减法',i:'➖',c:'#00E5FF'},{t:NT_MX_MAX,n:'最大值',i:'⬆️',c:'#FFD600'},{t:NT_MX_MIN,n:'最小值',i:'⬇️',c:'#76FF03'},{t:NT_MX_MUL,n:'乘法',i:'✖️',c:'#FF6D00'},{t:NT_MX_DIV,n:'除法',i:'➗',c:'#D50000'},{t:NT_MX_ABS,n:'绝对值',i:'📏',c:'#00C853'},{t:NT_MX_CMP,n:'比较',i:'⚖️',c:'#FF1744'}];
		var h='<div class="card" style="width:150px;flex-shrink:0;display:flex;flex-direction:column;overflow:hidden"><div class="card-header"><span>🧮 运算符</span></div><div style="flex:1;overflow-y:auto;font-size:11px;padding:4px" id="plc-op-list">';
		for(var oi=0;oi<ops.length;oi++){var op=ops[oi];h+='<div class="plc-op-row" data-gt="'+op.t+'" style="padding:6px 8px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,0.04);display:flex;align-items:center;gap:6px;transition:background 0.15s" onmouseover="this.style.background=\'rgba(255,255,255,0.06)\'" onmouseout="this.style.background=\'transparent\'"><span>'+op.i+'</span><span style="color:'+op.c+';font-weight:600;font-size:11px">'+op.n+'</span></div>';}
		h+='</div></div>';
		return h;
	}
	function loadDcOutSignals(cb) {
		requestFile('config/SYSTEM/datacenter_out.xml', function(content) {
			if(!content){showToast('加载 datacenter_out.xml 失败','err');return;}
			dcOutSignals=[];
			try{
				var doc=new DOMParser().parseFromString(content,'text/xml');
				var outNodes=doc.documentElement.getElementsByTagName('Out');
				if(outNodes.length>0){var sigs=outNodes[0].getElementsByTagName('Signal');for(var i=0;i<sigs.length;i++)dcOutSignals.push({no:parseInt(sigs[i].getAttribute('no'))||i,saddr:sigs[i].getAttribute('saddr')||'',desc:sigs[i].getAttribute('desc')||''});}
			}catch(e){showToast('解析失败: '+e.message,'err');}
			if(cb)cb();
		});
	}

	function refreshSigPanel() {
		var list=document.getElementById('plc-sig-list'),search=(document.getElementById('plc-sig-search')||{}).value||'';
		if(!list)return;
		var data=dcOutSignals;
		if(search){var q=search.toLowerCase();data=data.filter(function(s){return s.saddr.toLowerCase().indexOf(q)!==-1||s.desc.toLowerCase().indexOf(q)!==-1||String(s.no).indexOf(q)!==-1;});}
		var h='';
		for(var i=0;i<data.length;i++){var s=data[i];h+='<div class="plc-sig-row" data-no="'+s.no+'" data-saddr="'+escHtml(s.saddr)+'" data-desc="'+escHtml(s.desc)+'"><span style="color:var(--accent);min-width:24px;font-size:11px">#'+s.no+'</span><code style="flex:1;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+escHtml(s.saddr)+'</code><span style="color:var(--text-muted);font-size:10px;max-width:55px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+escHtml(s.desc)+'</span>';if(mode==='edit'){h+='<button class="btn btn-xs" style="font-size:10px;padding:1px 4px;background:#2196F3;color:#fff" onclick="event.stopPropagation();PlcConfigPage._addSigIO('+s.no+',\''+escHtml(s.saddr)+'\',\''+escHtml(s.desc)+'\',1)">入</button><button class="btn btn-xs" style="font-size:10px;padding:1px 4px;background:#4CAF50;color:#fff;margin-left:2px" onclick="event.stopPropagation();PlcConfigPage._addSigIO('+s.no+',\''+escHtml(s.saddr)+'\',\''+escHtml(s.desc)+'\',0)">出</button>';}h+='</div>';}
		list.innerHTML=h||'<div style="color:var(--text-muted);text-align:center;padding:20px">无匹配信号</div>';
		[].forEach.call(list.querySelectorAll('.plc-sig-row'),function(row){});
	}

	/* ================================================================
	 * 查看模式
	 * ================================================================ */
	function renderViewMode(main) {
		main.innerHTML = renderSigPanel()
			+ '<div class="card" style="flex:1;display:flex;flex-direction:column;overflow:hidden">'
			+ '<div class="card-header"><span>👁️ 逻辑图查看</span>'
			+ '<select id="plc-view-graph" style="margin:0 8px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px;min-width:240px"><option value="">-- 选择逻辑图 --</option></select>'
			+ '<span style="flex:1"></span>'
			+ '<button class="btn btn-sm btn-outline" id="plc-view-zoomin">🔍+</button>'
			+ '<button class="btn btn-sm btn-outline" id="plc-view-zoomout">🔍−</button>'
			+ '<button class="btn btn-sm btn-outline" id="plc-view-reset">↺</button>'
			+ '<button class="btn btn-sm btn-primary" id="plc-view-reload">🔄 刷新</button>'
			+ '</div>'
			+ '<div style="flex:1;position:relative;min-height:400px" id="plc-view-canvas-wrap"><canvas id="plc-view-canvas"></canvas></div>'
			+ '<div style="padding:6px 12px;font-size:11px;color:var(--text-muted);border-top:1px solid var(--border)" id="plc-view-legend"></div>'
			+ '</div>';
		var lh='<span style="font-weight:700;color:var(--text-secondary)">图例：</span> ';
		for(var t in NODE_INFO){var inf=NODE_INFO[t];lh+='<span style="display:inline-block;margin:2px 4px;padding:1px 6px;border-radius:3px;background:'+inf.color+';color:#fff;font-size:11px">'+inf.icon+' '+inf.name+'</span>';}
		document.getElementById('plc-view-legend').innerHTML=lh;
		document.getElementById('plc-sig-search').oninput=refreshSigPanel;
		document.getElementById('plc-sig-reload').onclick=function(){loadedFiles['config/SYSTEM/datacenter_out.xml']=null;dcOutSignals=[];loadDcOutSignals(function(){refreshSigPanel();});};
		document.getElementById('plc-view-zoomin').onclick=function(){viewScale=Math.min(3,viewScale*1.2);drawViewer();};
		document.getElementById('plc-view-zoomout').onclick=function(){viewScale=Math.max(0.2,viewScale/1.2);drawViewer();};
		document.getElementById('plc-view-reset').onclick=function(){viewAutoFit=true;drawViewer();};
		document.getElementById('plc-view-reload').onclick=function(){loadedFiles['config/PLC/Reclose_logic.xml']=null;loadedFiles['config/PLC/Reclose_logic.xml.bak']=null;logicGraphs=[];loadAllConfigs(function(){refreshViewSelect();});};
		document.getElementById('plc-view-graph').onchange=function(){viewAutoFit=true;drawViewer();};
		setupViewerCanvas();
		if(logicGraphs.length===0)loadAllConfigs(function(){refreshViewSelect();});else refreshViewSelect();
	}

	function loadAllConfigs(cb){var pending=2;function done(){pending--;if(pending<=0&&cb)cb();}requestFile('config/PLC/Reclose_logic.xml',function(c){if(!c){done();return;}parseLogicXml(c,'Reclose_logic.xml');done();});requestFile('config/PLC/Reclose_logic.xml.bak',function(c){if(!c){done();return;}parseLogicXml(c,'Reclose_logic.xml.bak');done();});}

	function loadCurrentLogic(cb){requestFile('config/PLC/Reclose_logic.xml',function(c){if(!c){showToast('加载失败','err');return;}parseLogicXml(c,'Reclose_logic.xml');if(cb)cb();});}

	function refreshViewSelect(){var sel=document.getElementById('plc-view-graph');if(!sel)return;sel.innerHTML='<option value="">-- 选择逻辑图 --</option>';for(var i=0;i<logicGraphs.length;i++){var g=logicGraphs[i],bak=g._source&&g._source.indexOf('.bak')>=0?' [备份]':'';sel.innerHTML+='<option value="'+i+'">'+(g.key||'?')+' — '+(g.name||'(未命名)')+bak+' ('+g.nodes.length+'节点 '+g.links.length+'连线)</option>';}if(logicGraphs.length>0){sel.value='0';setTimeout(function(){drawViewer();},100);}}

	function setupViewerCanvas(){var wrap=document.getElementById('plc-view-canvas-wrap'),cnv=document.getElementById('plc-view-canvas');if(!wrap||!cnv)return;function resize(){cnv.width=wrap.clientWidth;cnv.height=wrap.clientHeight-4;viewAutoFit=true;drawViewer();}resize();window.addEventListener('resize',function(){resize();});cnv.onmousedown=function(e){viewDrag={sx:e.clientX,sy:e.clientY,px:viewPanX,py:viewPanY};};cnv.onmousemove=function(e){if(!viewDrag)return;viewPanX=viewDrag.px+(e.clientX-viewDrag.sx);viewPanY=viewDrag.py+(e.clientY-viewDrag.sy);drawViewer();};cnv.onmouseup=function(){viewDrag=null;};cnv.onmouseleave=function(){viewDrag=null;};cnv.onwheel=function(e){e.preventDefault();var d=e.deltaY>0?0.9:1.1,ns=Math.max(0.2,Math.min(3,viewScale*d)),r=cnv.getBoundingClientRect();viewPanX=(e.clientX-r.left)-((e.clientX-r.left)-viewPanX)*(ns/viewScale);viewPanY=(e.clientY-r.top)-((e.clientY-r.top)-viewPanY)*(ns/viewScale);viewScale=ns;drawViewer();};}

	function drawViewer(){var cnv=document.getElementById('plc-view-canvas');if(!cnv)return;var ctx=cnv.getContext('2d'),W=cnv.width,H=cnv.height;ctx.clearRect(0,0,W,H);ctx.fillStyle='#0a0f16';ctx.fillRect(0,0,W,H);var sel=document.getElementById('plc-view-graph');if(!sel||sel.value===''){ctx.fillStyle='#556677';ctx.font='14px sans-serif';ctx.textAlign='center';ctx.fillText('请选择一个逻辑图',W/2,H/2);return;}var g=logicGraphs[parseInt(sel.value)];if(!g)return;var vn=[],bMinX=Infinity,bMaxX=-Infinity,bMinY=Infinity,bMaxY=-Infinity;for(var vi=0;vi<g.nodes.length;vi++){var vv=g.nodes[vi];if(vv.x<bMinX)bMinX=vv.x;if(vv.x>bMaxX)bMaxX=vv.x;if(vv.y<bMinY)bMinY=vv.y;if(vv.y>bMaxY)bMaxY=vv.y;var vsig=findSignalByNo(vv.id);vn.push({type:vv.type,id:vv.id,_lx:vv.x,_ly:vv.y,saddr:(vsig&&vsig.saddr)||'',desc:(vsig&&vsig.desc)||'',no:vv.id});}if(viewAutoFit){viewAutoFit=false;var cw=bMaxX-bMinX,ch=bMaxY-bMinY;if(cw<=0)cw=800;if(ch<=0)ch=600;viewScale=Math.min((W-160)/cw,(H-160)/ch,1.2);viewPanX=W/2-(bMinX+cw/2)*viewScale;viewPanY=H/2-(bMinY+ch/2)*viewScale;}var vl=[];for(var vj=0;vj<g.links.length;vj++){var vlk=g.links[vj],vls=-1,vld=-1;for(var vk=0;vk<g.nodes.length;vk++){var vln=g.nodes[vk];if(vls<0&&vln.type===vlk.src_type&&(vln.type<=1?vln.id===vlk.src_id:(vln.id&0xFF)===(vlk.src_id&0xFF)))vls=vk;if(vld<0&&vln.type===vlk.dst_type&&(vln.type<=1?vln.id===vlk.dst_id:(vln.id&0xFF)===(vlk.dst_id&0xFF)))vld=vk;}if(vls>=0&&vld>=0)vl.push({srcIdx:vls,dstIdx:vld,dstPin:vlk.dstPin||0});}ctx.strokeStyle='rgba(255,255,255,0.04)';ctx.lineWidth=1;var gs=50,sx=Math.floor(-viewPanX/viewScale/gs)*gs,sy=Math.floor(-viewPanY/viewScale/gs)*gs;ctx.save();ctx.translate(viewPanX,viewPanY);ctx.scale(viewScale,viewScale);for(var gx=sx-gs;gx<sx+W/viewScale+gs;gx+=gs){ctx.beginPath();ctx.moveTo(gx,sy-gs);ctx.lineTo(gx,sy+H/viewScale+gs);ctx.stroke();}for(var gy=sy-gs;gy<sy+H/viewScale+gs;gy+=gs){ctx.beginPath();ctx.moveTo(sx-gs,gy);ctx.lineTo(sx+W/viewScale+gs,gy);ctx.stroke();}drawGraphContent(ctx,vn,vl,-1,-1,null);ctx.restore();}

	/* ================================================================
	 * 编辑模式
	 * ================================================================ */
	function renderEditMode(main) {
		editNodes=[];editLinks=[];editKey='';editName='';editSelectedNode=-1;editSelectedLink=-1;
		editWiring=null;editDragNode=null;editPanX=0;editPanY=0;editScale=1.0;editNextGateIdx={};

		main.innerHTML = renderSigPanel()
				+ renderOpPanel()
			+ '<div style="flex:1;display:flex;flex-direction:column;gap:8px">'
			+ '<div class="card"><div class="card-header"><span>✏️ 编辑配置</span>'
			+ '<span style="flex:1"></span>'
			+ '<span style="font-size:12px;color:var(--text-secondary);margin-right:8px">基础:</span>'
			+ '<select id="plc-edit-base" style="margin-right:8px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
			+ '<option value="current">当前配置</option><option value="backup">备份配置</option><option value="empty">空白新建</option>'
			+ '</select>'
			+ '<button class="btn btn-sm btn-primary" id="plc-edit-load">📥 加载</button>'
			+ '</div></div>'
			+ '<div class="card" style="flex:1;display:flex;flex-direction:column;overflow:hidden">'
			+ '<div style="padding:6px 8px;display:flex;gap:4px;align-items:center;flex-wrap:wrap;border-bottom:1px solid var(--border)">'
			+ '<input type="text" id="plc-edit-key" placeholder="Key" style="width:100px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
			+ '<input type="text" id="plc-edit-name" placeholder="名称" style="width:130px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:12px">'
			+ '<span style="flex:1"></span>'
			+ '<button class="btn btn-sm" style="background:#2196F3;color:#fff" id="plc-edit-add-in">📥 输入</button>'
			+ '<button class="btn btn-sm btn-outline" id="plc-edit-add-gate">🧮 运算符</button>'
				+ '<button class="btn btn-sm" style="background:#4CAF50;color:#fff" id="plc-edit-add-out">📤 输出</button>'
			+ '<button class="btn btn-sm btn-outline" id="plc-edit-modify">✏️ 修改</button>'
			+ '<button class="btn btn-sm btn-danger" id="plc-edit-del">🗑 删除</button>'
			+ '<button class="btn btn-sm btn-outline" id="plc-edit-zoomin">🔍+</button>'
			+ '<button class="btn btn-sm btn-outline" id="plc-edit-zoomout">🔍−</button>'
			+ '<button class="btn btn-sm btn-primary" id="plc-edit-generate">✅ 生成</button>'
			+ '</div>'
			+ '<div style="flex:1;position:relative;min-height:400px" id="plc-edit-canvas-wrap"><canvas id="plc-edit-canvas" tabindex="0"></canvas></div>'
			+ '<div style="padding:4px 12px;font-size:11px;color:var(--text-muted);border-top:1px solid var(--border)" id="plc-edit-info">💡 双击修改 | 单击选中/连线 | 右键删除连线 | 滚轮缩放 | Delete删除</div>'
			+ '</div></div>';

		document.getElementById('plc-sig-search').oninput=refreshSigPanel;
		document.getElementById('plc-sig-reload').onclick=function(){loadedFiles['config/SYSTEM/datacenter_out.xml']=null;dcOutSignals=[];loadDcOutSignals(function(){refreshSigPanel();});};
		document.getElementById('plc-edit-load').onclick=function(){var base=document.getElementById('plc-edit-base').value;if(base==='empty'){resetEditor();return;}var path=base==='backup'?'config/PLC/Reclose_logic.xml.bak':'config/PLC/Reclose_logic.xml';logicGraphs=[];loadedFiles[path]=null;requestFile(path,function(c){if(!c){showToast('加载失败: '+path,'err');resetEditor();return;}parseLogicXml(c,path);loadGraphIntoEditor(path);});};
		document.getElementById('plc-edit-key').oninput=function(){editKey=this.value;};
		document.getElementById('plc-edit-name').oninput=function(){editName=this.value;};
		document.getElementById('plc-edit-add-in').onclick=function(){showAddIOModal(NT_INPUT);};
		document.getElementById('plc-edit-add-out').onclick=function(){showAddIOModal(NT_OUTPUT);};
		document.getElementById('plc-edit-add-gate').onclick=showAddGateModal;
		document.getElementById('plc-edit-zoomin').onclick=function(){editScale=Math.min(3,editScale*1.2);drawEditor();};
		document.getElementById('plc-edit-zoomout').onclick=function(){editScale=Math.max(0.2,editScale/1.2);drawEditor();};
		document.getElementById('plc-edit-del').onclick=deleteSelectedNode;
		document.getElementById('plc-edit-modify').onclick=modifySelectedNode;
		document.getElementById('plc-edit-generate').onclick=generateAndSaveConfig;
		setupEditorCanvas();drawEditor();
			/* 运算符面板点击事件 */
			var opList=document.getElementById('plc-op-list');
			if(opList){[].forEach.call(opList.querySelectorAll('.plc-op-row'),function(row){row.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt);PlcConfigPage._addOp(gt);};});}
			/* 运算符面板点击事件 */
			var opList=document.getElementById('plc-op-list');
			if(opList){[].forEach.call(opList.querySelectorAll('.plc-op-row'),function(row){row.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt);PlcConfigPage._addOp(gt);};});}
		/* 重试之前未成功的文件请求（同页切换时 onEnter 不触发） */
		if (PlcConfigPage._retryList && PlcConfigPage._retryList.length > 0) {
			var _rty = PlcConfigPage._retryList.slice();
			PlcConfigPage._retryList = [];
			for (var _ri = 0; _ri < _rty.length; _ri++)
				requestFile(_rty[_ri], (PlcConfigPage._pending || {})[_rty[_ri]] || function(){});
		}
		if(dcOutSignals.length===0){loadDcOutSignals(function(){refreshSigPanel();autoLoadEditor();});}else{refreshSigPanel();autoLoadEditor();}
	}

	function resetEditor(){editNodes=[];editLinks=[];editKey=String(Date.now());editName='';editNextGateIdx={};var kel=document.getElementById('plc-edit-key'),nel=document.getElementById('plc-edit-name');if(kel)kel.value=editKey;if(nel)nel.value=editName;drawEditor();showToast('已重置为空白','ok');}

	function autoLoadEditor(){if(editNodes.length>0)return;loadCurrentLogic(function(){if(logicGraphs.length>0)loadGraphIntoEditor('Reclose_logic.xml');else resetEditor();});}

	function loadGraphIntoEditor(srcName){var g=null;for(var gi=0;gi<logicGraphs.length;gi++){if(logicGraphs[gi]._source&&(logicGraphs[gi]._source===srcName||logicGraphs[gi]._source.endsWith('/'+srcName))){g=logicGraphs[gi];break;}}if(!g&&logicGraphs.length>0)g=logicGraphs[0];if(!g){resetEditor();return;}editNodes=[];editLinks=[];editKey=g.key;editName=g.name||'';for(var i=0;i<g.nodes.length;i++){var n=g.nodes[i];var sig=findSignalByNo(n.id);var nd={type:n.type,id:n.id,x:n.x,y:n.y,_lx:n.x,_ly:n.y,saddr:(sig&&sig.saddr)||'',desc:(sig&&sig.desc)||'',no:n.id};if(n.type!==NT_INPUT&&n.type!==NT_OUTPUT)nd.inPins=(n.inPins!==undefined)?n.inPins:((NODE_INFO[n.type]||{}).inPins||2);if(n.delayMs!==undefined)nd.delayMs=n.delayMs;if(n.combId!==undefined)nd.combId=n.combId;if(n.rsR)nd.rsR=n.rsR;if(n.rsS)nd.rsS=n.rsS;editNodes.push(nd);}for(var j=0;j<g.links.length;j++){var l=g.links[j],si=findEditIdx(l.src_type,l.src_id),di=findEditIdx(l.dst_type,l.dst_id);if(si>=0&&di>=0){var el={srcIdx:si,dstIdx:di};if(l.dstPin!==undefined)el.dstPin=l.dstPin;editLinks.push(el);}}var mx={};for(var di=0;di<editNodes.length;di++){var dn=editNodes[di];if(dn.type===NT_INPUT||dn.type===NT_OUTPUT)continue;var idx=dn.id&0xFF;if(idx>=(mx[dn.type]||-1))mx[dn.type]=idx;}for(var t in mx)editNextGateIdx[t]=mx[t]+1;var seen={};for(var di=0;di<editNodes.length;di++){var dn=editNodes[di];if(dn.type===NT_INPUT||dn.type===NT_OUTPUT)continue;var k=dn.type+':'+(dn.id&0xFF);if(seen[k]){var nxt=editNextGateIdx[dn.type]||0;editNodes[di].id=(dn.id&~0xFF)|nxt;editNextGateIdx[dn.type]=nxt+1;}else seen[k]=true;}var kel=document.getElementById('plc-edit-key'),nel=document.getElementById('plc-edit-name');if(kel)kel.value=editKey;if(nel)nel.value=editName;drawEditor();}

	function findEditIdx(type,id){for(var i=0;i<editNodes.length;i++){var n=editNodes[i];if(n.type===type){if(type<=1&&n.id===id)return i;if(type>1&&(n.id&0xFF)===(id&0xFF))return i;}}return -1;}

	/* ---- 编辑器 Canvas ---- */
	function setupEditorCanvas(){var wrap=document.getElementById('plc-edit-canvas-wrap'),cnv=document.getElementById('plc-edit-canvas');if(!wrap||!cnv)return;function resize(){cnv.width=wrap.clientWidth;cnv.height=Math.max(500,wrap.clientHeight-4);editScale=0.7;editPanX=cnv.width/2;editPanY=cnv.height/2;drawEditor();}setTimeout(resize,50);window.addEventListener('resize',function(){resize();});cnv.onmousedown=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;var ph=hitTestPin(mx,my);if(ph){if(editWiring){if(editWiring.fromIdx!==ph.nodeIdx){if(editWiring.fromSide==='out'&&ph.side==='in'){var si=NODE_INFO[editNodes[editWiring.fromIdx].type]||{},om=si.outMax||99,oc=0;for(var lc=0;lc<editLinks.length;lc++)if(editLinks[lc].srcIdx===editWiring.fromIdx)oc++;if(oc>=om){showToast('输出上限('+om+')','err');editWiring=null;editSelectedNode=-1;drawEditor();return;}var lk={srcIdx:editWiring.fromIdx,dstIdx:ph.nodeIdx,dstPin:ph.pinIdx};var dn=editNodes[ph.nodeIdx];var dInfo=NODE_INFO[dn.type]||{};if(dInfo.inPins>1){for(var lc2=0;lc2<editLinks.length;lc2++){if(editLinks[lc2].dstIdx===ph.nodeIdx&&editLinks[lc2].dstPin===ph.pinIdx){showToast('引脚已连线','err');editWiring=null;editSelectedNode=-1;drawEditor();return;}}}editLinks.push(lk);showToast('连线已添加','ok');}else if(editWiring.fromSide==='in'&&ph.side==='out'){editLinks.push({srcIdx:ph.nodeIdx,dstIdx:editWiring.fromIdx,dstPin:0});showToast('连线已添加','ok');}else{showToast('不能这样连线','err');}}editWiring=null;editSelectedNode=-1;editSelectedLink=-1;drawEditor();return;}else{editWiring={fromIdx:ph.nodeIdx,fromSide:ph.side,fromPin:ph.pinIdx};editSelectedNode=-1;editSelectedLink=-1;drawEditor();return;}}var lh=hitTestLink(mx,my);if(lh>=0){editSelectedLink=lh;editSelectedNode=-1;editDragNode=null;if(editWiring)editWiring=null;drawEditor();return;}var nh=hitTestNode(mx,my);if(nh>=0){editSelectedNode=nh;editSelectedLink=-1;editDragNode=nh;editDragOffX=mx-editNodes[nh]._lx;editDragOffY=my-editNodes[nh]._ly;drawEditor();return;}editSelectedNode=-1;editSelectedLink=-1;if(editWiring)editWiring=null;editDragCanvas={sx:e.clientX,sy:e.clientY,px:editPanX,py:editPanY};cnv.style.cursor='grabbing';drawEditor();};cnv.onmousemove=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;if(editDragCanvas){editPanX=editDragCanvas.px+(e.clientX-editDragCanvas.sx);editPanY=editDragCanvas.py+(e.clientY-editDragCanvas.sy);drawEditor();return;}if(editDragNode!==null){editNodes[editDragNode]._lx=mx-editDragOffX;editNodes[editDragNode]._ly=my-editDragOffY;drawEditor();return;}if(editWiring&&editWiring.fromIdx<editNodes.length)drawEditor();var pin=hitTestPin(mx,my);cnv.style.cursor=pin?'crosshair':(hitTestNode(mx,my)>=0?'grab':'default');};cnv.onmouseup=function(e){if(editDragCanvas){editDragCanvas=null;cnv.style.cursor='default';return;}if(editDragNode!==null){var nd=editNodes[editDragNode],ni=editDragNode;var dx=Math.abs((e.clientX-cnv.getBoundingClientRect().left-editPanX)/editScale-nd._lx-editDragOffX);var dy=Math.abs((e.clientY-cnv.getBoundingClientRect().top-editPanY)/editScale-nd._ly-editDragOffY);if(dx<3&&dy<3){var n=Date.now();if(ni===editLastClickNode&&(n-editLastClickTime)<400){onNodeDblClick(ni);editLastClickTime=0;editLastClickNode=-1;}else{editLastClickTime=n;editLastClickNode=ni;}}}editDragNode=null;};cnv.onmouseleave=function(){editDragNode=null;editDragCanvas=null;cnv.style.cursor='default';};cnv.onwheel=function(e){e.preventDefault();var d=e.deltaY>0?0.9:1.1,r=cnv.getBoundingClientRect();var ns=Math.max(0.2,Math.min(3,editScale*d));editPanX=(e.clientX-r.left)-((e.clientX-r.left)-editPanX)*(ns/editScale);editPanY=(e.clientY-r.top)-((e.clientY-r.top)-editPanY)*(ns/editScale);editScale=ns;drawEditor();};cnv.oncontextmenu=function(e){e.preventDefault();if(editSelectedLink>=0){editLinks.splice(editSelectedLink,1);editSelectedLink=-1;editSelectedNode=-1;showToast('连线已删除','ok');drawEditor();return;}if(editSelectedNode>=0){deleteSelectedNode();return;}};cnv.tabIndex=0;cnv.onkeydown=function(e){if(e.key==='Escape'){editWiring=null;editSelectedNode=-1;editSelectedLink=-1;drawEditor();}if(e.key==='Delete'&&editSelectedNode>=0){deleteSelectedNode();}if(e.key==='Delete'&&editSelectedLink>=0){editLinks.splice(editSelectedLink,1);editSelectedLink=-1;showToast('连线已删除','ok');drawEditor();}};}

	function hitTestNode(mx,my){for(var i=editNodes.length-1;i>=0;i--){var n=editNodes[i],sz=getNodeSize(n);if(mx>n._lx-sz.w/2&&mx<n._lx+sz.w/2&&my>n._ly-sz.h/2&&my<n._ly+sz.h/2)return i;}return -1;}
	function hitTestPin(mx,my){var pinR=8;for(var i=editNodes.length-1;i>=0;i--){var n=editNodes[i],sz=getNodeSize(n),info=NODE_INFO[n.type]||NODE_INFO[0];var isIO=(n.type===NT_INPUT||n.type===NT_OUTPUT),isSR=(n.type===NT_SR||n.type===NT_RS);var inP=getNodeInPins(n);if(info.inPins>0){var tp=isSR?2:(isIO?inP:inP);for(var p=0;p<tp;p++){var py=n._ly-sz.h/2+sz.h*(p+1)/(isSR?3:(inP+1));if(Math.sqrt((mx-(n._lx-sz.w/2))*(mx-(n._lx-sz.w/2))+(my-py)*(my-py))<=pinR)return{nodeIdx:i,side:'in',pinIdx:p};}}if(info.outPins>0){if(Math.sqrt((mx-(n._lx+sz.w/2))*(mx-(n._lx+sz.w/2))+(my-n._ly)*(my-n._ly))<=pinR)return{nodeIdx:i,side:'out',pinIdx:0};}}return null;}
	function hitTestLink(mx,my){for(var i=0;i<editLinks.length;i++){var l=editLinks[i];if(l.srcIdx>=editNodes.length||l.dstIdx>=editNodes.length)continue;var sn=editNodes[l.srcIdx],dn=editNodes[l.dstIdx],szS=getNodeSize(sn),szD=getNodeSize(dn);var x1=sn._lx+szS.w/2,y1=sn._ly,isSR=(dn.type===NT_SR||dn.type===NT_RS);var inP=getNodeInPins(dn),dpi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-szD.h/2+szD.h*(dpi+1)/(isSR?3:(inP+1)),x2=dn._lx-szD.w/2;var t=Math.max(0,Math.min(1,((mx-x1)*(x2-x1)+(my-y1)*(y2-y1))/((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))));var px=x1+t*(x2-x1),py=y1+t*(y2-y1);if(Math.sqrt((mx-px)*(mx-px)+(my-py)*(my-py))<10)return i;}return -1;}

	function drawEditor(){var cnv=document.getElementById('plc-edit-canvas');if(!cnv)return;var ctx=cnv.getContext('2d'),W=cnv.width,H=cnv.height;ctx.clearRect(0,0,W,H);ctx.fillStyle='#0a0f16';ctx.fillRect(0,0,W,H);ctx.save();ctx.translate(editPanX,editPanY);ctx.scale(editScale,editScale);ctx.strokeStyle='rgba(255,255,255,0.05)';ctx.lineWidth=1;var gs=40,sx=Math.floor(-editPanX/editScale/gs)*gs,sy=Math.floor(-editPanY/editScale/gs)*gs;for(var gx=sx-gs;gx<sx+W/editScale+gs;gx+=gs){ctx.beginPath();ctx.moveTo(gx,sy-gs);ctx.lineTo(gx,sy+H/editScale+gs);ctx.stroke();}for(var gy=sy-gs;gy<sy+H/editScale+gs;gy+=gs){ctx.beginPath();ctx.moveTo(sx-gs,gy);ctx.lineTo(sx+W/editScale+gs,gy);ctx.stroke();}ctx.fillStyle='rgba(255,255,255,0.08)';ctx.font='24px sans-serif';ctx.textAlign='center';ctx.fillText('📥 输入区',-250,-180);ctx.fillText('🧮 逻辑区',0,-180);ctx.fillText('📤 输出区',250,-180);drawGraphContent(ctx,editNodes,editLinks,editSelectedNode,editSelectedLink,editWiring);ctx.restore();var ie=document.getElementById('plc-edit-info');if(ie){var si='';if(editSelectedNode>=0&&editSelectedNode<editNodes.length)si=' | 🔍 '+getNodeDisplayLabel(editNodes[editSelectedNode]);ie.innerHTML='节点:'+editNodes.length+' | 连线:'+editLinks.length+' | Key:'+escHtml(editKey)+' | '+escHtml(editName||'(未命名)')+si+(editWiring?' | 🔗 连线...':'')+' | 右键删除连线';}}

	/* ---- 节点操作 ---- */
	function onNodeDblClick(idx){var nd=editNodes[idx];editSelectedNode=idx;drawEditor();if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(idx);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(idx);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(idx);else if(nd.type>=50&&nd.type<=56)showMXInfoModal(idx);else showGateCfgModal(idx);}
	function modifySelectedNode(){if(editSelectedNode<0||editSelectedNode>=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode];if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(editSelectedNode);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(editSelectedNode);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(editSelectedNode);else if(nd.type>=50&&nd.type<=56)showMXInfoModal(editSelectedNode);else showGateCfgModal(editSelectedNode);}
	function deleteSelectedNode(){if(editSelectedNode<0||editSelectedNode>=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode],idx=editSelectedNode,lb=getNodeDisplayLabel(nd);showConfirm('🗑 确认删除','确定删除 <strong style="color:#f44336">'+escHtml(lb)+'</strong> 及其连线？',function(){var dt=nd.type;editLinks=editLinks.filter(function(l){return l.srcIdx!==idx&&l.dstIdx!==idx;});for(var i=0;i<editLinks.length;i++){if(editLinks[i].srcIdx>idx)editLinks[i].srcIdx--;if(editLinks[i].dstIdx>idx)editLinks[i].dstIdx--;}editNodes.splice(idx,1);editSelectedNode=-1;if(dt!==NT_INPUT&&dt!==NT_OUTPUT){var st=[];for(var s=0;s<editNodes.length;s++)if(editNodes[s].type===dt)st.push(s);st.sort(function(a,b){return(editNodes[a].id&0xFF)-(editNodes[b].id&0xFF);});for(var a=0;a<st.length;a++)editNodes[st[a]].id=(editNodes[st[a]].id&~0xFF)|a;editNextGateIdx[dt]=st.length;}drawEditor();showToast('已删除','ok');});}

	/* ---- Modals ---- */
	function showConfirm(title,msg,cb){var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:420px"><h3>'+title+'</h3><p style="margin:12px 0;font-size:14px">'+msg+'</p><div style="margin-top:16px;display:flex;gap:8px"><button class="btn btn-danger" id="plc-modal-ok">确认</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){closeModal();cb();};}
	function closeModal(){var m=document.getElementById('plc-modal');if(m)m.remove();}

	function showIOModal(title,cb){if(dcOutSignals.length===0){showToast('信号列表为空','err');return;}var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:600px"><h3>'+title+'</h3><input type="text" id="plc-modal-search" placeholder="搜索..." style="width:100%;margin:8px 0;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary)"><div style="max-height:350px;overflow-y:auto" id="plc-modal-list">';for(var j=0;j<dcOutSignals.length;j++){var s=dcOutSignals[j];h+='<div class="plc-modal-row" data-no="'+s.no+'" data-saddr="'+escHtml(s.saddr)+'" data-desc="'+escHtml(s.desc)+'"><strong>#'+s.no+' '+escHtml(s.saddr)+'</strong> <span style="color:var(--text-muted)">'+escHtml(s.desc)+'</span></div>';}h+='</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;var si2=document.getElementById('plc-modal-search');if(si2)si2.oninput=function(){var q=this.value.toLowerCase();document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.style.display=(r.textContent||'').toLowerCase().indexOf(q)!==-1?'':'none';});};document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.onclick=function(){var no=parseInt(this.dataset.no)||0,sa=this.dataset.saddr,de=this.dataset.desc||'';var rs=cb(no,sa,de);if(rs!==false)closeModal();};});}

	function showAddIOModal(ioType){var title=ioType===NT_INPUT?'📥 绑定输入信号':'📤 绑定输出信号';showIOModal(title,function(no,saddr,desc){editNodes.push({type:ioType,id:no,x:0,y:0,_lx:ioType===NT_INPUT?-260:260,_ly:editNodes.filter(function(n){return n.type===ioType;}).length*110,saddr:saddr,desc:desc,no:no});drawEditor();showToast('已添加: '+saddr,'ok');return true;});}
	function showRebindModal(idx){var nd=editNodes[idx],title=nd.type===NT_INPUT?'📥 重新绑定输入':'📤 重新绑定输出';showIOModal(title,function(no,saddr,desc){editNodes[idx].no=no;editNodes[idx].id=no;editNodes[idx].saddr=saddr;editNodes[idx].desc=desc;drawEditor();showToast('已绑定: '+saddr,'ok');return true;});}

	function showAddGateModal(){var gates=[{t:NT_AND,n:'与门AND',d:'全1出1',i:'🔷'},{t:NT_OR,n:'或门OR',d:'有1出1',i:'🔶'},{t:NT_NOT,n:'非门NOT',d:'取反',i:'🔺'},{t:NT_62P,n:'62p延时',d:'开延时',i:'⏱️'},{t:NT_62D,n:'62d延时',d:'关延时',i:'⏱️'},{t:NT_RISING,n:'上升沿',d:'0→1脉冲',i:'⬆️'},{t:NT_FALLING,n:'下降沿',d:'1→0脉冲',i:'⬇️'},{t:NT_TOGGLE,n:'翻转元件',d:'输入变化翻转',i:'🔄'},{t:NT_SR,n:'SR触发器',d:'S优先',i:'🔒'},{t:NT_RS,n:'RS触发器',d:'R优先',i:'🔒'},{t:NT_MX_ADD,n:'加法元件',d:'两输入求和',i:'➕'},{t:NT_MX_SUB,n:'减法元件',d:'两输入差绝对值',i:'➖'},{t:NT_MX_MAX,n:'最大值元件',d:'两输入取大',i:'⬆️'},{t:NT_MX_MIN,n:'最小值元件',d:'两输入取小',i:'⬇️'},{t:NT_MX_MUL,n:'乘法元件',d:'两输入乘积',i:'✖️'},{t:NT_MX_DIV,n:'除法元件',d:'两输入之商(除0出0)',i:'➗'},{t:NT_MX_ABS,n:'绝对值元件',d:'单输入取绝对值',i:'📏'},{t:NT_MX_CMP,n:'比较元件',d:'A>B出1,否则出0',i:'⚖️'}];var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:480px"><h3>🧮 选择运算模块</h3><div style="max-height:400px;overflow-y:auto">';for(var i=0;i<gates.length;i++){var g=gates[i];h+='<div class="plc-modal-row" data-gt="'+g.t+'"><strong>'+g.i+' '+g.n+'</strong><br><span style="color:var(--text-muted);font-size:12px">'+g.d+'</span></div>';}h+='</div><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt),info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0;if(gt===NT_62P||gt===NT_62D){closeModal();showDelayInputModal(gt,ni,info);return;}addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;closeModal();drawEditor();};});}

	function showDelayInputModal(gateType,nextIdx,info){var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:360px"><h3>⏱️ '+info.name+' — 延时配置</h3><p style="color:var(--text-muted);margin:8px 0">延时(毫秒)</p><input type="number" id="plc-delay-input" value="1000" min="0" max="600000" step="100" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:16px;margin:8px 0"><div style="margin-top:12px;display:flex;gap:8px"><button class="btn btn-primary" id="plc-modal-ok">✅ 确认</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){var v=parseInt(document.getElementById('plc-delay-input').value);if(isNaN(v)||v<0){showToast('无效延时','err');return;}closeModal();addGateNode(gateType,nextIdx,v,info);editNextGateIdx[gateType]=nextIdx+1;drawEditor();};document.getElementById('plc-delay-input').onkeydown=function(e){if(e.key==='Enter')document.getElementById('plc-modal-ok').click();};setTimeout(function(){var inp=document.getElementById('plc-delay-input');if(inp){inp.focus();inp.select();}},100);}
	function showDelayEditModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},cur=nd.delayMs||1000;var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:360px"><h3>⏱️ '+info.name+' — 修改延时</h3><p style="color:var(--text-muted)">当前: <strong>'+cur+' ms</strong></p><input type="number" id="plc-delay-input" value="'+cur+'" min="0" max="600000" step="100" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-input);color:var(--text-primary);font-size:16px;margin:8px 0"><div style="margin-top:12px;display:flex;gap:8px"><button class="btn btn-primary" id="plc-modal-ok">💾 保存</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){var v=parseInt(document.getElementById('plc-delay-input').value);if(isNaN(v)||v<0){showToast('无效延时','err');return;}editNodes[idx].delayMs=v;closeModal();drawEditor();showToast('延时更新: '+v+' ms','ok');};document.getElementById('plc-delay-input').onkeydown=function(e){if(e.key==='Enter')document.getElementById('plc-modal-ok').click();};setTimeout(function(){var inp2=document.getElementById('plc-delay-input');if(inp2){inp2.focus();inp2.select();}},100);}
	function showRSInfoModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},sSrc='',rSrc='';for(var li=0;li<editLinks.length;li++){var l=editLinks[li];if(l.dstIdx===idx){var snl=editNodes[l.srcIdx],desc=info.icon+' '+(snl.type<=1?'输入点':NODE_INFO[snl.type].name)+' #'+(snl.type<=1?snl.id:(snl.id&0xFF));if(nd.type===NT_SR){if(l.dstPin===0)sSrc=desc;else rSrc=desc;}else{if(l.dstPin===1)sSrc=desc;else rSrc=desc;}}}var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:400px"><h3>🔒 '+info.name+' — 连接</h3><p style="margin:8px 0"><strong>S(置位)</strong>: '+(sSrc||'<span style="color:#f44336">未连接</span>')+'</p><p style="margin:8px 0"><strong>R(复位)</strong>: '+(rSrc||'<span style="color:#f44336">未连接</span>')+'</p><p style="color:var(--text-muted);font-size:12px;margin-top:12px">💡 拖输出到S/R引脚连线</p><button class="btn btn-outline" id="plc-modal-cancel">关闭</button></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;}
	function showMXInfoModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},inPins=getNodeInPins(nd);var aSrc='',bSrc='';for(var li=0;li<editLinks.length;li++){var l=editLinks[li];if(l.dstIdx===idx){var snl=editNodes[l.srcIdx],desc=(snl.type<=1?'输入点':(NODE_INFO[snl.type]||{}).icon+' '+(NODE_INFO[snl.type]||{}).name)+' #'+(snl.type<=1?snl.id:(snl.id&0xFF));if(l.dstPin===0)aSrc=desc;else if(l.dstPin===1)bSrc=desc;}}var orderNote='';if(nd.type===NT_MX_SUB)orderNote='<p style="color:#FF9800;font-size:12px;margin-top:8px">⚠️ 顺序敏感: A - B</p>';else if(nd.type===NT_MX_DIV)orderNote='<p style="color:#FF9800;font-size:12px;margin-top:8px">⚠️ 顺序敏感: A ÷ B (B=0→结果为0)</p>';else if(nd.type===NT_MX_CMP)orderNote='<p style="color:#FF9800;font-size:12px;margin-top:8px">⚠️ 顺序敏感: A > B → 1，否则 → 0</p>';else orderNote='<p style="color:var(--text-muted);font-size:12px;margin-top:8px">💡 输入可交换，顺序不影响结果</p>';var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:400px"><h3>'+info.icon+' '+info.name+' — 连接</h3><p style="margin:8px 0"><strong>A</strong>: '+(aSrc||'<span style="color:#f44336">未连接</span>')+'</p><p style="margin:8px 0"><strong>B</strong>: '+(bSrc||'<span style="color:#f44336">未连接</span>')+'</p>'+orderNote+'<button class="btn btn-outline" id="plc-modal-cancel">关闭</button></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;}
function showGateCfgModal(idx){var nd=editNodes[idx],cur=getNodeInPins(nd),info=NODE_INFO[nd.type]||{},mx=info.inMax||99;var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:380px"><h3>'+info.icon+' '+info.name+' 配置</h3><p style="color:var(--text-muted)">当前输入: <strong>'+cur+'</strong></p><label style="display:block;margin:12px 0 4px">输入引脚数(1~'+mx+'):</label><input type="number" id="plc-gate-pins" value="'+cur+'" min="1" max="'+mx+'" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-input);color:var(--text-primary);font-size:16px"><div style="margin-top:16px;display:flex;gap:8px"><button class="btn btn-primary" id="plc-modal-save">💾 保存</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-save').onclick=function(){var v=parseInt(document.getElementById('plc-gate-pins').value);if(isNaN(v)||v<1){showToast('最少1个','err');return;}if(v>mx){showToast('最多'+mx+'个','err');return;}editNodes[idx].inPins=v;closeModal();drawEditor();showToast(info.name+' 输入='+v,'ok');};}
	function addGateNode(gateType,nextIdx,delayMs,info){var pos=findEmptyPos();var nd={type:gateType,id:nextIdx,x:0,y:0,_lx:pos.x,_ly:pos.y,saddr:'',desc:'',no:0};if(delayMs>0)nd.delayMs=delayMs;editNodes.push(nd);showToast('已添加: '+(info?info.name:NODE_INFO[gateType].name)+' #'+nextIdx,'ok');}
	function findEmptyPos(){if(editNodes.length===0)return{x:0,y:0};var mxX=-Infinity,mnX=Infinity,mxY=-Infinity,mnY=Infinity;for(var i=0;i<editNodes.length;i++){var n=editNodes[i];if(n._lx>mxX)mxX=n._lx;if(n._lx<mnX)mnX=n._lx;if(n._ly>mxY)mxY=n._ly;if(n._ly<mnY)mnY=n._ly;}var cs=[{x:0,y:mxY+140},{x:0,y:mnY-140},{x:mxX+300,y:(mnY+mxY)/2},{x:mnX-300,y:(mnY+mxY)/2}];for(var c=0;c<cs.length;c++){var occ=false;for(var i2=0;i2<editNodes.length;i2++){if(Math.abs(editNodes[i2]._lx-cs[c].x)<120&&Math.abs(editNodes[i2]._ly-cs[c].y)<80){occ=true;break;}}if(!occ)return cs[c];}return{x:0,y:mxY+140};}

	/* ================================================================
	 * 共用 Canvas 绘制
	 * ================================================================ */
	function drawGraphContent(ctx,nodes,links,selectedIdx,selectedLinkIdx,wiring){var IW=250,IH=80,GW=170,GH=66,CR=12,SO=3,LBW=8;for(var i=0;i<links.length;i++){var l=links[i];if(l.srcIdx>=nodes.length||l.dstIdx>=nodes.length)continue;var sn=nodes[l.srcIdx],dn=nodes[l.dstIdx];var sIO=(sn.type===NT_OUTPUT||sn.type===NT_INPUT),dIO=(dn.type===NT_OUTPUT||dn.type===NT_INPUT);var dSR=(dn.type===NT_SR||dn.type===NT_RS);var sW=sIO?IW:GW,dW=dIO?IW:GW,dH=dIO?IH:(dSR?86:GH);var x1=sn._lx+sW/2,y1=sn._ly;var dIP=getNodeInPins(dn),epi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-dH/2+dH*(epi+1)/(dSR?3:(dIP+1)),x2=dn._lx-dW/2;var isAct=wiring&&wiring.fromIdx===l.srcIdx,isSel=(selectedLinkIdx===i);var mid=(x1+x2)/2,la=isAct||isSel?1:0.6,lw=isAct||isSel?3:2;var lc=isSel?'#FF5722':(isAct?'#FFC107':'rgba(180,200,220,'+la+')');ctx.beginPath();ctx.strokeStyle=isSel?'rgba(255,87,34,0.3)':'rgba(255,200,0,'+(la*0.2)+')';ctx.lineWidth=lw+4;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.beginPath();ctx.strokeStyle=lc;ctx.lineWidth=lw;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.fillStyle=lc;ctx.beginPath();ctx.moveTo(x2,y2);ctx.lineTo(x2-7,y2-3.5);ctx.lineTo(x2-7,y2+3.5);ctx.closePath();ctx.fill();if(isSel){var lx2=(x1+x2)/2,ly2=(y1+y2)/2-18;ctx.fillStyle='#FF5722';ctx.font='bold 14px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('✕ 右键删除',lx2,ly2);}}if(wiring&&wiring.fromIdx<nodes.length){var fn=nodes[wiring.fromIdx],fIO=(fn.type===NT_OUTPUT||fn.type===NT_INPUT),fW=fIO?IW:GW;var fx=wiring.fromSide==='out'?fn._lx+fW/2:fn._lx-fW/2,fy=fn._ly;ctx.beginPath();ctx.strokeStyle='#FFD700';ctx.lineWidth=2;ctx.setLineDash([6,4]);ctx.moveTo(fx,fy);ctx.lineTo(fx+(wiring.fromSide==='out'?100:-100),fy);ctx.stroke();ctx.setLineDash([]);}for(var j=0;j<nodes.length;j++){var n=nodes[j],info=NODE_INFO[n.type]||{name:'?',color:'#555',icon:'?'};var isIO=(n.type===NT_OUTPUT||n.type===NT_INPUT),inP=getNodeInPins(n);var nw=isIO?IW:GW,nh=isIO?IH:(GH+Math.max(0,inP-2)*14);var x=n._lx,y=n._ly,hw=nw/2,hh=nh/2;var isWS=wiring&&wiring.fromIdx===j,isS=(selectedIdx===j);var gw=(isS||isWS)?3:1;ctx.fillStyle='rgba(0,0,0,0.5)';ctx.beginPath();ctx.roundRect(x-hw+SO,y-hh+SO,nw,nh,CR);ctx.fill();var grad=ctx.createLinearGradient(x-hw,y-hh,x-hw,y+hh);if(isS){grad.addColorStop(0,'#3b3b6b');grad.addColorStop(1,'#282848');}else if(isWS){grad.addColorStop(0,'#3c3c50');grad.addColorStop(1,'#252538');}else{grad.addColorStop(0,'#2e2e4a');grad.addColorStop(1,'#1e1e36');}ctx.fillStyle=grad;ctx.beginPath();ctx.roundRect(x-hw,y-hh,nw,nh,CR);ctx.fill();ctx.strokeStyle=isS?'#FFD700':(isWS?'#FFC107':info.color);ctx.lineWidth=gw;ctx.beginPath();ctx.roundRect(x-hw,y-hh,nw,nh,CR);ctx.stroke();ctx.fillStyle=info.color;ctx.beginPath();ctx.roundRect(x-hw,y-hh,LBW,nh,[CR,0,0,CR]);ctx.fill();if(info.name){var lbx=x-hw+LBW,lbw=info.name.length*7.5+18,lby=y-hh+2,lbh=16;ctx.fillStyle=info.color;ctx.globalAlpha=0.25;ctx.fillRect(lbx,lby,Math.min(lbw,nw-LBW-4),lbh);ctx.globalAlpha=1;ctx.fillStyle=isIO?'#fff':info.color;ctx.font='9px sans-serif';ctx.textAlign='left';ctx.textBaseline='top';ctx.fillText(info.name,lbx+4,lby+2.5);}ctx.textAlign='center';ctx.textBaseline='middle';if(isIO){var sig=n.saddr?n:findSignalByNo(n.id),pfx=n.type===NT_INPUT?'📥':'📤',rl=n.type===NT_INPUT?'输入点':'输出点';ctx.fillStyle='#e0e0e0';ctx.font='bold 12px sans-serif';ctx.fillText(pfx+' '+rl+' #'+n.id,x,y-18);ctx.fillStyle='#aaa';ctx.font='10px monospace';ctx.fillText((sig&&sig.saddr)?sig.saddr:'—',x,y);var dt2=(sig&&sig.desc)?sig.desc:'';if(dt2){ctx.fillStyle='#888';ctx.font='9px sans-serif';ctx.fillText(dt2.length>18?dt2.substring(0,16)+'…':dt2,x,y+14);}}else{var gi=n.id&0xFF,ri=0,ro=0;for(var li=0;li<links.length;li++){if(links[li].dstIdx===j)ri++;if(links[li].srcIdx===j)ro++;}ctx.fillStyle='#e8e8e8';ctx.font='bold 11px sans-serif';var tt=info.icon+' #'+gi;if(n.type===NT_62P||n.type===NT_62D)tt+=' ('+(n.delayMs||1000)+'ms)';ctx.fillText(tt,x,y-6);ctx.fillStyle='#999';ctx.font='9px monospace';ctx.fillText(ri+'入 '+ro+'出',x,y+14);}if(inP>0){var isSR=(n.type===NT_SR||n.type===NT_RS),rp=isSR?2:(isIO?inP:inP);for(var p=0;p<rp;p++){var py=y-nh/2+nh*(p+1)/(isSR?3:(inP+1));ctx.fillStyle='#2e2e4a';ctx.beginPath();ctx.arc(x-hw,py,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle=isSR?(p===0?'#FFC107':'#FF5722'):'#4CAF50';ctx.lineWidth=2;ctx.stroke();ctx.fillStyle=isSR?(p===0?'#FFC107':'#FF5722'):'#4CAF50';ctx.beginPath();ctx.arc(x-hw,py,3.5,0,Math.PI*2);ctx.fill();if(isSR){ctx.fillStyle='#fff';ctx.font='bold 10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';var sl=(n.type===NT_SR)?['S','R']:['R','S'];ctx.fillText(sl[p],x-hw-12,py);}var isMX=(n.type>=50&&n.type<=57);if(isMX){ctx.fillStyle='#fff';ctx.font='bold 10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';ctx.fillText(p===0?'A':'B',x-hw-12,py);}}}if(info.outPins>0){ctx.fillStyle='#2e2e4a';ctx.beginPath();ctx.arc(x+hw,y,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#FF9800';ctx.lineWidth=2;ctx.stroke();ctx.fillStyle='#FF9800';ctx.beginPath();ctx.arc(x+hw,y,3.5,0,Math.PI*2);ctx.fill();}}}

	/* ================================================================
	 * XML 解析 (与原工程完全一致)
	 * ================================================================ */
	function parseLogicXml(xmlStr,sourceName){var parser=new DOMParser(),doc=parser.parseFromString(xmlStr,'text/xml'),root=doc.documentElement;if(!root||root.nodeName!=='PLCConfig'){showToast('根元素不是 PLCConfig','err');return;}var thisKey=root.getAttribute('key')||sourceName,thisName=root.getAttribute('name')||'';var g={key:thisKey,name:thisName,nodes:[],links:[]};var GTM={INPUT:1,OUTPUT:0,OR:2,AND:3,NOT:4,T62P:5,T62D:6,RISING:7,SR:8,RS:9,FALLING:10,TOGGLE:11,MX_ADD:50,MX_SUB:51,MX_MAX:52,MX_MIN:53,MX_MUL:54,MX_DIV:55,MX_ABS:56,MX_CMP:57};var sigMap={};function ens(ioType,no,x,y){var k=ioType+':'+no;if(sigMap[k]!==undefined){if(x!==undefined){g.nodes[sigMap[k]].x=x;g.nodes[sigMap[k]].y=y;}return sigMap[k];}g.nodes.push({type:ioType,id:no,x:x||0,y:y||0});sigMap[k]=g.nodes.length-1;return g.nodes.length-1;}function eng(type,id,x,y,inPins,delayMs){for(var i=0;i<g.nodes.length;i++)if(g.nodes[i].type===type&&(g.nodes[i].id&0xFF)===(id&0xFF))return i;var nd={type:type,id:id,x:x||0,y:y||0};if(inPins)nd.inPins=inPins;if(delayMs)nd.delayMs=delayMs;g.nodes.push(nd);return g.nodes.length-1;}function parseComb(ce){var ge=ce.firstElementChild;while(ge&&ge.nodeName!=='Gate')ge=ge.nextElementSibling;if(!ge)return -1;var t=GTM[ge.getAttribute('type')]||0,gid=parseInt(ge.getAttribute('id'));var gi=eng(t,gid,parseInt(ge.getAttribute('x')),parseInt(ge.getAttribute('y')),ge.getAttribute('inPins')?parseInt(ge.getAttribute('inPins')):null,ge.getAttribute('delayMs')?parseInt(ge.getAttribute('delayMs')):null);var cid=ce.getAttribute('id');if(cid)g.nodes[gi].combId=parseInt(cid);var ra=ge.getAttribute('R'),sa=ge.getAttribute('S');if(ra)g.nodes[gi].rsR=ra;if(sa)g.nodes[gi].rsS=sa;var c=ce.firstElementChild;while(c){if(c.nodeName==='Comb'){var upi=parseComb(c);if(upi>=0)g.links.push({src_type:g.nodes[upi].type,src_id:g.nodes[upi].id,dst_type:t,dst_id:gid});}c=c.nextElementSibling;}var ie=ce.firstElementChild;while(ie&&ie.nodeName!=='Inputs')ie=ie.nextElementSibling;if(ie){var s=ie.firstElementChild;while(s){if(s.nodeName==='Signal'){var no=parseInt(s.getAttribute('no'));ens(NT_INPUT,no,parseInt(s.getAttribute('x')),parseInt(s.getAttribute('y')));g.links.push({src_type:NT_INPUT,src_id:no,dst_type:t,dst_id:gid});}s=s.nextElementSibling;}}var oe=ce.firstElementChild;while(oe&&oe.nodeName!=='Outputs')oe=oe.nextElementSibling;if(oe){var s2=oe.firstElementChild;while(s2){if(s2.nodeName==='Signal'){var no2=parseInt(s2.getAttribute('no'));ens(NT_OUTPUT,no2,parseInt(s2.getAttribute('x')),parseInt(s2.getAttribute('y')));g.links.push({src_type:t,src_id:gid,dst_type:NT_OUTPUT,dst_id:no2});}s2=s2.nextElementSibling;}}return gi;}var chains=root.getElementsByTagName('Chain');for(var ci=0;ci<chains.length;ci++){var chain=chains[ci],c=chain.firstElementChild;while(c){if(c.nodeName==='Inputs'){var s=c.firstElementChild;while(s){if(s.nodeName==='Signal')ens(NT_INPUT,parseInt(s.getAttribute('no')),parseInt(s.getAttribute('x')),parseInt(s.getAttribute('y')));s=s.nextElementSibling;}}if(c.nodeName==='Outputs'){var s2=c.firstElementChild;while(s2){if(s2.nodeName==='Signal')ens(NT_OUTPUT,parseInt(s2.getAttribute('no')),parseInt(s2.getAttribute('x')),parseInt(s2.getAttribute('y')));s2=s2.nextElementSibling;}}if(c.nodeName==='Comb')parseComb(c);if(c.nodeName==='Gate'){var gt=GTM[c.getAttribute('type')]||0,gi2=eng(gt,parseInt(c.getAttribute('id')),parseInt(c.getAttribute('x')),parseInt(c.getAttribute('y')));var ra=c.getAttribute('R'),sa=c.getAttribute('S'),dms=c.getAttribute('delayMs');if(ra)g.nodes[gi2].rsR=ra;if(sa)g.nodes[gi2].rsS=sa;if(dms)g.nodes[gi2].delayMs=parseInt(dms);}c=c.nextElementSibling;}}for(var ci2=0;ci2<chains.length;ci2++){var c2=chains[ci2];var hc=false,c3=c2.firstElementChild;while(c3){if(c3.nodeName==='Comb'){hc=true;break;}c3=c3.nextElementSibling;}var ge2=null,ie2=null,oe2=null;c3=c2.firstElementChild;while(c3){if(c3.nodeName==='Gate')ge2=c3;if(c3.nodeName==='Inputs')ie2=c3;if(c3.nodeName==='Outputs')oe2=c3;c3=c3.nextElementSibling;}if(hc&&ge2){c3=c2.firstElementChild;while(c3){if(c3.nodeName==='Comb'){var cg=c3.firstElementChild;while(cg&&cg.nodeName!=='Gate')cg=cg.nextElementSibling;if(cg){var ct=GTM[cg.getAttribute('type')]||0,cid2=parseInt(cg.getAttribute('id'));g.links.push({src_type:ct,src_id:cid2,dst_type:GTM[ge2.getAttribute('type')]||0,dst_id:parseInt(ge2.getAttribute('id'))});}}c3=c3.nextElementSibling;}var alrIn={};if(ie2){var s3=ie2.firstElementChild;while(s3){if(s3.nodeName==='Signal'){var n2=parseInt(s3.getAttribute('no'));alrIn[n2]=true;g.links.push({src_type:NT_INPUT,src_id:n2,dst_type:GTM[ge2.getAttribute('type')]||0,dst_id:parseInt(ge2.getAttribute('id'))});}s3=s3.nextElementSibling;}}if(ge2){var gtCS=GTM[ge2.getAttribute('type')]||0;if(gtCS===NT_SR||gtCS===NT_RS){var sa2=ge2.getAttribute('S'),ra2=ge2.getAttribute('R');if(sa2&&sa2.indexOf('Input#')===0){var sn2=parseInt(sa2.substring(6));if(!alrIn[sn2]){ens(NT_INPUT,sn2,0,0);g.links.push({src_type:NT_INPUT,src_id:sn2,dst_type:gtCS,dst_id:parseInt(ge2.getAttribute('id'))});}}if(ra2&&ra2.indexOf('Input#')===0){var rn2=parseInt(ra2.substring(6));if(!alrIn[rn2]){ens(NT_INPUT,rn2,0,0);g.links.push({src_type:NT_INPUT,src_id:rn2,dst_type:gtCS,dst_id:parseInt(ge2.getAttribute('id'))});}}}}if(oe2){var s4=oe2.firstElementChild;while(s4){if(s4.nodeName==='Signal')g.links.push({src_type:GTM[ge2.getAttribute('type')]||0,src_id:parseInt(ge2.getAttribute('id')),dst_type:NT_OUTPUT,dst_id:parseInt(s4.getAttribute('no'))});s4=s4.nextElementSibling;}}}else if(!hc&&ge2){var gt2=GTM[ge2.getAttribute('type')]||0,gid2=parseInt(ge2.getAttribute('id'));if(ie2){var s5=ie2.firstElementChild;while(s5){if(s5.nodeName==='Signal')g.links.push({src_type:NT_INPUT,src_id:parseInt(s5.getAttribute('no')),dst_type:gt2,dst_id:gid2});s5=s5.nextElementSibling;}}if(oe2){var s6=oe2.firstElementChild;while(s6){if(s6.nodeName==='Signal')g.links.push({src_type:gt2,src_id:gid2,dst_type:NT_OUTPUT,dst_id:parseInt(s6.getAttribute('no'))});s6=s6.nextElementSibling;}}}}for(var ni=0;ni<g.nodes.length;ni++){var nd=g.nodes[ni];if(nd.type!==NT_SR&&nd.type!==NT_RS)continue;var sS=nd.rsS||'',rS=nd.rsR||'';if(!sS&&!rS)continue;for(var lj=0;lj<g.links.length;lj++){var lk=g.links[lj];if(lk.dst_type!==nd.type||(lk.dst_id&0xFF)!==(nd.id&0xFF))continue;var sd='',sN=null;for(var sk=0;sk<g.nodes.length;sk++)if(g.nodes[sk].type===lk.src_type&&(g.nodes[sk].type<=1?g.nodes[sk].id===lk.src_id:(g.nodes[sk].id&0xFF)===(lk.src_id&0xFF))){sN=g.nodes[sk];break;}if(!sN)continue;if(sN.type===NT_INPUT)sd='Input#'+sN.id;else sd='Comb#'+(sN.combId!==undefined?sN.combId:(sN.id&0xFF));if(nd.type===NT_SR){if(sS&&sd===sS)lk.dstPin=0;else if(rS&&sd===rS)lk.dstPin=1;}else{if(sS&&sd===sS)lk.dstPin=1;else if(rS&&sd===rS)lk.dstPin=0;}}}g._source=sourceName;logicGraphs=logicGraphs.filter(function(x){return x._source!==sourceName;});logicGraphs.push(g);}

	/* ================================================================
	 * XML 生成 + 保存
	 * ================================================================ */
	function findLinkPin(srcIdx, dstIdx) { for (var lk = 0; lk < editLinks.length; lk++) { if (editLinks[lk].srcIdx === srcIdx && editLinks[lk].dstIdx === dstIdx) return editLinks[lk].dstPin; } return -1; }

	function generateXmlConfig() {
		var tN = {2:'OR',3:'AND',4:'NOT',5:'T62P',6:'T62D',7:'RISING',8:'SR',9:'RS',10:'FALLING',11:'TOGGLE',50:'MX_ADD',51:'MX_SUB',52:'MX_MAX',53:'MX_MIN',54:'MX_MUL',55:'MX_DIV',56:'MX_ABS',57:'MX_CMP'};
		var aI = {}, aO = {};
		for (var i = 0; i < editNodes.length; i++) { aI[i] = []; aO[i] = []; }
		for (var j = 0; j < editLinks.length; j++) {
			var l = editLinks[j];
			aO[l.srcIdx].push(l.dstIdx);
			aI[l.dstIdx].push(l.srcIdx);
		}
		var vis = {}, chs = [];
		for (var s = 0; s < editNodes.length; s++) {
			if (vis[s]) continue;
			var c = [], q = [s]; vis[s] = true;
			while (q.length > 0) {
				var cur = q.shift(); c.push(cur);
				var nb = aO[cur].concat(aI[cur]);
				for (var nb2 = 0; nb2 < nb.length; nb2++)
					if (!vis[nb[nb2]]) { vis[nb[nb2]] = true; q.push(nb[nb2]); }
			}
			chs.push(c);
		}
		function sx(n, pin) { var s = '<Signal no="' + n.id + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '"'; if (pin !== undefined && pin >= 0) s += ' pin="' + pin + '"'; s += ' />'; return s; }
		function gt(n, ni) {
			var tn = tN[n.type] || '?';
			var a = ' type="' + tn + '" id="' + (n.id & 0xFF) + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '"';
			if (n.inPins && n.inPins > 2) a += ' inPins="' + n.inPins + '"';
			if (n.type === NT_62P || n.type === NT_62D) a += ' delayMs="' + (n.delayMs || 1000) + '"';
			if ((n.type === NT_SR || n.type === NT_RS) && ni !== undefined) {
				var sl = [];
				for (var li = 0; li < editLinks.length; li++)
					if (editLinks[li].dstIdx === ni) sl.push(editLinks[li]);
				var hp = false;
				for (var si2 = 0; si2 < sl.length; si2++)
					if (sl[si2].dstPin !== undefined) { hp = true; break; }
				if (!hp) for (var si3 = 0; si3 < sl.length; si3++) {
					if (n.type === NT_SR) sl[si3].dstPin = si3;
					else sl[si3].dstPin = 1 - si3;
				}
				var dedupSR = {};
				for (var si4 = 0; si4 < sl.length; si4++) {
					var l3 = sl[si4], snl = editNodes[l3.srcIdx];
					var sd2 = '';
					if (snl.type === NT_INPUT) sd2 = 'Input#' + snl.id;
					else sd2 = 'Comb#' + (snl.combId !== undefined ? snl.combId : (snl.id & 0xFF));
					var dk = snl.type + '_' + snl.id + '_' + (l3.dstPin || 0);
					if (!dedupSR[dk]) {
						dedupSR[dk] = true;
						if (n.type === NT_SR) {
							if (l3.dstPin === 0) a += ' S="' + sd2 + '"';
							else a += ' R="' + sd2 + '"';
						} else {
							if (l3.dstPin === 1) a += ' S="' + sd2 + '"';
							else a += ' R="' + sd2 + '"';
						}
					}
				}
			}
			return '<Gate' + a + ' />';
		}
		function ac(idx, ctr, cga) {
			editNodes[idx].combId = undefined;
			for (var ui = 0; ui < aI[idx].length; ui++) {
				var up = aI[idx][ui];
				if (editNodes[up].type > 1 && cga.indexOf(up) >= 0) ac(up, ctr, cga);
			}
			editNodes[idx].combId = ctr.count++;
		}
		function cx(idx, ind) {
			var s2 = '';
			var cid = editNodes[idx].combId;
			s2 += ind + '<Comb id="' + (cid !== undefined ? cid : 0) + '">\n';
			var i2 = ind + '    ';
			var di2 = [];
			for (var fi = 0; fi < aI[idx].length; fi++)
				if (editNodes[aI[idx][fi]].type === NT_INPUT) di2.push(aI[idx][fi]);
			if (di2.length > 0) {
				s2 += i2 + '<Inputs>\n';
				for (var d = 0; d < di2.length; d++) s2 += i2 + '    ' + sx(editNodes[di2[d]]) + '\n';
				s2 += i2 + '</Inputs>\n';
			}
			for (var ui2 = 0; ui2 < aI[idx].length; ui2++)
				if (editNodes[aI[idx][ui2]].type > 1 && cGs.indexOf(aI[idx][ui2]) >= 0)
					s2 += cx(aI[idx][ui2], i2);
			s2 += i2 + gt(editNodes[idx], idx) + '\n';
			var dos = [];
			for (var oi = 0; oi < aO[idx].length; oi++)
				if (editNodes[aO[idx][oi]].type === NT_OUTPUT) dos.push(aO[idx][oi]);
			if (dos.length > 0) {
				s2 += i2 + '<Outputs>\n';
				for (var o = 0; o < dos.length; o++) s2 += i2 + '    ' + sx(editNodes[dos[o]]) + '\n';
				s2 += i2 + '</Outputs>\n';
			}
			s2 += ind + '</Comb>\n';
			return s2;
		}
		var cGs = [];
		var xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
		xml += '<PLCConfig name="' + escXml(editName || 'PLC Logic') + '" key="' + escXml(editKey) + '" checksum="00000000">\n';
		for (var ci = 0; ci < chs.length; ci++) {
			var ch = chs[ci], cis = [], cos = [];
			cGs = [];
			for (var ni = 0; ni < ch.length; ni++) {
				var nd = editNodes[ch[ni]];
				if (nd.type === NT_INPUT) cis.push(ch[ni]);
				else if (nd.type === NT_OUTPUT) cos.push(ch[ni]);
				else cGs.push(ch[ni]);
			}
			if (cGs.length === 0) continue;
			xml += '    <Chain gates="' + cGs.length + '" inputs="' + cis.length + '" outputs="' + cos.length + '">\n';
			if (cGs.length === 1) {
				xml += '        <Inputs>\n';
				for (var ii = 0; ii < cis.length; ii++) xml += '            ' + sx(editNodes[cis[ii]]) + '\n';
				xml += '        </Inputs>\n';
				xml += '        ' + gt(editNodes[cGs[0]], cGs[0]) + '\n';
				xml += '        <Outputs>\n';
				for (var oo = 0; oo < cos.length; oo++) xml += '            ' + sx(editNodes[cos[oo]]) + '\n';
				xml += '        </Outputs>\n';
			} else {
				var chG = [];
				for (var gg = 0; gg < cGs.length; gg++) {
					var ato = true;
					for (var so = 0; so < aO[cGs[gg]].length; so++)
						if (cGs.indexOf(aO[cGs[gg]][so]) >= 0) { ato = false; break; }
					if (ato) chG.push(cGs[gg]);
				}
				var crS = {};
				for (var cg = 0; cg < chG.length; cg++)
					for (var ui3 = 0; ui3 < aI[chG[cg]].length; ui3++) {
						var up = aI[chG[cg]][ui3];
						if (cGs.indexOf(up) >= 0) crS[up] = true;
					}
				var crA = Object.keys(crS).map(Number), crs = [];
				for (var rr = 0; rr < crA.length; rr++) {
					var isD = false;
					for (var rr2 = 0; rr2 < crA.length; rr2++) {
						if (rr === rr2) continue;
						var v2 = {}, q2 = [crA[rr2]]; v2[crA[rr2]] = true;
						while (q2.length > 0) {
							var c2 = q2.shift();
							if (c2 === crA[rr]) { isD = true; break; }
							for (var si5 = 0; si5 < aO[c2].length; si5++) {
								var nx = aO[c2][si5];
								if (!v2[nx] && cGs.indexOf(nx) >= 0) { v2[nx] = true; q2.push(nx); }
							}
						}
						if (isD) break;
					}
					if (!isD) crs.push(crA[rr]);
				}
				var cidC = {count: 0};
				for (var tt = 0; tt < crs.length; tt++) ac(crs[tt], cidC, cGs);
				for (var tt = 0; tt < crs.length; tt++) xml += cx(crs[tt], '        ');
				for (var cg2 = 0; cg2 < chG.length; cg2++) {
					var cgN = editNodes[chG[cg2]], dis = [], seenDis = {};
					for (var cui = 0; cui < aI[chG[cg2]].length; cui++) {
						var ui4 = aI[chG[cg2]][cui];
						if (editNodes[ui4].type === NT_INPUT && !seenDis[ui4]) { seenDis[ui4] = true; dis.push(ui4); }
					}
					if (dis.length > 0) {
						xml += '        <Inputs>\n';
						for (var di3 = 0; di3 < dis.length; di3++) xml += '            ' + sx(editNodes[dis[di3]]) + '\n';
						xml += '        </Inputs>\n';
					}
					xml += '        ' + gt(cgN, chG[cg2]) + '\n';
				}
				var coS = {};
				for (var cg3 = 0; cg3 < chG.length; cg3++)
					for (var oi2 = 0; oi2 < aO[chG[cg3]].length; oi2++) {
						var oi3 = aO[chG[cg3]][oi2];
						if (editNodes[oi3].type === NT_OUTPUT) coS[oi3] = true;
					}
				for (var oo2 = 0; oo2 < cos.length; oo2++) {
					var hgs = false;
					for (var ai = 0; ai < aI[cos[oo2]].length; ai++)
						if (cGs.indexOf(aI[cos[oo2]][ai]) >= 0) hgs = true;
					if (!hgs) coS[cos[oo2]] = true;
				}
				var coL = Object.keys(coS).map(Number);
				if (coL.length > 0) {
					xml += '        <Outputs>\n';
					for (var oo3 = 0; oo3 < coL.length; oo3++) xml += '            ' + sx(editNodes[coL[oo3]]) + '\n';
					xml += '        </Outputs>\n';
				}
			}
			xml += '    </Chain>\n';
		}
		xml += '</PLCConfig>\n';
		return xml;
	}



	function generateAndSaveConfig(){if(editNodes.length===0){showToast('请先添加节点','err');return;}var hi=editNodes.some(function(n){return n.type===NT_INPUT;}),ho=editNodes.some(function(n){return n.type===NT_OUTPUT;});if(!hi||!ho){showToast('至少需要一个输入和输出','err');return;}if(editLinks.length===0){showToast('请先连线','err');return;}var seen={};for(var di=0;di<editNodes.length;di++){var dn=editNodes[di];if(dn.type===NT_INPUT||dn.type===NT_OUTPUT)continue;var k=dn.type+':'+(dn.id&0xFF);if(seen[k]){var nxt=editNextGateIdx[dn.type]||0;editNodes[di].id=(dn.id&~0xFF)|nxt;editNextGateIdx[dn.type]=nxt+1;}else seen[k]=true;}var fc=generateXmlConfig(),pv=fc.length>2000?fc.substring(0,2000)+'…':fc;var h='<div class="modal-overlay" id="plc-modal"><div class="modal-box" style="max-width:700px"><h3>✅ 配置预览</h3><p>Key: <strong>'+escHtml(editKey)+'</strong> | '+escHtml(editName||'PLC Logic')+'</p><p>节点:'+editNodes.length+' | 连线:'+editLinks.length+'</p><div style="max-height:250px;overflow:auto;background:var(--bg-deep);color:#0f0;padding:12px;border-radius:6px;font-family:monospace;font-size:12px;white-space:pre-wrap">'+escHtml(pv)+'</div><div style="margin-top:12px;display:flex;gap:8px"><button class="btn btn-primary" id="plc-modal-save">💾 保存配置</button><button class="btn btn-outline" id="plc-modal-cancel">取消</button></div></div></div>';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-save').onclick=function(){closeModal();var old=loadedFiles['config/PLC/Reclose_logic.xml']||'';if(old&&old.length>10){WsClient.send({type:'set_file',path:'config/PLC/Reclose_logic.xml.bak',content:old});}var snt=WsClient.send({type:'set_file',path:'config/PLC/Reclose_logic.xml',content:fc});if(snt){showToast('已保存！重启PLC生效','ok');loadedFiles['config/PLC/Reclose_logic.xml']=null;loadedFiles['config/PLC/Reclose_logic.xml.bak']=null;logicGraphs=[];}else{showToast('保存失败','err');}};}

	/* ================================================================
	 * 页面生命周期
	 * ================================================================ */
	function onEnter(){if(PlcConfigPage._retryList&&PlcConfigPage._retryList.length>0){var rty=PlcConfigPage._retryList.slice();PlcConfigPage._retryList=[];for(var ri=0;ri<rty.length;ri++)requestFile(rty[ri],PlcConfigPage._pending[rty[ri]]||function(){});}if(dcOutSignals.length===0)loadDcOutSignals();}
	function onLeave(){editWiring=null;editDragNode=null;editSelectedNode=-1;editSelectedLink=-1;}
	function onData(data){if(!data||typeof data!=='object')return;if(data.type==='file_content'){var p=data.path,c=data.content||'',e=data.error||'';loadedFiles[p]=e?null:c;if(PlcConfigPage._pending&&PlcConfigPage._pending[p]){PlcConfigPage._pending[p](e?null:c);delete PlcConfigPage._pending[p];}if(PlcConfigPage._retryList){var rIdx=PlcConfigPage._retryList.indexOf(p);if(rIdx>=0)PlcConfigPage._retryList.splice(rIdx,1);}return;}if(data.type==='file_saved'){if(data.success)showToast('已保存: '+data.path,'ok');else showToast('保存失败: '+(data.error||''),'err');}}

	return {render:render,onEnter:onEnter,onLeave:onLeave,onData:onData,_pending:{},
			_addSigIO:function(no,saddr,desc,ioType){
				if(mode!=='edit')return;
				var type=ioType===1?NT_INPUT:NT_OUTPUT;
				editNodes.push({type:type,id:no,x:0,y:0,_lx:type===NT_INPUT?-260:260,_ly:editNodes.filter(function(n){return n.type===type;}).length*110,saddr:saddr,desc:desc||'',no:no});
				drawEditor();showToast('已添加'+(type===NT_INPUT?'输入':'输出')+': '+saddr,'ok');
			},
			_addOp:function(gt){
				if(mode!=='edit')return;
				var info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0;
				if(gt===NT_62P||gt===NT_62D){addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;drawEditor();return;}
				addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;drawEditor();
			}
		};
})();

Router.register('#plc_config', function(c) { PlcConfigPage.render(c); });
