import logging
from flask import Flask, request, jsonify, Response, stream_with_context
from flask_cors import CORS
from dotenv import load_dotenv
import os
import subprocess
import requests

app = Flask(__name__)
load_dotenv('/home/gds/printer/.env')  # .env 파일 경로
CORS(app, resources={r"/*": {"origins": "*"}})  # 모든 출처에 대해 CORS를 활성화합니다.

# 로그 디렉토리와 파일 설정
log_dir = '/home/gds/printer/log'
if not os.path.exists(log_dir):
    os.makedirs(log_dir)
log_file = os.path.join(log_dir, 'app.log')

# 로깅 설정
logging.basicConfig(filename=log_file, level=logging.DEBUG,
                    format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s')
app.logger.addHandler(logging.StreamHandler())
app.logger.setLevel(logging.DEBUG)

# start_gcode, end_gcode 변수 선언
start_gcode = """
M17
M862.1 P[nozzle_diameter]
M862.3 P "MK4"
M862.5 P2
M862.6 P "Input shaper"
M115 U6.0.1+14848
M555 X{(min(print_bed_max[0], first_layer_print_min[0] + 32) - 32)} Y{(max(0, first_layer_print_min[1]) - 4)}
W{((min(print_bed_max[0], max(first_layer_print_min[0] + 32, first_layer_print_max[0])))) - ((min(print_bed_max[0], first_layer_print_min[0] + 32) - 32))} H{((first_layer_print_max[1])) - ((max(0, first_layer_print_min[1]) - 4))}
G90
M83
M140 S[first_layer_bed_temperature]

{if filament_notes[0]=~/.*HT_MBL10.*/}
M104 T0 S{first_layer_temperature[0] - 10}
M109 T0 R{first_layer_temperature[0] - 10}
{endif}

{if filament_type[0] == "PC" or filament_type[0] == "PA"}
M104 T0 S{first_layer_temperature[0] - 25}
M109 T0 R{first_layer_temperature[0] - 25}
{endif}

{if filament_type[0] == "FLEX"}
M104 T0 S210
M109 T0 R210
{endif}

{if filament_type[0]=~/.*PET.*/}
M104 T0 S175
M109 T0 R175
{endif}

{if not (filament_notes[0]=~/.*HT_MBL10.*/ or filament_type[0] == "PC" or filament_type[0] == "PA" or filament_type[0] == "FLEX" or filament_type[0]=~/.*PET.*/)}
M104 T0 S170
M109 T0 R170
{endif}

M84 E
G28
G1 X{10 + 32} Y-4 Z5 F4800
M302 S160
{if filament_type[initial_tool]=="FLEX"}
G1 E-4 F2400
{else}
G1 E-2 F2400
{endif}
M84 E
G29 P9 X10 Y-4 W32 H4
{if first_layer_bed_temperature[initial_tool]<=60}M106 S100{endif}
G0 Z40 F10000
M190 S[first_layer_bed_temperature]
M107
M84 E
G29 P1
G29 P1 X0 Y0 W50 H20 C
G29 P3.2
G29 P3.13
G29 A
M104 S{first_layer_temperature[0]}
G0 X0 Y-4 Z15 F4800
M109 S{first_layer_temperature[0]}
G92 E0
M569 S0 E
G92 E0
G1 E{(filament_type[0] == "FLEX" ? 4 : 2)} F2400
G0 E7 X15 Z0.2 F500
G0 X25 E4 F500
G0 X35 E4 F650
G0 X45 E4 F800
G0 X{45 + 3} Z{0.05} F{8000}
G0 X{45 + 3 * 2} Z0.2 F{8000}
G92 E0
M221 S100
"""

end_gcode = """
{if layer_z < max_print_height}G1 Z{z_offset+min(layer_z+1, max_print_height)} F720{endif}
M104 S0
M140 S0
M107
G1 X241 Y170 F3600
{if layer_z < max_print_height}G1 Z{z_offset+min(layer_z+23, max_print_height)} F300{endif}
G4
M572 S0
M593 X T2 F0
M593 Y T2 F0
M84 X Y E
"""

error_code = ''
err_code =''

def slice_stl(file_path, output_dir, options=None):
    """
    STL 파일을 PrusaSlicer를 사용하여 슬라이스하는 함수
    """
    # PrusaSlicer 명령어 생성
    prusa_slicer_path = '/usr/bin/PrusaSlicer'

    command = [
        prusa_slicer_path, '-g',
    ]

    # 옵션 추가
    for key, value in options.items():
        command.extend([f'--{key}', value])

    # 파일 경로 추가
    command.append(f'"{file_path}"')

    # 출력 디렉토리는 PrusaSlicer가 인식할 수 있는 형식으로 설정
    command.extend(['-o', output_dir])

    # skirts가 0일 때 관련 옵션 제거
    if options.get('skirts') == '0':
        command = [opt for idx, opt in enumerate(command) if 'skirt' not in opt and (idx == 0 or 'skirt' not in command[idx - 1])]
        command = [opt for idx, opt in enumerate(command) if 'skirts' not in opt and (idx == 0 or 'skirts' not in command[idx - 1])]

    # brim-type이 no_brim일 때 관련 옵션 제거
    if options.get('brim_type') == 'no_brim':
        command = [opt for idx, opt in enumerate(command) if 'brim' not in opt and (idx == 0 or 'brim' not in command[idx - 1])]
        command = [opt for idx, opt in enumerate(command) if 'draft' not in opt and (idx == 0 or 'draft' not in command[idx - 1])]
    

    # 이 코드 처리 중에 --support-material 이나 --support-material-auto가 true,false에 따라서 다르게 동작해야 되는데, 둘중 하나 통과하는 부분에서
    # command option이 사라진것 같아 확인 필요
    i = 0
    while i < len(command):
        if command[i] == '--support-material' and options.get('support_material') == 'true':
            command[i + 1] = ''  # Set the next index to empty string
            i += 1  # Move to the next index to skip removing '--support-material'
            continue

        if command[i] == '--support-material-auto' and options.get('support_material_auto') == 'true':
            command[i + 1] = ''  # Set the next index to empty string
            i += 1  # Move to the next index to skip removing '--support-material-auto'
            continue

        if command[i] == '--support-material' and options.get('support_material') == 'false':
            command.pop(i)  # Remove '--support-material'
            command.pop(i)  # Remove the value after '--support-material'
            continue


        if command[i] == '--support-material-auto' and options.get('support_material_auto') == 'false':
            command.pop(i)  # Remove '--support-material-auto'
            command.pop(i)  # Remove the value after '--support-material-auto'
            continue

        i += 1

    #app.logger.debug("command : %s", ' '.join(command))       

    try:
        result = subprocess.run(' '.join(command), shell=True, capture_output=True, text=True, check=True)
        app.logger.debug("PrusaSlicer output: %s", result.stdout)
        return True
    except subprocess.CalledProcessError as e:
        app.logger.error("PrusaSlicer error: %s", e.stderr)
        global error_code
        error_code = e.stderr
        return False
    
def parse_slicing_error(error_code):
    error_patterns = {
        "There is an object with no extrusions in the first layer.": (401, "첫 번째 레이어에 압출물이 없는 객체가 있습니다."),
        "No extrusions were generated for objects.": (402, "객체에 대한 압출물이 생성되지 않았습니다."),
        "The print is empty. The model is not printable with current print settings.": (403, "출력이 비어 있습니다. 현재 인쇄 설정으로는 모델을 인쇄할 수 없습니다."),
        "Levitating objects cannot be printed without supports.": (404, "떠있는 객체는 지지대 없이 인쇄할 수 없습니다."),
        "No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.": (405, "레이어가 감지되지 않았습니다. STL 파일을 수리하거나 크기 또는 두께를 확인하고 다시 시도해 보십시오."),
        "No pad can be generated for this model with the current configuration": (406, "현재 설정으로는 이 모델에 대해 패드를 생성할 수 없습니다."),
        "There are unprintable objects. Try to adjust support settings to make the objects printable.": (407, "인쇄할 수 없는 객체가 있습니다. 객체를 인쇄 가능하게 만들기 위해 지지대 설정을 조정해 보십시오.")
    }

    for pattern, (code, message) in error_patterns.items():
        if pattern in error_code:
            return code, message
    
    # 기본 에러 메시지
    return 400, "모델 슬라이스에 실패했습니다."

@app.route('/slice', methods=['POST'])
def slice_model():
    global error_code
    upload_dir = '/home/gds/printer/stl'
    if not os.path.exists(upload_dir):
        os.makedirs(upload_dir)
    # 1. file 없을 시 에러 발생
    file = request.files.get('file')
    if not file:
        # STL 파일을 저장할 경로 설정
        #2. file이 없어도 was에서 저장시킨 원래 파일명이 있을시
        if 'orignlFileNm' in request.form and request.form['orignlFileNm']:
            file_path = os.path.join(upload_dir, request.form['orignlFileNm'])
            #3. 저장 경로 + 파일명이 존재하지 않을 시
            if not os.path.exists(file_path):
                app.logger.error('No file in filepath')
                return jsonify({'err_code': '200','err_title':'경로상 파일 존재하지 않음','err_msg':'파일이 현재 경로 상에 존재하지 않습니다(/home/gds/printer)'})  
        else:
            app.logger.error('No file part')
            return jsonify({'err_code': '100','err_title':'업로드 파일 없음','err_msg':'업로드될 파일이 존재하지 않습니다.'})
    else:
        file_extension = os.path.splitext(file.filename)[1].lower()
        if file_extension != '.stl':
            app.logger.error('Invalid file type: %s', file_extension)
            return jsonify({'err_code': '300','err_title':'확장자가 다름','err_msg':'STL 파일만 업로드 가능합니다.'})

        file_path = os.path.join(upload_dir, file.filename)
        file.save(file_path)
        app.logger.debug('File saved to %s', file_path)

    #form으로 부터 dictionary 형태로 가지고 옴
    options = request.form.to_dict()

    keys_to_remove = ['options_select_printer','options_select_filament', 'preset-options','wipe-tower-adjustment','menuId','orId','prId','atchFileId1','pageIndex','_csrf','orignlFileNm','atchFileId']
    for key in keys_to_remove:
        if key in options:
            del options[key]

    options['filament-colour'] = '"'+options['filament-colour']+'"'
    options['fill-density'] = options['fill-density'] + "%" 

    # 값이 "on"인 항목을 ""로 변경
    for key, value in options.items(): 
        if value == 'on' or value == 'true' or value == 'false' :
            options[key] = ''

    #start-gcode, end-gcode 추가
    options['start-gcode'] = f"'{start_gcode}'"
    options['end-gcode'] = f"'{end_gcode}'"

    # PrusaSlicer를 사용하여 STL 파일 슬라이스
    output_dir = '/home/gds/printer/gcode/'
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    success = slice_stl(file_path, output_dir, options)

    if success:
        # G-code 파일 경로
        if not file:
            gcode_file_path = os.path.join(output_dir,os.path.splitext(request.form['orignlFileNm'])[0]+ '.gcode')
            return jsonify({'gcode_file_path': gcode_file_path})
        else:
            gcode_file_path = os.path.join(output_dir, os.path.splitext(file.filename)[0] + '.gcode')
            return jsonify({'gcode_file_path': gcode_file_path})
    else:
        error_code, error_message = parse_slicing_error(error_code)
        return jsonify({'err_code': error_code,'err_title':'슬라이스 에러','err_msg': error_message})  

@app.route('/getGCodeFile', methods=['GET'])
def get_gcode_file():
    gcode_file_path = request.args.get('gcodeFilePath')
    if not gcode_file_path:
        return jsonify({'error': 'Missing gcode_file_path parameter'})

    try:
        with open(gcode_file_path, 'r') as file:
            gcode_string = file.read()

        return gcode_string
    except FileNotFoundError:
        return jsonify({'error': 'File not found'})
    
@app.route('/getPrusaConfig', methods=['GET'])
def get_prusa_config():
    config = {
        'printer1': {
            'url': os.getenv('PRUSA_PRINTER1_API_URL'),
            'api_key': os.getenv('PRUSA_PRINTER1_API_KEY')
        },
        'printer2': {
            'url': os.getenv('PRUSA_PRINTER2_API_URL'),
            'api_key': os.getenv('PRUSA_PRINTER2_API_KEY')
        }
    }
    return jsonify(config)

@app.route('/proxy', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy_prusa_link():
    try:
        # 요청 URL과 파라미터 설정
        url = request.args.get('url')
        if not url:
            return Response("URL is required", status=400)

        # PrusaLink에 보낼 요청 설정
        headers = {key: value for key, value in request.headers.items() if key != 'Host'}
        response = requests.request(
            method=request.method,
            url=url,
            headers=headers,
            data=request.get_data(),
            cookies=request.cookies,
            stream=True  # 스트리밍 응답 처리
        )

        # PrusaLink에서 받은 스트리밍 응답을 클라이언트에 전달
        def generate():
            for chunk in response.iter_content(chunk_size=8192):
                yield chunk

        proxy_response = Response(stream_with_context(generate()), status=response.status_code)
        for key, value in response.headers.items():
            if key.lower() != 'content-encoding' and key.lower() != 'transfer-encoding':
                proxy_response.headers[key] = value

        return proxy_response
    except Exception as e:
        return Response(f"An error occurred: {str(e)}", status=500)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000, debug=True)