1 module tagion.tools.graphview;
2 import std.algorithm : min;
3 import std.array : join;
4 import std.conv : to;
5 import std.file : exists, fwrite = write;
6 import std.format;
7 import std.functional : toDelegate;
8 import std.getopt;
9 import std.outbuffer;
10 import std.path : extension;
11 import std.traits : EnumMembers;
12 import std.traits : isIntegral;
13 import tagion.basic.basic : EnumText;
14 import tagion.hashgraphview.EventView : EventView;
15 import tagion.hibon.Document : Document;
16 import tagion.hibon.HiBONJSON;
17 import tagion.hibon.HiBONValid : error_callback;
18 import tagion.hibon.HiBONFile : fread;
19 import tagion.tools.revision;
20 import tagion.utils.BitMask;
21 
22 protected enum _params = [
23     "events",
24     "size",
25 ];
26 
27 mixin(EnumText!("Params", _params));
28 
29 //
30 
31 enum pastel19 = [
32     "#fbb4ae",
33     "#b3cde3",
34     "#ccebc5",
35     "#decde4",
36     "#fed9a6",
37     "#ffffcc",
38     "#e5d8bd",
39     "#fddaec",
40     "#f2f2f2"
41 ];
42 
43 string color(T)(string[] colors, T index) if (isIntegral!T) {
44     import std.math : abs;
45 
46     const i = abs(index) % colors.length;
47     return colors[i];
48 }
49 
50 enum fileextensions {
51     HIBON = ".hibon" //    JSON  = ".json"
52 }
53 
54 enum dot_fileextension {
55     CMAPX = "cmapx", /// Produces HTML map files for client-side image maps.
56     PDF = "pdf", /++ Adobe PDF via the Cairo library. We have seen problems when embedding
57                        into, other documents. Instead, use -Tps2 as described below. +/
58     PLAIN = "txt", /++ Simple, line-based ASCII format. Appendix E describes this output. An
59                      alternate format is plain-ext, which provides port names on the head and
60                      tail nodes of edges. +/
61     PNG = "png", /// PNG (Portable Network Graphics) output.
62     PS = "ps", /// PostScript (EPSF) output.
63     PS2 = "ps2", /++
64                    PostScript (EPSF) output with PDF annotations.
65                    This output should be distilled into PDF,
66                    such as for pdflatex, before being included in a document.
67                    (Use ps2pdf; epstopdf doesn’t handle %%BoundingBox: (atend).)
68                    +/
69     SVG = "svg", /// SVG output. The alternate form svgz produces compressed SVG.
70     VRML = "vrml", /// VRML output.
71     WBMP = "wbmp", ///Wireless BitMap (WBMP) format.
72 }
73 
74 // dot -Tsvg test.dot -o test1.svg
75 
76 struct Dot {
77     import std.format;
78 
79     static string INDENT;
80     static this() {
81         INDENT = "  ";
82     }
83     // OutputBuf obuf;
84     string name;
85     const size_t node_size;
86     //    string indet;
87     EventView[uint] events;
88     // this(string name, string indent="  ") {
89     //     this.obuf = obuf;
90     //     this.name = name;
91     //     this.indent=indent;
92     // }
93 
94     this(const Document doc, string name) {
95         node_size = doc[Params.size].get!ulong;
96         const events_doc = doc[Params.events].get!Document;
97         foreach (e; events_doc[]) {
98             auto event = EventView(e.get!Document);
99             events[event.id] = event;
100         }
101         this.name = name;
102     }
103 
104     private void edge(ref OutBuffer obuf, const(string) indent, const bool father_flag, ref const EventView e) const {
105         string witness_text;
106         void local_edge(string[] texts, const uint end_id, string[] options = null) {
107             string[] edge_params;
108             edge_params ~= format(`label="%s"`, texts.join("\n"));
109             edge_params ~= options;
110             obuf.writefln(`%s%s -> %s [%s];`, indent, e.id, end_id,
111                     edge_params.join(", "));
112         }
113 
114         string mask2text(const(uint[]) mask) {
115             const mask_text = format("%s", BitMask(mask));
116             return mask_text[0 .. min(node_size, mask_text.length)];
117         }
118 
119         if (father_flag && e.father !is e.father.init && e.father in events) {
120             const father_event = events[e.father];
121             string[] texts;
122             // texts ~= mask2text(father_event.witness_mask);
123             local_edge(texts, e.father);
124         }
125         else if (e.mother !is e.mother.init && e.mother in events) {
126             const mother_event = events[e.mother];
127             string[] texts;
128             string[] options;
129             // const witness_mask_text=mask2text(mother_event.witness_mask);
130             //texts~=mask2text(mother_event.witness_mask);
131             const round_received_mask = mask2text(mother_event.round_received_mask);
132             if (round_received_mask) {
133                 texts ~= format("%s:e", round_received_mask);
134             }
135             if (mother_event.witness) {
136                 options ~= format(`color="%s"`, "red");
137                 options ~= format(`fontcolor="%s"`, "blue");
138                 options ~= format(`shape="%s"`, "plane");
139 
140                 // const strongly_seening_mask = mask2text(
141                 //     // mother_event.strongly_seeing_mask);
142                 // if (strongly_seening_mask) {
143                 //     texts ~= format("%s:s", strongly_seening_mask);
144                 // }
145                 // const round_seen_mask = mask2text(mother_event.round_seen_mask);
146                 // if (round_seen_mask) {
147                 // texts ~= format("%s:r", round_seen_mask); //mask2text(mother_event.round_seen_mask));
148                 // }
149             }
150             // texts ~= format("%s:w", witness_mask_text);
151 
152             //     //       texts~=mask2text(mother_event.strongly_seeing_mask);
153             // }
154             // else {
155             //     texts~=witness_mask_text;
156             // }
157             local_edge(texts, e.mother, options);
158         }
159     }
160 
161     private void node(ref OutBuffer obuf, const(string) indent, ref const EventView e) const {
162         if (e.father !is e.father.init) {
163             //obuf.writefln("%s%s -> %s;", indent~INDENT, e.id, e.father);
164             edge(obuf, indent ~ INDENT, true, e);
165 
166         }
167         obuf.writefln(`%s%s [pos="%s, %s!"];`, indent ~ INDENT, e.id, e.node_id * 2, e.order);
168 
169         if (e.witness) {
170             const color = (e.famous) ? "red" : "lightgreen";
171             obuf.writefln(`%s%s [fillcolor="%s"];`, indent ~ INDENT, e.id, color);
172         }
173         else {
174             obuf.writefln(`%s%s [fillcolor="%s"];`, indent ~ INDENT, e.id, pastel19.color(e.round_received));
175         }
176         if (e.error) {
177             obuf.writefln(`%s%s [shape="%s"];`, indent ~ INDENT, e.id, "star");
178         }
179         else if (e.father_less) {
180             obuf.writefln(`%s%s [shape="%s"];`, indent ~ INDENT, e.id, "egg");
181         }
182         string round_text = (e.round is int.min) ? "\u2693" : e.round.to!string;
183         if (e.round_received !is int.min) {
184             round_text ~= format(":%s", e.round_received);
185         }
186         // if (e.erased) {
187         //     obuf.writefln(`%s%s [fontcolor="%s"];`, indent~INDENT, e.id, "yellow");
188         // }
189         obuf.writefln(`%s%s [xlabel="%s"];`, indent ~ INDENT, e.id, round_text);
190     }
191 
192     void draw(ref OutBuffer obuf, const(string) indent = null) {
193         import stdio = std.stdio;
194 
195         obuf.writefln("%sdigraph %s {", indent, name);
196         //        obuf.writefln(`%snode [margin=0.1 fontcolor=blue fixedsize=true fontsize=32 width=1.2 shape=ellipse rankdir=TB style=filled splines="line"]`, indent);
197         obuf.writefln(`%ssplines=line; pin=true;`, indent);
198         //obuf.writefln(`%sgraph  [ranksep="1", nodesep="2"]`, indent);
199         obuf.writefln(`%snode [margin=0.1 fontcolor=blue fixedsize=true fontsize=24 width=1.2 shape=ellipse rankdir=TB style=filled splines=true];`, indent);
200         // edge [lblstyle="above, sloped"];
201         //obuf.writefln(`%sedge [lblstyle="above, sloped"];`, indent);
202         scope (exit) {
203             obuf.writefln("%s}", indent);
204         }
205         void subgraphs(const(string) indent) {
206             OutBuffer[size_t] subbuf;
207             scope (exit) {
208                 foreach (buf; subbuf) {
209                     buf.writefln("%s}", indent);
210                     obuf.writefln("%s", buf);
211 
212                 }
213             }
214             const sub_indent = indent ~ INDENT;
215             foreach (e; events) {
216                 OutBuffer subgraph_header(ref const EventView e) {
217                     auto sbuf = new OutBuffer;
218                     sbuf.writefln("%ssubgraph node_%d { peripheries=0", indent, e.node_id);
219                     return sbuf;
220                 }
221                 //                stdio.writefln("%s %d", e.id, e.mother);
222                 auto sub_obuf = subbuf.require(e.node_id, subgraph_header(e));
223                 if (e.mother !is e.mother.init) {
224                     //sub_obuf.writefln("%s%s -> %s;", sub_indent, e.id, e.mother);
225                     edge(sub_obuf, indent ~ INDENT, false, e);
226                 }
227             }
228         }
229 
230         subgraphs(indent ~ INDENT);
231         foreach (e; events) {
232             node(obuf, indent ~ INDENT, e);
233             // if (e.father !is e.father.init) {
234             //     //obuf.writefln("%s%s -> %s;", indent~INDENT, e.id, e.father);
235             //     edge(obuf, indent~INDENT, true, e);
236 
237             // }
238             // obuf.writefln(`%s%s [pos="%s, %s!"];`, indent~INDENT, e.id, e.node_id*2, e.order);
239             // if (e.witness) {
240             //     obuf.writefln(`%s%s [fillcolor="%s"];`, indent~INDENT, e.id, "red");
241             // }
242             // if (e.father_less) {
243             //     obuf.writefln(`%s%s [shape="%s"];`, indent~INDENT, e.id, "egg");
244             // }
245             // if (e.witness_mask.length) {
246             //     BitArray witness_mask;
247             //     obuf.writefln(`%s%s [fillcolor="%s"];`, indent~INDENT, e.id, "red");
248 
249             // }
250         }
251     }
252 }
253 
254 import tagion.tools.Basic;
255 
256 mixin Main!_main;
257 
258 int _main(string[] args) {
259     import std.stdio;
260 
261     immutable program = args[0];
262 
263     bool version_switch;
264 
265     string inputfilename;
266     string outputfilename;
267 
268     auto logo = import("logo.txt");
269     auto main_args = getopt(args,
270             std.getopt.config.caseSensitive,
271             std.getopt.config.bundling,
272             "version", "display the version", &version_switch,
273             "inputfile|i", "Sets the HiBON input file name", &inputfilename, // "outputfile|o", "Sets the output file name",  &outputfilename,
274             // "bin|b", "Use HiBON or else use JSON", &binary,
275             // "value|V", format("Bill value : default: %d", value), &value,
276             // "pretty|p", format("JSON Pretty print: Default: %s", pretty), &pretty,
277             //        "passphrase|P", format("Passphrase of the keypair : default: %s", passphrase), &passphrase
278 
279             
280 
281     );
282 
283     if (version_switch) {
284         revision_text.writeln;
285         return 0;
286     }
287 
288     if (main_args.helpWanted) {
289         defaultGetoptPrinter(
290                 [
291             "Documentation: https://tagion.org/",
292             "",
293             "",
294             "Example:",
295             "graphview Alice.hibon | neato -Tsvg -o outputfile.svg",
296             "Usage:",
297             // format("%s [<option>...] <in-file> <out-file>", program),
298             format("%s [<option>...] <in-file>", program),
299             "",
300             "Where:",
301             "<in-file>           Is an input file in .hibon format",
302             // "<out-file>          Is an output file in .json or .hibon format",
303             // "                    stdout is used of the output is not specifed the",
304             "",
305 
306             "<option>:",
307         ].join("\n"),
308                 main_args.options);
309         return 0;
310     }
311 
312     if (args.length > 1) {
313         inputfilename = args[1];
314     }
315     else {
316         stderr.writefln("Input file missing");
317         return 1;
318     }
319 
320     const input_extension = inputfilename.extension;
321     switch (input_extension) {
322     case fileextensions.HIBON:
323         Document doc = fread(inputfilename);
324         const error_code = doc.valid(toDelegate(&error_callback));
325         if (error_code !is Document.Element.ErrorCode.NONE) {
326             writeln("Document format error");
327             writefln("For the file %s", inputfilename);
328             return 1;
329         }
330         auto dot = Dot(doc, "G");
331         auto obuf = new OutBuffer;
332         dot.draw(obuf);
333         writefln("%s", obuf);
334         break;
335     default:
336         stderr.writefln("File extensions %s not valid (only %s)",
337                 input_extension, [EnumMembers!fileextensions]);
338     }
339 
340     return 0;
341 }