001package gudusoft.gsqlparser.dlineage.comment;
002
003import gudusoft.gsqlparser.dlineage.comment.CommentBindingBundle.*;
004import gudusoft.gsqlparser.dlineage.dataflow.model.xml.*;
005import java.util.*;
006import java.util.function.UnaryOperator;
007
008/** Centralized, typed model-ID projection. Source occurrence identities never use model IDs. */
009public final class CommentBindingProjection {
010    private CommentBindingProjection() {}
011    private static CommentBindingBundle copy(CommentBindingBundle b) {
012        CommentBindingBundle n = new CommentBindingBundle();
013        n.state=b.state; n.limits=b.limits; n.supportedHostKinds.addAll(b.supportedHostKinds);
014        n.sourceUnits.addAll(b.sourceUnits); n.comments.addAll(b.comments); n.bindings.addAll(b.bindings);
015        n.coverage.addAll(b.coverage); n.diagnostics.addAll(b.diagnostics); return n;
016    }
017    private static String mapped(String id, Map<String,String> map) { return id == null ? null : map.getOrDefault(id,id); }
018    public static void remap(dataflow flow, Map<String,String> objects, Map<String,String> columns, Map<String,String> relations) {
019        if (flow == null || flow.getCommentBindings() == null) return;
020        CommentBindingBundle b=flow.getCommentBindings(), n=copy(b);
021        for (Host h:b.hosts) {
022            List<Target> targets=new ArrayList<Target>();
023            for (Target t:h.targets) targets.add(new Target(t.space,mapped(t.id,
024                    "column".equals(t.space)?columns:"relationship".equals(t.space)?relations:objects),mapped(t.parentId,objects)));
025            n.hosts.add(host(h,targets,mapped(h.processId,objects),mapped(h.procedureId,objects),h.bindingState));
026        }
027        flow.setCommentBindings(n);
028    }
029    public static void offset(dataflow flow, UnaryOperator<String> offset) {
030        if (flow == null || flow.getCommentBindings() == null) return;
031        CommentBindingBundle b=flow.getCommentBindings(), n=copy(b);
032        for (Host h:b.hosts) {
033            List<Target> targets=new ArrayList<Target>();
034            for (Target t:h.targets) targets.add(new Target(t.space,offset.apply(t.id),t.parentId==null?null:offset.apply(t.parentId)));
035            n.hosts.add(host(h,targets,h.processId==null?null:offset.apply(h.processId),h.procedureId==null?null:offset.apply(h.procedureId),h.bindingState));
036        }
037        flow.setCommentBindings(n);
038    }
039    private static Host host(Host h,List<Target> targets,String processId,String procedureId,String state) {
040        return new Host(h.id,h.sourceUnitId,h.kind,h.syntaxKind,h.range,h.nameRange,h.queryBlockId,h.parentHostId,h.ordinal,state,targets,processId,procedureId);
041    }
042    /** Remove dangling projections, preserving the occurrence and its original owner. */
043    public static void retain(dataflow flow) { retain(flow, true); }
044
045    public static void retain(dataflow flow, boolean filtering) {
046        if (flow == null || flow.getCommentBindings() == null) return;
047        Set<String> present=new HashSet<String>();
048        for (table t:flow.getTables()) table(present,t,"table");
049        for (table t:flow.getViews()) table(present,t,"table");
050        for (table t:flow.getResultsets()) table(present,t,"resultset");
051        for (procedure p:flow.getProcedures()) present.add("procedure:"+p.getId());
052        for (oraclePackage p:flow.getPackages()) {
053            present.add("package:"+p.getId());
054            if (p.getProcedures()!=null) for (procedure routine:p.getProcedures()) present.add("procedure:"+routine.getId());
055        }
056        for (process p:flow.getProcesses()) present.add("process:"+p.getId());
057        for (relationship r:flow.getRelationships()) present.add("relationship:"+r.getId());
058        CommentBindingBundle b=flow.getCommentBindings(), n=copy(b);
059        Set<String> filtered=new HashSet<String>();
060        for (Host h:b.hosts) {
061            List<Target> targets=new ArrayList<Target>();
062            for (Target t:h.targets) if (present.contains(key(t))) targets.add(t);
063            boolean removed=targets.size()!=h.targets.size();
064            if (removed) {
065                if (filtering) filtered.add(h.sourceUnitId+":"+h.kind);
066                n.diagnostics.add(new Diagnostic("MODEL_NOT_EMITTED",h.sourceUnitId,null,h.id));
067            }
068            n.hosts.add(host(h,targets,present.contains("process:"+h.processId)?h.processId:null,
069                    present.contains("procedure:"+h.procedureId)?h.procedureId:null,
070                    removed && targets.isEmpty()?(filtering?"filtered":"source_only"):h.bindingState));
071        }
072        if (!filtered.isEmpty()) {
073            n.coverage.clear();
074            for (Coverage c:b.coverage) n.coverage.add(new Coverage(c.sourceUnitId,c.hostKind,
075                    filtered.contains(c.sourceUnitId+":"+c.hostKind)&&"complete".equals(c.state)?"filtered":c.state));
076            if ("complete".equals(n.state)) n.state="partial";
077        }
078        flow.setCommentBindings(n);
079    }
080    private static String key(Target t) { return t.space+":"+t.id+("column".equals(t.space)?":"+t.parentId:""); }
081    private static void table(Set<String> ids,table t,String space) {
082        ids.add(space+":"+t.getId());
083        if (t.getColumns()!=null) for (column c:t.getColumns()) ids.add("column:"+c.getId()+":"+t.getId());
084    }
085    /** Preserve imported source facts when a legacy append has no unambiguous model-ID map. */
086    public static CommentBindingBundle sourceOnly(CommentBindingBundle b) {
087        CommentBindingBundle n = copy(b);
088        n.state = "failed".equals(b.state) ? "failed" : "partial";
089        n.coverage.clear();
090        for (Coverage c : b.coverage) n.coverage.add(new Coverage(c.sourceUnitId, c.hostKind, "unsupported"));
091        for (Host h : b.hosts) n.hosts.add(host(h, Collections.<Target>emptyList(), null, null, "source_only"));
092        n.diagnostics.add(new Diagnostic("MODEL_MAPPING_UNAVAILABLE", null, null, null));
093        return n;
094    }
095    /** Merge already-remapped documents, giving each document its own source namespace. */
096    public static CommentBindingBundle merge(List<CommentBindingBundle> bundles) {
097        CommentBindingBundle n=null;
098        for (CommentBindingBundle b:bundles) {
099            if (b==null) continue;
100            if (n==null) { n=new CommentBindingBundle(); n.limits=b.limits; n.supportedHostKinds.addAll(b.supportedHostKinds); }
101            Map<String,String> sources = new HashMap<String,String>();
102            Map<String,String> comments = new HashMap<String,String>();
103            Map<String,String> hosts = new HashMap<String,String>();
104            if ("failed".equals(b.state)) n.state="failed";
105            else if (!"complete".equals(b.state) && !"failed".equals(n.state)) n.state="partial";
106            for (SourceUnit unit:b.sourceUnits) {
107                String id="s"+(n.sourceUnits.size()+1); sources.put(unit.id,id);
108                n.sourceUnits.add(new SourceUnit(id,n.sourceUnits.size(),unit.vendor,unit.contentDigest,unit.originKind,unit.fileName));
109            }
110            int nextComment = n.comments.size() + 1, nextHost = n.hosts.size() + 1;
111            for (Comment c:b.comments) comments.put(c.id,sources.get(c.sourceUnitId)+":c"+(nextComment++));
112            for (Host h:b.hosts) hosts.put(h.id,h.id.equals(h.sourceUnitId)?sources.get(h.sourceUnitId):sources.get(h.sourceUnitId)+":h"+(nextHost++));
113            for (Comment c:b.comments) n.comments.add(new Comment(comments.get(c.id),sources.get(c.sourceUnitId),c.range,c.lexicalKind,c.raw,c.rawEncoding,c.originalLength,c.rawTruncated));
114            for (Host h:b.hosts) n.hosts.add(new Host(hosts.get(h.id),sources.get(h.sourceUnitId),h.kind,h.syntaxKind,h.range,h.nameRange,
115                    hosts.get(h.queryBlockId),hosts.get(h.parentHostId),h.ordinal,h.bindingState,h.targets,h.processId,h.procedureId));
116            for (Binding x:b.bindings) n.bindings.add(new Binding(comments.get(x.commentId),hosts.get(x.hostId),x.placement,x.ruleId));
117            for (Coverage c:b.coverage) n.coverage.add(new Coverage(sources.get(c.sourceUnitId),c.hostKind,c.state));
118            for (Diagnostic d:b.diagnostics) n.diagnostics.add(new Diagnostic(d.code,sources.get(d.sourceUnitId),comments.get(d.commentId),hosts.get(d.hostId)));
119        }
120        if (n != null) limitMergedRaw(n);
121        return n;
122    }
123    private static void limitMergedRaw(CommentBindingBundle bundle) {
124        if (bundle.limits == null) return;
125        int remaining = bundle.limits.maxTotalRawLength;
126        Set<String> limited = new HashSet<String>();
127        for (int i=0;i<bundle.comments.size();i++) {
128            Comment c=bundle.comments.get(i);
129            String raw=c.raw == null ? "" : c.raw;
130            boolean encoded="base64-utf8".equals(c.rawEncoding);
131            if (encoded) raw=new String(Base64.getDecoder().decode(raw),java.nio.charset.StandardCharsets.UTF_8);
132            int keep=Math.min(raw.length(),Math.min(bundle.limits.maxRawLength,remaining));
133            if (keep<raw.length() && keep>0 && Character.isHighSurrogate(raw.charAt(keep-1))) keep--;
134            remaining-=keep;
135            if (keep<raw.length()) {
136                limited.add(c.sourceUnitId);
137                raw=raw.substring(0,keep);
138                if (encoded) raw=Base64.getEncoder().encodeToString(raw.getBytes(java.nio.charset.StandardCharsets.UTF_8));
139                bundle.comments.set(i,new Comment(c.id,c.sourceUnitId,c.range,c.lexicalKind,raw,c.rawEncoding,c.originalLength,true));
140            }
141        }
142        if (!limited.isEmpty()) {
143            bundle.state="partial";
144            for (String source:limited) bundle.diagnostics.add(new Diagnostic("LIMIT_REACHED",source,null,null));
145            for (int i=0;i<bundle.coverage.size();i++) {
146                Coverage c=bundle.coverage.get(i);
147                if (limited.contains(c.sourceUnitId)) bundle.coverage.set(i,new Coverage(c.sourceUnitId,c.hostKind,"limited"));
148            }
149        }
150    }
151
152}