///|
/// G-Code synthesis and volumetric extrusion calculations for Marlin/RepRap firmware.
/// Implements PrusaSlicer-compatible multi-island, hole perimeters, and solid infill.

///|
/// Formatted G-code result containing the output string and comprehensive physical slicing statistics.
pub(all) struct GCodeResult {
  gcode : String
  total_filament_mm : Double
  total_filament_grams : Double
  estimated_cost : Double
  total_layers : Int32
  estimated_print_time_sec : Double
  travel_time_sec : Double
  extrusion_time_sec : Double
  total_collisions_resolved : Int32
} derive(Debug)

///|
/// Calculate extrusion cross-sectional area based on nozzle line width and layer height.
/// Uses stadium cross-section (flat rectangle core with two semi-circular rounded caps).
pub fn calculate_extrusion_area(
  line_width : Double,
  layer_height : Double,
) -> Double {
  let rect_area = (line_width - layer_height) * layer_height
  let semi_area = 3.141592653589793 *
    (layer_height * 0.5) *
    (layer_height * 0.5)
  rect_area + semi_area
}

///|
/// Calculate extrusion length (delta E) for moving distance L with given filament diameter.
pub fn calculate_delta_e(
  move_length : Double,
  line_width : Double,
  layer_height : Double,
  filament_diameter : Double,
) -> Double {
  let bead_area = calculate_extrusion_area(line_width, layer_height)
  let fil_radius = filament_diameter * 0.5
  let fil_area = 3.141592653589793 * fil_radius * fil_radius
  bead_area * move_length / fil_area
}

///|
/// Helper to format a Double to 3 decimal places without exponential notation.
pub fn format_f3(val : Double) -> String {
  let is_neg = val < 0.0
  let abs_v = if is_neg { -val } else { val }

  let mut int_part = abs_v.to_int()
  let mut frac_part = ((abs_v - int_part.to_double()) * 1000.0 + 0.5000001).to_int()
  if frac_part >= 1000 {
    int_part += 1
    frac_part -= 1000
  }

  let sb = StringBuilder()
  if is_neg && (int_part > 0 || frac_part > 0) {
    sb.write_char('-')
  }
  sb.write_string(int_part.to_string())
  sb.write_char('.')
  if frac_part < 10 {
    sb.write_string("00")
  } else if frac_part < 100 {
    sb.write_char('0')
  }
  sb.write_string(frac_part.to_string())
  sb.to_string()
}

///|
/// Synthesize standard Marlin/RepRap G-code from sliced layers with PrusaSlicer feature alignment.
pub fn generate_gcode(
  layers : Array[SliceLayer],
  config : SlicerConfig,
  bed_center_x~ : Double = 100.0,
  bed_center_y~ : Double = 100.0,
) -> GCodeResult {
  let sb = StringBuilder()
  let mut current_e = 0.0
  let mut total_travel_sec = 0.0
  let mut total_extrusion_sec = 0.0
  let mut total_collisions = 0

  let mut current_pos_x = bed_center_x
  let mut current_pos_y = bed_center_y

  // Header comments
  sb.write_string("; ==========================================\n")
  sb.write_string("; Generated by MoonSlicer (Pure MoonBit 3D Slicer)\n")
  sb.write_string("; Total Layers: \{layers.length()}\n")
  sb.write_string("; Layer Height: \{config.layer_height} mm\n")
  sb.write_string("; Nozzle Line Width: \{config.line_width} mm\n")
  sb.write_string("; Wall Count: \{config.wall_count}\n")
  sb.write_string("; Infill Pattern: \{config.infill_pattern}\n")
  sb.write_string("; Seam Mode: \{config.seam_mode}\n")
  sb.write_string("; Material: \{config.material}\n")
  sb.write_string("; Infill Density: \{config.infill_density * 100.0}%\n")
  sb.write_string("; Target Bed Center: X\{bed_center_x} Y\{bed_center_y}\n")
  sb.write_string("; ==========================================\n\n")

  // Start G-code initialization
  sb.write_string("G21 ; set units to millimeters\n")
  sb.write_string("G90 ; use absolute coordinates\n")
  sb.write_string("M82 ; use absolute distances for extrusion\n")
  sb.write_string("M140 S\{config.bed_temp} ; set bed temp\n")
  sb.write_string("M104 S\{config.nozzle_temp} ; set extruder temp\n")
  sb.write_string("M190 S\{config.bed_temp} ; wait for bed temp\n")
  sb.write_string("M109 S\{config.nozzle_temp} ; wait for extruder temp\n")
  sb.write_string("G28 ; home all axes\n")
  sb.write_string("M106 S\{config.fan_speed} ; enable cooling fan\n")
  sb.write_string("G92 E0 ; reset extruder position\n")
  sb.write_string("G1 Z0.300 F3000 ; lift nozzle\n")
  sb.write_string("G1 X10.000 Y10.000 F6000 ; move to prime start\n")
  sb.write_string("G1 X60.000 Y10.000 E2.000 F1500 ; prime nozzle\n")
  sb.write_string("G92 E0 ; reset extruder\n\n")

  let default_print_feedrate = (config.print_speed * 60.0).to_int()
  let solid_infill_feedrate = (config.solid_infill_speed * 60.0).to_int()
  let bridge_feedrate = (config.bridge_speed * 60.0).to_int()
  let travel_feedrate = (config.travel_speed * 60.0).to_int()
  let retract_feedrate = (config.retract_speed * 60.0).to_int()

  for layer_idx, layer in layers {
    let layer_z = layer.z
    let current_h = if layer_idx == 0 {
      config.initial_layer_height
    } else {
      config.layer_height
    }

    let layer_surface = classify_layer(layer_idx, layers.length(), config)

    sb.write_string("; ------------------------------------------\n")
    sb.write_string("; LAYER: \{layer_idx}\n")
    sb.write_string("; Z: \{format_f3(layer_z)}\n")
    sb.write_string("; LAYER_TYPE: \{layer_surface}\n")
    sb.write_string("G0 Z\{format_f3(layer_z)} F\{travel_feedrate}\n")

    let expolys = if !layer.expolygons.is_empty() {
      layer.expolygons
    } else {
      build_expolygons(layer.polygons)
    }

    for _island_idx, expoly in expolys {
      let island_surface = classify_island_surface(
        expoly, layer_idx, layers, config,
      )

      // 1. Generate multi-wall perimeters (outer contours + hole contours)
      let perims = generate_expolygon_perimeters(
        expoly,
        config.wall_count,
        config.line_width,
      )
      total_collisions += perims.collisions.length()
      for c in perims.collisions {
        sb.write_string(
          "; [COLLISION RESOLVED] Hole \{c.hole_a_idx} - \{c.action} (clearance: \{format_f3(c.clearance)} mm)\n",
        )
      }

      // 1a. Outer walls with seam placement
      for wall_idx, wall in perims.outer_walls {
        let pts_raw = wall.points
        if pts_raw.length() < 3 {
          continue
        }

        // Apply SeamMode
        let seam_idx = match config.seam_mode {
          SharpestCorner => wall.sharpest_corner_idx()
          Nearest =>
            wall.nearest_vertex_idx(
              Vec2::new(
                current_pos_x - bed_center_x,
                current_pos_y - bed_center_y,
              ),
            )
          Aligned => wall.nearest_vertex_idx(Vec2::new(0.0, 100.0))
          Random => (layer_idx * 17 + wall_idx * 31) % pts_raw.length()
        }
        let aligned_wall = wall.rotate_start(seam_idx)
        let pts = aligned_wall.points

        let start_x = pts[0].x + bed_center_x
        let start_y = pts[0].y + bed_center_y
        let travel_dist = Vec2::new(current_pos_x, current_pos_y).distance(
          Vec2::new(start_x, start_y),
        )
        total_travel_sec += travel_dist / config.travel_speed
        current_pos_x = start_x
        current_pos_y = start_y

        sb.write_string(
          "G0 X\{format_f3(start_x)} Y\{format_f3(start_y)} F\{travel_feedrate}\n",
        )

        let wall_type_label = if wall_idx == 0 {
          "; TYPE: Outer perimeter"
        } else {
          "; TYPE: Perimeter"
        }

        let m = pts.length()
        for k = 0; k < m; k = k + 1 {
          let curr = pts[k]
          let next = pts[(k + 1) % m]
          let move_dist = curr.distance(next)

          let delta_e = calculate_delta_e(
            move_dist,
            config.line_width,
            current_h,
            config.filament_diameter,
          )
          current_e += delta_e
          total_extrusion_sec += move_dist / config.print_speed

          let nx = next.x + bed_center_x
          let ny = next.y + bed_center_y
          current_pos_x = nx
          current_pos_y = ny

          sb.write_string(
            "G1 X\{format_f3(nx)} Y\{format_f3(ny)} E\{format_f3(current_e)} F\{default_print_feedrate} \{wall_type_label}\n",
          )
        }
      }

      // 1b. Hole walls with seam placement
      for _hole_idx, hwalls in perims.hole_walls {
        for _wall_idx, hwall in hwalls {
          let pts_raw = hwall.points
          if pts_raw.length() < 3 {
            continue
          }

          let seam_idx = match config.seam_mode {
            SharpestCorner => hwall.sharpest_corner_idx()
            Nearest =>
              hwall.nearest_vertex_idx(
                Vec2::new(
                  current_pos_x - bed_center_x,
                  current_pos_y - bed_center_y,
                ),
              )
            Aligned => hwall.nearest_vertex_idx(Vec2::new(0.0, 100.0))
            Random => (layer_idx * 13 + _wall_idx * 19) % pts_raw.length()
          }
          let aligned_hwall = hwall.rotate_start(seam_idx)
          let pts = aligned_hwall.points

          let start_x = pts[0].x + bed_center_x
          let start_y = pts[0].y + bed_center_y
          let travel_dist = Vec2::new(current_pos_x, current_pos_y).distance(
            Vec2::new(start_x, start_y),
          )
          total_travel_sec += travel_dist / config.travel_speed
          current_pos_x = start_x
          current_pos_y = start_y

          sb.write_string(
            "G0 X\{format_f3(start_x)} Y\{format_f3(start_y)} F\{travel_feedrate}\n",
          )

          let m = pts.length()
          for k = 0; k < m; k = k + 1 {
            let curr = pts[k]
            let next = pts[(k + 1) % m]
            let move_dist = curr.distance(next)

            let delta_e = calculate_delta_e(
              move_dist,
              config.line_width,
              current_h,
              config.filament_diameter,
            )
            current_e += delta_e
            total_extrusion_sec += move_dist / config.print_speed

            let nx = next.x + bed_center_x
            let ny = next.y + bed_center_y
            current_pos_x = nx
            current_pos_y = ny

            sb.write_string(
              "G1 X\{format_f3(nx)} Y\{format_f3(ny)} E\{format_f3(current_e)} F\{default_print_feedrate} ; TYPE: Hole perimeter\n",
            )
          }
        }
      }

      // 2. Generate infill lines within infill boundary
      match perims.infill_boundary {
        Some(ib) => {
          let is_bridge = island_surface == BottomSolid && layer_idx > 0
          if is_bridge {
            sb.write_string(
              "M106 S\{config.bridge_fan_speed} ; bridge fan boost\n",
            )
          }

          let (density, print_feedrate, speed_val, pattern_to_use, infill_label) = match
            island_surface {
            TopSolid =>
              (
                1.0,
                solid_infill_feedrate,
                config.solid_infill_speed,
                InfillPattern::Rectilinear,
                "; TYPE: Top solid infill",
              )
            BottomSolid =>
              if is_bridge {
                (
                  1.0,
                  bridge_feedrate,
                  config.bridge_speed,
                  InfillPattern::Rectilinear,
                  "; TYPE: Bridge infill",
                )
              } else {
                (
                  1.0,
                  solid_infill_feedrate,
                  config.solid_infill_speed,
                  InfillPattern::Rectilinear,
                  "; TYPE: Bottom solid infill",
                )
              }
            InternalSparse =>
              (
                config.infill_density,
                default_print_feedrate,
                config.print_speed,
                config.infill_pattern,
                "; TYPE: Internal infill",
              )
          }

          let infill_lines = generate_expolygon_infill(
            ib,
            pattern_to_use,
            density,
            config.line_width,
            layer_idx,
            config.infill_angle,
          )

          for seg in infill_lines {
            let p1x = seg.p1.x + bed_center_x
            let p1y = seg.p1.y + bed_center_y
            let p2x = seg.p2.x + bed_center_x
            let p2y = seg.p2.y + bed_center_y

            let travel_dist = Vec2::new(current_pos_x, current_pos_y).distance(
              Vec2::new(p1x, p1y),
            )
            total_travel_sec += travel_dist / config.travel_speed
            current_pos_x = p1x
            current_pos_y = p1y

            sb.write_string(
              "G0 X\{format_f3(p1x)} Y\{format_f3(p1y)} F\{travel_feedrate}\n",
            )

            let move_dist = seg.length()
            let flow_scale = if is_bridge {
              config.bridge_flow_ratio
            } else {
              1.0
            }
            let delta_e = calculate_delta_e(
                move_dist,
                config.line_width,
                current_h,
                config.filament_diameter,
              ) *
              flow_scale
            current_e += delta_e
            total_extrusion_sec += move_dist / speed_val

            current_pos_x = p2x
            current_pos_y = p2y

            sb.write_string(
              "G1 X\{format_f3(p2x)} Y\{format_f3(p2y)} E\{format_f3(current_e)} F\{print_feedrate} \{infill_label}\n",
            )
          }

          if is_bridge {
            sb.write_string(
              "M106 S\{config.fan_speed} ; restore standard fan\n",
            )
          }
        }
        None => ()
      }
    }

    // Layer-end retraction
    if config.retract_distance > 0.0 {
      let ret_e = current_e - config.retract_distance
      sb.write_string(
        "G1 E\{format_f3(ret_e)} F\{retract_feedrate} ; retract\n",
      )
    }
  }

  // End G-code sequence
  sb.write_string("\n; End G-code sequence\n")
  sb.write_string("M104 S0 ; turn off hotend\n")
  sb.write_string("M140 S0 ; turn off heatbed\n")
  sb.write_string("G91 ; relative coordinates\n")
  sb.write_string("G1 Z+5.000 F3000 ; lift nozzle\n")
  sb.write_string("G90 ; absolute coordinates\n")
  sb.write_string("G1 X0.000 Y200.000 F6000 ; present print\n")
  sb.write_string("M84 ; disable stepper motors\n")

  // Volumetric & physical mass calculations
  let fil_radius_cm = config.filament_diameter * 0.5 * 0.1 // mm to cm
  let fil_area_cm2 = 3.141592653589793 * fil_radius_cm * fil_radius_cm
  let fil_len_cm = current_e * 0.1
  let vol_cm3 = fil_area_cm2 * fil_len_cm
  let filament_grams = vol_cm3 * config.material.density()
  let est_cost = filament_grams / 1000.0 * config.filament_cost_per_kg
  let total_time_sec = total_travel_sec + total_extrusion_sec

  sb.write_string("; Total Filament Used: \{format_f3(current_e)} mm\n")
  sb.write_string("; Total Filament Mass: \{format_f3(filament_grams)} g\n")
  sb.write_string("; Estimated Print Cost: \{format_f3(est_cost)}\n")

  {
    gcode: sb.to_string(),
    total_filament_mm: current_e,
    total_filament_grams: filament_grams,
    estimated_cost: est_cost,
    total_layers: layers.length(),
    estimated_print_time_sec: total_time_sec,
    travel_time_sec: total_travel_sec,
    extrusion_time_sec: total_extrusion_sec,
    total_collisions_resolved: total_collisions,
  }
}