import java.text.*

/**
 * Some functions to help generate GCode directly (rather than via a slicer).
 * NOTE. Bad GCode can damage your printer (including setting it on fire).
 * So never use DirectGCode scripts from untrusted sources.
 * Only run DirectGCode scripts that you completely trust with your life!
 *
*/

class DirectHelper {

    // If moveTo or lineTo got beyond these limits, the values
    // an exception is thrown.
    // This one way to prevent damage to your printer.
    // NOTE. there are other ways that direct gcode can damage your printer!
    
    static var maxLimit = Vector3( 230, 230, 300 )
    static var minLimit = Vector3(0,0,0)
    static var offset = Vector3( 0, 0, 0 )

    // Keep track of the current position
    // As used by lineTo() etc.
    // NOTE, if you add G1 commands manually, then you must update position
    // before calling lineTo etc.
    static var position = Vector3(0,0,0)
    static var totalE = 0.0 // The running total of extrusions from lineTo()

    // Should X Y Z values use relative values?
    static var isRelative = false
    // Should E values (of the G1 command) use relative values?
    static var isRelativeE = true
    // NOTE, the values should be set during the initialisation,
    // and are usually left unchanged for the rest of the print.

    // How much to extrude (as the E parameter of the G1 command), when
    // moving the head by 1mm.
    // This is based on the nozzle size and layer height, as well as any
    // adjustment for over/under extrusion.
    static var ePerMM = 1.0

    static var formatter : NumberFormat = DecimalFormat("0.#####")

   func format( value : double ) =  formatter.format( value )


    // Throws an Exception if the point is outside of the limits of minLimit .. maxLimit
   func checkPoint( x : double, y : double ) {
        if (x<minLimit.x || y < minLimit.y || x >= maxLimit.x || y >= maxLimit.y ) {
            throw Exception( "$x $y is outside the limits : $minLimit .. $maxLimit" )
        }
    }
   func checkPoint( x : double, y : double, z : double ) {
        checkPoint( x, y )
        if (z<minLimit.z || z >= maxLimit.z ) {
            throw Exception( "$x $y $z is outside the limits : $minLimit .. $maxLimit" )
        }
    }

    // amount is a relative value, but the final gcode E parameter will be relative/absolte
    // based on isRelativeE
   func extrusion( amount : double ) : String {
        totalE += amount
        return if (isRelativeE) {
            "E${format(amount)}"
        } else {
            "E${format(totalE)}"
        }
    }
    // x and y are absolute values, but the final X and Y parameters will be relative/absolute
    // based in isRelative
   func movement( x : double, y : double ) : String {
        if (isRelative) {
            val dx = x - position.x
            val dy = y - position.y
    
            position = Vector3( x, y, position.z )
            return "X${format(dx)} Y${format(dy)}"
        } else {
            position = Vector3( x, y, position. z )
            return "X${format(x)} Y${format(y)}"
        }
    }
    // x,y and z are absolute values, but the final X Y Z parameters will be relative/absolute
    // based in isRelative
   func movement( x : double, y : double, z : double ) : String {
        if (isRelative) {
            val dx = x - position.x
            val dy = y - position.y
            val dz = z - position.z
    
            position = Vector3( x, y, z )
            return "X${format(dx)} Y${format(dy)} Z${format(dz)}"
        } else {
            position = Vector3( x, y, z )
            return "X${format(x)} Y${format(y)} Z${format(z)}"
        }
    }

   func moveTo( to : Vector2 ) {
        moveTo( to.x, to.y )
    }
   func moveTo( x : double, y : double ) {
        val offsetX = x + offset.x
        val offsetY = y + offset.y
        if ( offsetX != position.x || offsetY != position.y ) {
            checkPoint( offsetX, offsetY )
            println( "G1 ${movement( offsetX, offsetY )}" )
        }
    }

   func moveTo( to : Vector3 ) {
        moveTo( to.x, to.y, to.z )
    }
   func moveTo( x : double, y : double, z : double ) {
        val offsetX = x + offset.x
        val offsetY = y + offset.y
        val offsetZ = z + offset.z

        println( "moveTo $offsetX, $offsetY, $offsetZ" )

        if ( offsetX != position.x || offsetY != position.y || offsetZ != position.z ) {
            checkPoint( offsetX, offsetY, offsetZ )
            println( "G1 ${movement( offsetX, offsetY, offsetZ )}" )
        }
    }

   func nextLayer( dz : double ) {
        println( "; nextLayer by $dz" )
        moveTo( position.x - offset.x, position.y - offset.y, position.z - offset.z + dz )
    }

   func lineTo( to : Vector2 ) {
        lineTo( to.x, to.y )
    }
   func lineTo( x : double, y : double ) {
        val offsetX = x + offset.x
        val offsetY = y + offset.y
        if ( offsetX != position.x || offsetY != position.y ) {
            checkPoint( offsetX, offsetY )
            val dx = offsetX - position.x
            val dy = offsetY - position.y
            val dist = Math.sqrt( dx*dx + dy*dy )
            val e = dist * ePerMM
            println( "G1 ${movement(offsetX, offsetY)} ${extrusion(e)}" )
        }
    }
    

   func outlines( shape: Shape2d, count : int, nozzleSize : double ) {
        for ( i in 0 until count ) {
            outline( shape, - nozzleSize * (i + 0.5) )
        }
    }

    // Draw the outline of the 2d shape. Note the thickness of the line is NOT
    // taken into account. (The head follows the line of the shape, so the filament
    // straddles the outside and inside by the same amount.
   func outline( shape : Shape2d ) {
        for ( path in shape.paths ) {
            var isFirst = true
            for (point in path.points) {
                if (isFirst) {
                    moveTo( point )
                    isFirst = false
                } else {
                    lineTo( point )
                }
            }
            lineTo( path.points[0] )
        }
    }

   func outline( shape : Shape2d, offset : double) {
        for ( unadjustedPath in shape.paths ) {
            val path = unadjustedPath.offset( offset )            
            var isFirst = true
            for (point in path.points) {
                if (isFirst) {
                    moveTo( point )
                    isFirst = false
                } else {
                    lineTo( point )
                }
            }
            lineTo( path.points[0] )
        }
    }

   func extrusionRate( filamentDiameter : double, nozzleSize : double, layerHeight : double ) : double {
        val lineVolumePerMM = nozzleSize * layerHeight
        val extrusionVolumePerMM = Math.PI*filamentDiameter

        return lineVolumePerMM / extrusionVolumePerMM
    }

   func start( bedTemp : int, filamentTemp : int ) {
        print( """
; BEGIN DirectHelper.start($bedTemp,$filamentTemp)
G90 ; use absolute coordinates
M83 ; extruder relative mode
; M900 W[extrusion_width] H[layer_height] D[filament_diameter]
M200 D0 ; disable volumetric e
M220 S100 ; reset speed factor to 100%
M221 S100 ; reset extrusion rate to 100%

; Set the heating
M140 S${bedTemp}; Start bed heating, but don't wait
M104 S${filamentTemp}; start nozzle heating but don't wait

; Home
G1 Z3 F3000 ; move z up little to prevent scratching of surface
G28 ; home all axes
G1 X3 Y3 F5000 ; move to corner of the bed to avoid ooze over centre

; Wait for final heating
M109 S${bedTemp} ; wait for the nozzle to heat up
M190 S${filamentTemp} ; wait for the bed to heat up

; Return to prime position, Prime line routine
G92 E0 ; Reset Extruder
G1 Z3 F3000 ; move z up little to prevent scratching of surface
G1 X10 Y.5 Z0.25 F5000.0 ; Move to start position
G1 X100 Y.5 Z0.25 F1500.0 E15 ; Draw the first line
G1 X100 Y.2 Z0.25 F5000.0 ; Move to side a little
G1 X10 Y.2 Z0.25 F1500.0 E30 ; Draw the second line
G92 E0 ; Reset Extruder
; END DirectHelper.start($bedTemp,$filamentTemp)
""" )
        position = Vector3(10, 0.2, 0.25) // Same as the G1 above.
        isRelative = false // We used G90 above.
        isRelativeE = true // We use M83 above.
    }


   func end() {
        print( """
; BEGIN end()
G4 ; wait
G92 E0 ; prepare to retract
G1 E-0.5 F3000; retract to avoid stringing

; Anti-stringing end wiggle
G91 ; use relative coordinates
G1 X1 Y1 F1200

; Raise nozzle and present bed
G1 Z120 ; Move print head up
G90 ; use absolute coordinates

; Reset print setting overrides
M200 D0 ; disable volumetric e
M220 S100 ; reset speed factor to 100%
M221 S100 ; reset extrusion rate to 100%

; Shut down printer
M106 S0 ; turn-off fan
M104 S0 ; turn-off hotend
M140 S0 ; turn-off bed
M150 P0 ; turn off led
M85 S0 ; deactivate idle timeout
M84 ; disable motors
END end()
""" )
    }
    
}
