import serial
import serial.tools.list_ports
import time
import os
import subprocess
import tempfile
import shutil
import importlib.resources
from pathlib import Path

folder_name = "arduino_bowcar"
file_name = "arduino_bowcar.ino"
firmware_version = "0.0.5"

# 아두이노 보드의 핀 번호 기본 설정
# Default pin numbers for Arduino board
arduino_pins = '''
// Arduino pin numbers for BowCar
// 바우카를 위한 아두이노 핀 번호

// LED control pins:Digital
#define RED_LED_PIN 10
#define BLUE_LED_PIN 11

// Ultrasonic sensor pins:Digital
#define TRIG_PIN 12
#define ECHO_PIN 13

// IR sensor pins:Analog
#define IR_LEFT_PIN A6
#define IR_RIGHT_PIN A7

// Sound sensor pin:Analog
#define SOUND_SENSOR_PIN A3

// Buzzer pin:Digital
#define BUZZER_PIN 3 

// Motor control pins:Digital
#define MOTOR_LEFT_DIR_PIN 2
#define MOTOR_LEFT_PIN 5

#define MOTOR_RIGHT_DIR_PIN 4
#define MOTOR_RIGHT_PIN 6

// Button pin
// Analog
#define UP_BUTTON_PIN A0
#define DOWN_BUTTON_PIN A1
// Digital
#define LEFT_BUTTON_PIN 7
#define RIGHT_BUTTON_PIN 8

'''

# Arduino code for Setup
# 아두이노 코드 셋업 부분
arduino_setup_code = '''
    pinMode(RED_LED_PIN, OUTPUT);
    pinMode(BLUE_LED_PIN, OUTPUT);
    pinMode(TRIG_PIN, OUTPUT);
    pinMode(ECHO_PIN, INPUT);
    pinMode(IR_LEFT_PIN, INPUT);
    pinMode(IR_RIGHT_PIN, INPUT);
    pinMode(SOUND_SENSOR_PIN, INPUT);
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(MOTOR_LEFT_DIR_PIN, OUTPUT);
    pinMode(MOTOR_LEFT_PIN, OUTPUT);
    pinMode(MOTOR_RIGHT_DIR_PIN, OUTPUT);
    pinMode(MOTOR_RIGHT_PIN, OUTPUT);
    pinMode(UP_BUTTON_PIN, INPUT);
    pinMode(DOWN_BUTTON_PIN, INPUT);
    pinMode(LEFT_BUTTON_PIN, INPUT);
    pinMode(RIGHT_BUTTON_PIN, INPUT);
'''

# Arduino code for Loop
# 아두이노 코드 루프 부분
arduino_loop_code = ""

# Tone mapping for buzzer
# 버저를 위한 음계 매핑
tones = [
  [ 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62 ],
  # 2옥타브: C2 ~ B2
  [ 65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 123 ],
  # 3옥타브: C3 ~ B3
  [ 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247 ],
  # 4옥타브: C4 ~ B4
  [ 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494 ],
  # 5옥타브: C5 ~ B5
  [ 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988 ],
  # 6옥타브: C6 ~ B6
  [ 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976 ]
]

class BowCar:
    """
    BowCar class for controlling a bowcar via serial communication.
    바우카를 제어하기 위한 BowCar 클래스입니다.
    """
    def __init__(self):
        # BowCar 객체가 생성될 때 시리얼 연결만 시도하도록 단순화
        self.port = self._find_arduino_port()
        self.upload_firmware()  # 펌웨어 업로드 시도
        self.connection = None
        if self.port:
            try:
                self.connection = serial.Serial(self.port, 9600, timeout=1)
                print(f'connected! 연결 성공! (port : {self.port})')
                time.sleep(2)
            except serial.SerialException:
                print(f'Failed to connect! 연결 실패! (port : {self.port})')

        self.duration = 2000 # 기본 지속 시간은 2초로 설정

    def generate_arduino_file(self):
        global arduino_setup_code, arduino_loop_code, arduino_pins

        """지금까지 누적된 코드를 합쳐 최종 .ino 파일을 생성합니다."""
        print("최종 아두이노 스케치 파일을 생성합니다...")

        # 1. 최종 코드 조립
        full_code = (
            "// Auto-generated by BowCar Python module\n\n"
            + arduino_pins + "\n"
            + "void setup() {\n"
            + "  Serial.begin(9600);\n" # 시리얼 통신은 기본으로 추가
            + arduino_setup_code 
            + "}\n\n"
            + "void loop() {\n"
            + arduino_loop_code 
            + "}\n"
        )

        # 2. 폴더 생성 및 파일 쓰기
        try:
            self.close()  # 연결이 열려있다면 닫기
            print("연결이 닫혔습니다. Closing connection...")

            os.makedirs(folder_name, exist_ok=True)
            full_path = os.path.join(".",folder_name, file_name)
            with open(full_path, 'w', encoding='utf-8') as f:
                f.write(full_code)
            print(f"'{full_path}' 파일 생성 완료!")
            """
            Upload the generated Arduino code to the board using arduino-cli.
            생성된 아두이노 코드를 arduino-cli를 사용하여 보드에 업로드합니다.
            """
            command: list[str] = [
                'arduino-cli', 'upload', 
                '--port', str(self.port),
                '--fqbn', 'arduino:avr:uno',  # 보드 유형을 지정합니다. (예: Arduino Uno)
                full_path
            ]
            print("업로드 명령어:", ' '.join(command))  # 업로드 명령어 출력
            print("코드 업로드 중... Uploading code...")
            try:
                # arduino-cli 명령어 실행
                result = subprocess.run(
                    command,
                    check=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                )
                print("코드 업로드 성공! Code upload successful!")
                print(result.stdout.decode('utf-8'))  # 업로드 성공 메시지 출력
            except subprocess.CalledProcessError as e:
                print(f"업로드 실패: {e}")
        except OSError as e:
            print(f"파일 생성 중 오류 발생: {e}")

    def log_arduino_code(self, code: str):
        """
        Log the Arduino code to the file.
        아두이노 코드를 파일에 기록합니다.
        """
        full_path = os.path.join(folder_name, file_name)
        with open(full_path, 'a') as f:
            f.write(code + '\n')
        print(f'Logged code to {file_name} 파일에 코드를 기록했습니다')
    
    def upload_firmware(self):
        """펌웨어를 올바른 구조의 임시 폴더에 복사한 뒤 업로드합니다."""
        
        # 실제 .ino 파일의 이름 (확장자 제외)
        sketch_name = f"bowCarForPython_V{firmware_version}"
        ino_filename = f"{sketch_name}.ino"

        try:
            # 1. 패키지 내의 .ino 파일을 가리키는 객체 가져오기
            traversable_path = importlib.resources.files("bowcar.firmware").joinpath(sketch_name, ino_filename)

            with importlib.resources.as_file(traversable_path) as concrete_path:
                # 2. 임시 디렉토리 생성
                with tempfile.TemporaryDirectory() as temp_dir:
                    # 3. .ino 파일과 이름이 같은 하위 폴더 생성
                    sketch_subdir = Path(temp_dir) / sketch_name
                    sketch_subdir.mkdir()
                    
                    # 4. .ino 파일을 이름이 같은 하위 폴더 안으로 복사
                    shutil.copy(concrete_path, sketch_subdir)
                    
                    # 5. arduino-cli에 '하위 폴더'의 경로를 전달
                    command = [
                        'arduino-cli', 'compile', '--upload', 
                        '--port', str(self.port),
                        '--fqbn', 'arduino:avr:uno',
                        str(sketch_subdir) # 최종 스케치 폴더 경로
                    ]

                    print("업로드 명령어 실행:", ' '.join(command))
                    
                    # ... (subprocess.run 호출 부분은 동일) ...
                    result = subprocess.run(command, check=True, capture_output=True, text=True, encoding='utf-8')
                    print("코드 업로드 성공!")
                    print(result.stdout)

        except (ModuleNotFoundError, FileNotFoundError):
            print("오류: 패키지 또는 펌웨어 파일을 찾을 수 없습니다.")
        except subprocess.CalledProcessError as e:
            print(f"업로드 실패: {e}\n{e.stderr}")
        except Exception as e:
            print(f"알 수 없는 오류가 발생했습니다: {e}")

    def _find_arduino_port(self):
        ports = serial.tools.list_ports.comports()
        for port in ports:
            if 'Arduino' in port.description or 'CH340' in port.description:
                return port.device
        print('No Arduino found! 아두이노를 찾을 수 없습니다.')
        return None
    
    def send_command(self, command: str):
        """
        Send a command to the bowcar.
        바우카에 명령을 전송합니다.
        """
        if self.connection and self.connection.is_open:
            full_command = command + '\n'
            self.connection.write(full_command.encode('utf-8'))
        else:
            print('Connection is not open! 연결 되지 않아 명령을 보낼 수 없습니다.')
    
    def close(self):
        """
        Close the serial connection.
        시리얼 연결을 닫습니다.
        """
        if self.connection:
            self.connection.close()
            print('Connection closed! 연결이 닫혔습니다.')
        else:
            print('No connection to close! 닫을 연결이 없습니다.')
        
    def red_on(self):
        global arduino_loop_code
        arduino_loop_code += "digitalWrite(RED_LED_PIN, HIGH);\n"
        self.send_command('lrn')

    def red_off(self):
        global arduino_loop_code
        arduino_loop_code += "digitalWrite(RED_LED_PIN, LOW);\n"
        self.send_command('lrf')

    def blue_on(self):
        global arduino_loop_code
        arduino_loop_code += "digitalWrite(BLUE_LED_PIN, HIGH);\n"
        self.send_command('lbn')

    def blue_off(self):
        global arduino_loop_code
        arduino_loop_code += "digitalWrite(BLUE_LED_PIN, LOW);\n"
        self.send_command('lbf')

    def all_light_on(self):
        global arduino_loop_code
        arduino_loop_code += "digitalWrite(RED_LED_PIN, HIGH);\n"
        arduino_loop_code += "digitalWrite(BLUE_LED_PIN, HIGH);\n"
        self.send_command('lan')

    def all_light_off(self):
        global arduino_loop_code
        arduino_loop_code += "digitalWrite(RED_LED_PIN, LOW);\n"
        arduino_loop_code += "digitalWrite(BLUE_LED_PIN, LOW);\n"
        self.send_command('laf')
    
    def buzzer_on(self, scale: str = "C0", octave: int = 3, note: int = 4):
        """
        Turn on the buzzer.
        버저를 켭니다.
        """
        global arduino_loop_code
        if octave < 1 or octave > 6:
            print("옥타브는 1에서 6 사이여야 합니다. Octave must be between 1 and 6.")
            return
        if scale[0] not in "CDEFGAB":
            print("음계는 C, D, E, F, G, A, B 중 하나여야 합니다. Scale must be one of C, D, E, F, G, A, B.")
            return
        if(scale == "C0"):
            idx = 0
        elif(scale == "C#"):
            idx = 1
        elif(scale == "D0"):
            idx = 2
        elif(scale == "D#"):
            idx = 3
        elif(scale == "E0"):
            idx = 4
        elif(scale == "F0"):
            idx = 5
        elif(scale == "F#"):
            idx = 6
        elif(scale == "G0"):
            idx = 7
        elif(scale == "G#"):
            idx = 8
        elif(scale == "A0"):
            idx = 9
        elif(scale == "A#"):
            idx = 10
        elif(scale == "B0"):
            idx = 11
        else:
            print("유효하지 않은 음계입니다. Invalid scale.")
            return

        arduino_loop_code += f"tone(BUZZER_PIN, {tones[octave][idx]}, {self.duration/note*0.95});\n"
        self.delay(self.duration // note)
        command = f'b{octave}{scale}{note}'
        self.send_command(command)

    def buzzer_off(self):
        """
        Turn off the buzzer.
        버저를 끕니다.
        """
        global arduino_loop_code
        arduino_loop_code += "noTone(BUZZER_PIN);\n"
        self.send_command('bnn')

    def delay(self, ms: int):
        """
        Delay for a specified number of milliseconds.
        지정된 밀리초 동안 지연합니다.
        """
        global arduino_loop_code
        arduino_loop_code += f"delay({ms});\n"
        time.sleep(ms/1000) # Python의 sleep은 초 단위이므로 ms를 1000으로 나눔
