1use std::fmt::Write;
7use std::io::{Result, Write as IoWrite};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use super::config::VisualizerConfig;
11use super::render::{
12 HydroWriteConfig, render_hydro_ir_dot, render_hydro_ir_json, render_hydro_ir_mermaid,
13};
14use crate::compile::ir::HydroRoot;
15
16pub fn open_mermaid(roots: &[HydroRoot], config: Option<HydroWriteConfig>) -> Result<()> {
18 let mermaid_src = render_with_config(roots, config, render_hydro_ir_mermaid);
19 open_mermaid_browser(&mermaid_src)
20}
21
22pub fn open_dot(roots: &[HydroRoot], config: Option<HydroWriteConfig>) -> Result<()> {
24 let dot_src = render_with_config(roots, config, render_hydro_ir_dot);
25 open_dot_browser(&dot_src)
26}
27
28pub fn save_mermaid(
31 roots: &[HydroRoot],
32 filename: Option<&str>,
33 config: Option<HydroWriteConfig>,
34) -> Result<std::path::PathBuf> {
35 let content = render_with_config(roots, config, render_hydro_ir_mermaid);
36 save_to_file(content, filename, "hydro_graph.mermaid", "Mermaid diagram")
37}
38
39pub fn save_dot(
42 roots: &[HydroRoot],
43 filename: Option<&str>,
44 config: Option<HydroWriteConfig>,
45) -> Result<std::path::PathBuf> {
46 let content = render_with_config(roots, config, render_hydro_ir_dot);
47 save_to_file(content, filename, "hydro_graph.dot", "DOT/Graphviz file")
48}
49
50fn open_mermaid_browser(mermaid_src: &str) -> Result<()> {
51 let state = serde_json::json!({
52 "code": mermaid_src,
53 "mermaid": serde_json::json!({
54 "theme": "default"
55 }),
56 "autoSync": true,
57 "updateDiagram": true
58 });
59 let state_json = serde_json::to_vec(&state)?;
60 let state_base64 = data_encoding::BASE64URL.encode(&state_json);
61 webbrowser::open(&format!(
62 "https://mermaid.live/edit#base64:{}",
63 state_base64
64 ))
65}
66
67fn open_dot_browser(dot_src: &str) -> Result<()> {
68 let mut url = "https://dreampuf.github.io/GraphvizOnline/#".to_owned();
69 for byte in dot_src.bytes() {
70 write!(url, "%{:02x}", byte).unwrap();
72 }
73 webbrowser::open(&url)
74}
75
76fn save_to_file(
79 content: String,
80 filename: Option<&str>,
81 default_name: &str,
82 content_type: &str,
83) -> Result<std::path::PathBuf> {
84 let file_path = if let Some(filename) = filename {
85 std::path::PathBuf::from(filename)
86 } else {
87 std::env::temp_dir().join(default_name)
88 };
89
90 std::fs::write(&file_path, content)?;
91 println!("Saved {} to {}", content_type, file_path.display());
92 Ok(file_path)
93}
94
95fn render_with_config<F>(
97 roots: &[HydroRoot],
98 config: Option<HydroWriteConfig>,
99 renderer: F,
100) -> String
101where
102 F: Fn(&[HydroRoot], HydroWriteConfig<'_>) -> String,
103{
104 let config = config.unwrap_or_default();
105 renderer(roots, config)
106}
107
108fn compress_json(json_content: &str) -> Result<Vec<u8>> {
111 use flate2::Compression;
112 use flate2::write::GzEncoder;
113
114 let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
115 encoder.write_all(json_content.as_bytes())?;
116 encoder.finish()
117}
118
119fn encode_base64_url_safe(data: &[u8]) -> String {
122 data_encoding::BASE64URL_NOPAD.encode(data)
123}
124
125fn try_compress_and_encode(json_content: &str, config: &VisualizerConfig) -> (String, bool, f64) {
131 let original_size = json_content.len();
132
133 if !config.enable_compression || original_size < config.min_compression_size {
135 let encoded = encode_base64_url_safe(json_content.as_bytes());
136 return (encoded, false, 1.0);
137 }
138
139 match compress_json(json_content) {
140 Ok(compressed) => {
141 let compressed_size = compressed.len();
142 let ratio = original_size as f64 / compressed_size as f64;
143
144 if compressed_size < original_size {
146 let encoded = encode_base64_url_safe(&compressed);
147 (encoded, true, ratio)
148 } else {
149 let encoded = encode_base64_url_safe(json_content.as_bytes());
151 (encoded, false, 1.0)
152 }
153 }
154 Err(e) => {
155 println!("ā ļø Compression failed: {}, using uncompressed", e);
157 let encoded = encode_base64_url_safe(json_content.as_bytes());
158 (encoded, false, 1.0)
159 }
160 }
161}
162
163fn calculate_url_length(base_url: &str, param_name: &str, encoded_data: &str) -> usize {
166 base_url.len() + 1 + param_name.len() + 1 + encoded_data.len()
168}
169
170fn generate_visualizer_url(
174 json_content: &str,
175 config: &VisualizerConfig,
176) -> Option<(String, bool)> {
177 let (encoded_data, is_compressed, _ratio) = try_compress_and_encode(json_content, config);
178
179 let param_name = if is_compressed { "compressed" } else { "data" };
181
182 let url_length = calculate_url_length(&config.base_url, param_name, &encoded_data);
183
184 if url_length <= config.max_url_length {
185 let url = format!("{}?{}={}", config.base_url, param_name, encoded_data);
186 Some((url, is_compressed))
187 } else {
188 None
190 }
191}
192
193fn generate_timestamped_filename() -> String {
196 let timestamp = SystemTime::now()
197 .duration_since(UNIX_EPOCH)
198 .expect("System clock is before Unix epoch - clock may be corrupted")
199 .as_secs();
200 format!("hydro_graph_{}.json", timestamp)
201}
202
203fn save_json_to_temp_file(json_content: &str) -> Result<std::path::PathBuf> {
208 let filename = generate_timestamped_filename();
209 let temp_file = std::env::temp_dir().join(filename);
210
211 std::fs::write(&temp_file, json_content)?;
212
213 println!("š Saved graph to temporary file: {}", temp_file.display());
214
215 Ok(temp_file)
216}
217
218fn url_encode_file_path(file_path: &std::path::Path) -> String {
223 let path_str = file_path.to_string_lossy();
224 urlencoding::encode(&path_str).to_string()
225}
226
227fn generate_file_based_url(file_path: &std::path::Path, config: &VisualizerConfig) -> String {
232 let encoded_path = url_encode_file_path(file_path);
233 format!("{}?file={}", config.base_url, encoded_path)
234}
235
236fn print_fallback_instructions(file_path: &std::path::Path, url: &str) {
241 println!("\nš Graph Visualization Instructions");
242 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
243 println!("The graph is too large to embed in a URL.");
244 println!("It has been saved to a temporary file:");
245 println!(" š {}", file_path.display());
246 println!();
247 println!("Opening visualizer in browser...");
248 println!(" š {}", url);
249 println!();
250 println!("If the browser doesn't open automatically, you can:");
251 println!(" 1. Manually open: {}", url);
252 println!(
253 " 2. Or visit {} and drag-and-drop the file",
254 url.split('?').next().unwrap_or(url)
255 );
256 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
257}
258
259fn handle_large_graph_fallback(json_content: &str, config: &VisualizerConfig) -> Result<()> {
263 let temp_file = save_json_to_temp_file(json_content)?;
264
265 let url = generate_file_based_url(&temp_file, config);
267
268 print_fallback_instructions(&temp_file, &url);
269
270 match webbrowser::open(&url) {
271 Ok(_) => {
272 println!("ā Successfully opened visualizer in browser");
273 }
274 Err(e) => {
275 println!("ā ļø Failed to open browser automatically: {}", e);
276 println!("Please manually open the URL above or drag-and-drop the file.");
277 }
278 }
279
280 Ok(())
281}
282
283fn open_json_visualizer_with_fallback(json_content: &str, config: &VisualizerConfig) -> Result<()> {
291 match generate_visualizer_url(json_content, config) {
293 Some((url, _is_compressed)) => {
294 webbrowser::open(&url)?;
296 println!("ā Successfully opened visualizer in browser");
297 Ok(())
298 }
299 None => {
300 println!("š¦ Graph too large for URL embedding, using file-based approach...");
302 handle_large_graph_fallback(json_content, config)
303 }
304 }
305}
306
307pub fn open_json_visualizer(
314 roots: &[HydroRoot],
315 config: Option<HydroWriteConfig<'_>>,
316) -> Result<()> {
317 let json_content = render_with_config(roots, config, render_hydro_ir_json);
318 let viz_config = VisualizerConfig::default();
319 open_json_visualizer_with_fallback(&json_content, &viz_config)
320}
321
322pub fn open_json_visualizer_with_config(
325 roots: &[HydroRoot],
326 config: Option<HydroWriteConfig<'_>>,
327 viz_config: VisualizerConfig,
328) -> Result<()> {
329 let json_content = render_with_config(roots, config, render_hydro_ir_json);
330 open_json_visualizer_with_fallback(&json_content, &viz_config)
331}
332
333pub fn save_json(
336 roots: &[HydroRoot],
337 filename: Option<&str>,
338 config: Option<HydroWriteConfig<'_>>,
339) -> Result<std::path::PathBuf> {
340 let content = render_with_config(roots, config, render_hydro_ir_json);
341 save_to_file(content, filename, "hydro_graph.json", "JSON file")
342}