use colored::Colorize;
use error_stack::{Report, ResultExt};
use std::collections::{HashMap, HashSet};
use crate::batbelt::evm::metadata::bat_metadata::{AutoDeployedFrame, EvmBatMetadata};
use crate::batbelt::evm::miro::auto_deploy::{save_frame_record, BOARD_UNITS_PER_PIXEL, PATH_HEADER_LINES};
use crate::batbelt::evm::miro::EvmMiroError;
use crate::batbelt::evm::types::EvmFileItemKind;
use crate::batbelt::miro::client::{ArrowEnd, ConnectorStyle, MiroClient, RelativeAnchor};
use crate::batbelt::miro::layout::{layout_graph, LayoutConfig, LayoutEdge, LayoutNode};
use crate::batbelt::silicon;
type Result<T> = error_stack::Result<T, EvmMiroError>;
const STRUCT_COLOR: &str = "#a259ff";
const CARD_WIDTH: f64 = 900.0;
const CARD_HEIGHT: f64 = 240.0;
pub struct TypeNode {
pub label: String,
pub file_path: String,
pub start: usize,
pub end: usize,
pub png_path: String,
pub png_width: u32,
pub png_height: u32,
}
pub struct TypeEdge {
pub from: usize,
pub to: usize,
pub line_in_slice: usize,
}
pub fn resolve_tree(
metadata: &EvmBatMetadata,
label: &str,
file_path: &str,
start: usize,
end: usize,
) -> Option<(Vec<TypeNode>, Vec<TypeEdge>)> {
let mut nodes: Vec<TypeNode> = vec![TypeNode {
label: label.to_string(),
file_path: file_path.to_string(),
start,
end,
png_path: String::new(),
png_width: 0,
png_height: 0,
}];
let mut edges: Vec<TypeEdge> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
seen.insert(label.to_string());
let mut queue = vec![0usize];
while let Some(index) = queue.pop() {
let (path, from, to) = {
let node = &nodes[index];
(node.file_path.clone(), node.start, node.end)
};
let lines = crate::batbelt::evm::miro::auto_deploy::read_slice(&path, from, to);
for (offset, line) in lines.iter().enumerate() {
let Some(type_name) = field_type(line) else {
continue;
};
let Some(item) = declaration_of(metadata, &type_name, &path) else {
continue;
};
let child_label = format!("{type_name}");
let target = match nodes.iter().position(|node| node.label == child_label) {
Some(existing) => existing,
None => {
nodes.push(TypeNode {
label: child_label.clone(),
file_path: item.0.clone(),
start: item.1,
end: item.2,
png_path: String::new(),
png_width: 0,
png_height: 0,
});
if seen.insert(child_label) {
queue.push(nodes.len() - 1);
}
nodes.len() - 1
}
};
edges.push(TypeEdge {
from: index,
to: target,
line_in_slice: offset + 1,
});
}
}
if edges.is_empty() {
return None;
}
Some((nodes, edges))
}
fn field_type(line: &str) -> Option<String> {
let code = line.split("//").next().unwrap_or(line).trim();
if code.is_empty() || code.starts_with("struct") || code.starts_with('}') {
return None;
}
let code = match code.split_once("=>") {
Some((_, value)) => value.trim_start(),
None => code,
};
let first: String = code
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if first.is_empty() {
return None;
}
const VALUE_TYPES: &[&str] = &[
"uint", "int", "bool", "address", "bytes", "string", "mapping", "struct", "enum",
];
if VALUE_TYPES.iter().any(|value| first.starts_with(value)) {
return None;
}
Some(first)
}
fn declaration_of(
metadata: &EvmBatMetadata,
name: &str,
from_file: &str,
) -> Option<(String, usize, usize)> {
let candidates: Vec<_> = metadata
.file_items
.iter()
.filter(|item| {
item.name == name
&& matches!(item.kind, EvmFileItemKind::Struct | EvmFileItemKind::Enum)
})
.collect();
let chosen = candidates
.iter()
.find(|item| item.file_path == from_file)
.or_else(|| candidates.first())?;
Some((chosen.file_path.clone(), chosen.line, chosen.end_line))
}
pub async fn draw(
client: &MiroClient,
host: &AutoDeployedFrame,
nodes: Vec<TypeNode>,
edges: Vec<TypeEdge>,
font_size: usize,
) -> Result<String> {
let root_label = nodes[0].label.clone();
let layout_nodes: Vec<LayoutNode> = nodes
.iter()
.map(|node| LayoutNode {
id: node.label.clone(),
width: node.png_width as f64 * BOARD_UNITS_PER_PIXEL,
height: node.png_height as f64 * BOARD_UNITS_PER_PIXEL,
})
.collect();
let geometry = silicon::line_geometry(Some(font_size));
let layout_edges: Vec<LayoutEdge> = edges
.iter()
.map(|edge| LayoutEdge {
from: nodes[edge.from].label.clone(),
to: nodes[edge.to].label.clone(),
from_line_fraction: geometry.line_center_fraction(
edge.line_in_slice + PATH_HEADER_LINES - 1,
nodes[edge.from].png_height,
),
})
.collect();
let layout = layout_graph(&root_label, &layout_nodes, &layout_edges, LayoutConfig::default());
let (x, y) = free_rect(host, layout.frame_width, layout.frame_height)?;
let frame_id = client
.create_frame(
&format!("auto: {root_label}"),
x,
y,
layout.frame_width,
layout.frame_height,
None,
)
.await
.change_context(EvmMiroError)?;
let frame_url = client.frame_url(&frame_id);
let mut image_ids: HashMap<String, String> = HashMap::new();
for node in &nodes {
let Some(placed) = layout.node(&node.label) else {
continue;
};
let id = client
.create_image_in_frame(
&node.png_path,
&frame_id,
&node.label,
placed.x,
placed.y,
placed.width,
)
.await
.change_context(EvmMiroError)?;
image_ids.insert(node.label.clone(), id);
}
let mut connector_ids = Vec::new();
for (edge, layout_edge) in edges.iter().zip(layout_edges.iter()) {
let (Some(from), Some(to)) = (
image_ids.get(&nodes[edge.from].label),
image_ids.get(&nodes[edge.to].label),
) else {
continue;
};
let id = client
.create_connector(
from,
RelativeAnchor::new(1.0, layout_edge.from_line_fraction),
to,
RelativeAnchor::new(0.0, 0.5),
ConnectorStyle {
stroke_color: STRUCT_COLOR.to_string(),
stroke_width: "8".to_string(),
dashed: false,
caption: None,
arrow: ArrowEnd::End,
},
)
.await
.change_context(EvmMiroError)?;
connector_ids.push(id);
}
let record = AutoDeployedFrame {
entry_point: root_label.clone(),
frame_id: frame_id.clone(),
frame_url: frame_url.clone(),
x,
y,
width: layout.frame_width,
height: layout.frame_height,
images: nodes
.iter()
.filter_map(|node| image_ids.get(&node.label).map(|id| (node.label.clone(), id.clone())))
.collect(),
image_dims: nodes
.iter()
.map(|node| (node.label.clone(), node.png_width, node.png_height))
.collect(),
node_positions: nodes
.iter()
.filter_map(|node| layout.node(&node.label).map(|p| (node.label.clone(), p.x, p.y)))
.collect(),
callee_connectors: Vec::new(),
link_cards: Vec::new(),
connector_ids,
marker_ids: Vec::new(),
border_ids: Vec::new(),
screenshots: Vec::new(),
cluster_root: host.cluster_root.clone(),
};
save_frame_record(&record)?;
Ok(frame_url)
}
pub async fn place_card(
client: &MiroClient,
host: &mut AutoDeployedFrame,
label: &str,
target_url: &str,
x: f64,
y: f64,
) -> Result<String> {
let id = client
.create_struct_card(&host.frame_id, label, target_url, x, y, CARD_WIDTH, CARD_HEIGHT)
.await
.change_context(EvmMiroError)?;
host.link_cards.push((label.to_string(), id.clone(), String::new()));
save_frame_record(host)?;
Ok(id)
}
pub fn card_size() -> (f64, f64) {
(CARD_WIDTH, CARD_HEIGHT)
}
fn free_rect(host: &AutoDeployedFrame, width: f64, height: f64) -> Result<(f64, f64)> {
const GAP: f64 = 600.0;
let metadata = EvmBatMetadata::read_metadata().change_context(EvmMiroError)?;
let taken: Vec<(f64, f64, f64, f64)> = metadata
.miro
.auto
.frames
.iter()
.map(|frame| (frame.x, frame.y, frame.width, frame.height))
.collect();
let overlaps = |x: f64, y: f64| {
taken.iter().any(|(ox, oy, ow, oh)| {
(x - ox).abs() * 2.0 < width + ow + GAP && (y - oy).abs() * 2.0 < height + oh + GAP
})
};
for ring in 1..40 {
let step = ring as f64;
let candidates = [
(host.x + (host.width + width) / 2.0 + GAP * step, host.y),
(host.x, host.y + (host.height + height) / 2.0 + GAP * step),
(host.x - (host.width + width) / 2.0 - GAP * step, host.y),
(host.x, host.y - (host.height + height) / 2.0 - GAP * step),
];
for (x, y) in candidates {
if !overlaps(x, y) {
return Ok((x, y));
}
}
}
Err(Report::new(EvmMiroError)
.attach_printable("no free space near the frame to put the type's frame in")
.attach(crate::Suggestion(
"move some frames apart on the board, or deploy this entry point again".to_string(),
)))
}
pub fn announce(label: &str, url: &str) {
println!(" {} {} drawn as its own frame", "✓".green(), label.bold());
println!(" {}", url.blue());
}