#!/usr/bin/env python

# This file is for preprocessing G-code and the new G29 Auto bed leveling from Marlin
# It will analyze the first 2 layers and return the maximum size for this part
# Then it will be replaced with g29_keyword = ';MarlinG29Script' with the new G29 LRFB.
# The new file will be created in the same folder.

from __future__ import print_function

# Your G-code file/folder
folder = './'
my_file = 'test.gcode'

# The minimum number of G1 instructions that should be between 2 different heights
min_g = 3

# Maximum number of lines to parse. We don't want to parse the
# whole file since we're only interested in the first plane.
max_g = 100000000

# G29 keyword
g29_keyword = 'G29'

# Output filename
output_file = folder + 'g29_' + my_file
# Input filename
input_file = folder + my_file

# Minimum scan size
min_size = 40
probing_points = 3  # points x points
max_lines = 1500

# Other stuff
min_x = 500
min_y = min_x
max_x = -500
max_y = max_x
last_z = 0.001

layer = 0
lines_of_g1 = 0

gcode = []

g29_found = False
g28_found = False

YELLOW = '\033[33m'
GREEN = '\033[32m'
RED = '\033[31m'
RESET = '\033[0m'

# Return only G0-G1 lines
def has_g_move(line):
    return line[:2].upper() in ("G0", "G1")

# Find position in G move (x,y,z)
def find_axis(line, axis):
    found = False
    number = ""
    for char in line:
        if found:
            if char == ".":
                number += char
            elif char == "-":
                number += char
            else:
                try:
                    int(char)
                    number += char
                except ValueError:
                    break
        else:
            found = char.upper() == axis.upper()
    try:
        return float(number)
    except ValueError:
        return None


# Save the min or max-values for each axis
def set_mima(line):
    global min_x, max_x, min_y, max_y, last_z

    current_x = find_axis(line, 'x')
    current_y = find_axis(line, 'y')

    if current_x is not None:
        min_x = min(current_x, min_x)
        max_x = max(current_x, max_x)
    if current_y is not None:
        min_y = min(current_y, min_y)
        max_y = max(current_y, max_y)

    return min_x, max_x, min_y, max_y


# Find z in the code and return it
def find_z(gcode, start_at_line=0):
    for i in range(start_at_line, len(gcode)):
        my_z = find_axis(gcode[i], 'Z')
        if my_z is not None:
            return my_z, i


def z_parse(gcode, start_at_line=0, end_at_line=0):
    i = start_at_line
    all_z = []
    line_between_z = []
    z_at_line = []
    #last_z = 0
    last_i = -1

    while len(gcode) > i:
        result = find_z(gcode, i + 1)

        if result is None:
            raise ValueError(f'{RED}Unable to determine Z height.{RESET}')

        z, i = result

        all_z.append(z)
        z_at_line.append(i)
        temp_line = i - last_i -1
        line_between_z.append(i - last_i - 1)
        #last_z = z
        last_i = i
        if 0 < end_at_line <= i or temp_line >= min_g:
            #print('break at line {} at height {}'.format(i, z))
            break

    line_between_z = line_between_z[1:]
    return all_z, line_between_z, z_at_line


# Get the lines which should be the first layer
def get_lines(gcode, minimum):
    i = 0
    all_z, line_between_z, z_at_line = z_parse(gcode, end_at_line=max_g)
    #print('Detected Z heights:', all_z)
    for count in line_between_z:
        i += 1
        if count > minimum:
            #print('layer: {}:{}'.format(z_at_line[i-1], z_at_line[i]))
            return z_at_line[i - 1], z_at_line[i]


with open(input_file, 'r', encoding='utf_8') as file:
    lines = 0
    for line in file:
        lines += 1
        if lines > max_lines:
            break
        if has_g_move(line):
            gcode.append(line)
file.close()

layer_range = get_lines(gcode, min_g)

if layer_range is None:
    raise ValueError(f'{RED}Unable to determine layer range.{RESET}')

start, end = layer_range

for i in range(start, end):
    set_mima(gcode[i])

print('x_min:{} x_max:{}\ny_min:{} y_max:{}'.format(min_x, max_x, min_y, max_y))

# Resize min/max - values for minimum scan
if max_x - min_x < min_size:
    offset_x = int((min_size - (max_x - min_x)) / 2 + 0.5)  # int round up
    #print('min_x! with {}'.format(int(max_x - min_x)))
    min_x = int(min_x) - offset_x
    max_x = int(max_x) + offset_x
if max_y - min_y < min_size:
    offset_y = int((min_size - (max_y - min_y)) / 2 + 0.5)  # int round up
    #print('min_y! with {}'.format(int(max_y - min_y)))
    min_y = int(min_y) - offset_y
    max_y = int(max_y) + offset_y


new_command = 'G29 L{0} R{1} F{2} B{3} P{4}\n'.format(min_x,
                                                      max_x,
                                                      min_y,
                                                      max_y,
                                                      probing_points)

with open(input_file, 'r', encoding='utf_8') as in_file, open(output_file, 'w', encoding='utf_8') as out_file:
    for line in in_file:
        # Check if G29 already exists
        if line.strip().upper().startswith(g29_keyword):
            g29_found = True
            out_file.write(new_command)
            print(f'{YELLOW}Write G29.{RESET}')
        else:
            out_file.write(line)

        # If we find G28 and G29 wasn't found earlier, insert G29 after G28
        if not g29_found and line.strip().upper().startswith('G28'):
            g28_found = True  # Mark that G28 was found
            out_file.write(new_command)  # Insert G29 command
            print(f'{YELLOW}Note: G29 was not found.\nInserted G29 after G28.{RESET}')

# Debugging messages
if not g28_found and not g29_found:
    print(f'{RED}Error: G28 not found! G29 was not added.{RESET}')
else:
    print(f'{GREEN}auto G29 finished!{RESET}')
