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.1.5"

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

// LED control pins
const int RED_LED_PIN =10;
const int BLUE_LED_PIN =11;

// Ultrasonic sensor pins
const int TRIG_PIN =13;
const int ECHO_PIN =12;

// IR sensor pins
const int IRL_PIN =A6;
const int IRR_PIN =A7;

// Sound sensor pin
const int SS_PIN =A3;

// Buzzer pin
const int BUZZER_PIN =3;

// Light Sensor
const int LightS_PIN = A2;

// Motor control pins
const int LM_DIR_PIN =2;
const int LM_PWM_PIN =5;

const int RM_DIR_PIN =4;
const int RM_PWM_PIN =6;

// Button pin
const int UB_PIN =A0;
const int DB_PIN =A1;
const int LB_PIN =7;
const int RB_PIN =8;

'''

# 핀을 제외한 변수 선언
# Variable declarations excluding pins
arduino_variables = '''
// Variables for BowCar
// 바우카를 위한 변수들
int duration = 2000;
int up_button = 0; // 위쪽 버튼 상태
int down_button = 0; // 아래쪽 버튼 상태
int left_button = 0; // 왼쪽 버튼 상태
int right_button = 0; // 오른쪽 버튼 상태
int ir_left_value = 0; // 왼쪽 IR 센서 값
int ir_right_value = 0; // 오른쪽 IR 센서 값
int sound_sensor_value = 0; // 소리 센서 값
int distance = 0; // 거리 측정 값
'''

# 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(IRL_PIN, INPUT);
  pinMode(IRR_PIN, INPUT);
  pinMode(SS_PIN, INPUT);
  pinMode(LM_DIR_PIN, OUTPUT);
  pinMode(LM_PWM_PIN, OUTPUT);
  pinMode(RM_DIR_PIN, OUTPUT);
  pinMode(RM_PWM_PIN, OUTPUT);
  pinMode(UB_PIN, INPUT);
  pinMode(DB_PIN, INPUT);
  pinMode(LB_PIN, INPUT);
  pinMode(RB_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 ]
]

# 음계 매핑 딕셔너리
SCALE_MAPPING = {
    "C0": 0, "C#": 1, "D0": 2, "D#": 3, "E0": 4, "F0": 5, "F#": 6,
    "G0": 7, "G#": 8, "A0": 9, "A#": 10, "B0": 11
}

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 _add_arduino_code(self, code: str):
        """아두이노 루프 코드에 코드를 추가합니다."""
        global arduino_loop_code
        arduino_loop_code += code + "\n"

    def _send_command_and_wait(self, command: str, wait_time: float | None = None):
        """명령을 보내고 필요시 대기합니다."""
        self.send_command(command)
        if wait_time:
            time.sleep(wait_time)

    def _get_sensor_value(self, command: str) -> int:
        """센서 값을 가져오는 공통 메서드"""
        self.send_command(command)
        if self.connection:
            line = self.connection.readline()
            if line:
                return int(line.decode('utf-8').strip())
        return -1

    def _get_sensor_value_float(self, command: str) -> float:
        """센서 값을 float로 가져오는 공통 메서드"""
        self.send_command(command)
        if self.connection:
            line = self.connection.readline()
            if line:
                return float(line.decode('utf-8').strip())
        return -1

    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"
            + arduino_variables + "\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)
            if( os.path.exists(full_path)):
                os.remove(full_path)  # 기존 파일이 있다면 삭제
                time.sleep(1)  # 파일 삭제 후 잠시 대기
            with open(full_path, 'w', encoding='utf-8') as f:
                f.write(full_code)
            print(f"'{full_path}' 파일 생성 완료!")
            
            self._upload_arduino_code(full_path)
        except OSError as e:
            print(f"파일 생성 중 오류 발생: {e}")

    def _upload_arduino_code(self, full_path: str):
        """아두이노 코드를 업로드합니다."""
        compile_command: list[str] = [
            'arduino-cli', 'compile',
            '--fqbn', 'arduino:avr:uno',  # 보드 유형을 지정합니다. (예: Arduino Uno)
            full_path, '--clean'
        ]

        upload_command: list[str] = [
            'arduino-cli', 'upload', 
            '--port', str(self.port),
            '--fqbn', 'arduino:avr:uno',  # 보드 유형을 지정합니다. (예: Arduino Uno)
            full_path
        ]
        print("업로드 명령어:", ' '.join(upload_command))  # 업로드 명령어 출력
        print("코드 업로드 중... Uploading code...")
        try:
            # arduino-cli 명령어 실행
            result = subprocess.run(
                compile_command,
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            print(result.stdout.decode('utf-8'))  # 업로드 성공 메시지 출력
            result = subprocess.run(
                upload_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}")

    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 _led_control(self, led_pin: str, state: str, command: str):
        """LED 제어 공통 메서드"""
        self._add_arduino_code(f"  digitalWrite({led_pin}, {state});")
        self.send_command(command)

    def red_on(self):
        self._led_control("RED_LED_PIN", "HIGH", 'lrn')

    def red_off(self):
        self._led_control("RED_LED_PIN", "LOW", 'lrf')

    def blue_on(self):
        self._led_control("BLUE_LED_PIN", "HIGH", 'lbn')

    def blue_off(self):
        self._led_control("BLUE_LED_PIN", "LOW", 'lbf')

    def all_light_on(self):
        self._add_arduino_code("  digitalWrite(RED_LED_PIN, HIGH);")
        self._add_arduino_code("  digitalWrite(BLUE_LED_PIN, HIGH);")
        self.send_command('lan')

    def all_light_off(self):
        self._add_arduino_code("  digitalWrite(RED_LED_PIN, LOW);")
        self._add_arduino_code("  digitalWrite(BLUE_LED_PIN, LOW);")
        self.send_command('laf')
    
    def buzzer_on(self, scale: str = "C0", octave: int = 3, note: int = 4):
        """
        Turn on the buzzer.
        버저를 켭니다.
        """
        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 not in SCALE_MAPPING:
            print("유효하지 않은 음계입니다. Invalid scale.")
            return

        idx = SCALE_MAPPING[scale]
        self._add_arduino_code(f"  tone(BUZZER_PIN, {tones[octave-1][idx]}, {self.duration/note*0.95});")
        self._add_arduino_code(f"  delay({self.duration/note});")
        command = f'b{octave}{scale}{note}'
        self._send_command_and_wait(command, self.duration // note / 1000)

    def buzzer_off(self):
        """
        Turn off the buzzer.
        버저를 끕니다.
        """
        self._add_arduino_code("noTone(BUZZER_PIN);")
        self.send_command('bnn')

    def set_duration(self, time: int = 2000):
        """
        Set the duration for buzzer sound.
        버저 소리의 지속 시간을 설정합니다.
        """
        if time < 100 or time > 10000:
            print("지속 시간은 100ms에서 10000ms 사이여야 합니다. Duration must be between 100ms and 10000ms.")
            return
        self._add_arduino_code(f"  duration = {time};")
        self.duration = time
        command = f'sd{time:05d}'
        self.send_command(command)

    def set_speed(self, type : str = 'a', speed: int = 100):
        """
        Set the speed of the motors.
        모터의 속도를 설정합니다.
        """
        if type not in "lra" and len(type)>1:
            print("타입은 왼쪽(l), 오른쪽(r), 모두(a) 중 하나입니다.")
            return

        if speed < 0 or speed > 255:
            print("모터 속도는 0에서 255 사이여야 합니다. Motor speed must be between 0 and 255.")
            return
        
        if(type == 'l'):
            self._add_arduino_code(f"  analogWrite(LM_PWM_PIN,{speed});")
        elif(type == 'r'):
            self._add_arduino_code(f"  analogWrite(RM_PWM_PIN,{speed});")
        elif(type == 'a'):
            self._add_arduino_code(f"  analogWrite(LM_PWM_PIN,{speed});")
            self._add_arduino_code(f"  analogWrite(RM_PWM_PIN,{speed});")
       
        command = f'sm{type}{speed:03d}'
        self.send_command(command)

    def set_direction(self, type: str = 'a', dir: str = 'f'):
        """
        Set the direction of Motors.
        모터의 방향을 정합니다.
        """
        if type not in "lra" and len(type)>1:
            print("타입은 왼쪽(l), 오른쪽(r), 모두(a) 중 하나입니다.")
            return
        
        temp = '0' if dir == 'f' else '1'

        if(type == 'l'):
            self._add_arduino_code(f"  digitalWrite(LM_DIR_PIN,{temp});")
        elif(type == 'r'):
            self._add_arduino_code(f"  digitalWrite(RM_DIR_PIN,{temp});")
        else:
            self._add_arduino_code(f"  digitalWrite(LM_DIR_PIN,{temp});")
            self._add_arduino_code(f"  digitalWrite(RM_DIR_PIN,{temp});")
        
        command = 'sw' + type + temp;
        print(f'명령: "{command}"가 입력되었습니다.')
        self.send_command(command)

    def is_light(self,type:str='u',thresehold:int=500) -> int:
        """
        Check Light Value using Light Sensor in ITPLE board.
        잇플 보드에 있는 조도 센서를 이용해서 빛의 세기를 비교합니다.
        """
        command = "rl"+type+f'{thresehold}'
        return self._get_sensor_value(command)

    def get_light(self) -> int:
        """
        Get Light Value using Light Sensor in ITPLE board.
        잇플 보드에 있는 조도 센서 값을 가져옵니다.
        """
        return self._get_sensor_value("gl")

    def is_push(self, type:str='u') -> int:
        """
        Check Push Buttons in ITPLE board.
        잇플 보드에 있는 버튼을 눌렀는지 확인합니다.
        """
        command = "rb"+type
        return self._get_sensor_value(command)

    def get_button(self, type:str='u') -> int:
        """
        Get Buttons stat using buttons in ITPLE board.
        잇플 보드에 있는 버튼 값을 가져옵니다.
        """
        command = "gb"+type
        return self._get_sensor_value(command)

    def is_sound(self,type:str='u',thresehold:int=500) -> int:
        """
        Check Sound using Sound Sensor in ITPLE board.
        잇플 보드에 있는 사운드센서(마이크)를 이용해서 주변에서 큰 소리가 나는지 확인합니다.
        """
        command = "rs"+type+f'{thresehold}'
        return self._get_sensor_value(command)

    def get_sound(self) -> int:
        """
        Get Sound Value using Sound Sensor in ITPLE board.
        잇플 보드에 있는 사운드 센서 값을 가져옵니다.
        """
        return self._get_sensor_value("gs")

    def is_line(self,dir:str='l',type:str='u',thresehold:int=500) -> int:
        """
        Check Sound using IR Sensor in ITPLE board.
        잇플 보드에 있는 라인 트레이서를 이용해서 라인을 확인합니다.
        """
        command = "rt"+dir+type+f'{thresehold}'
        return self._get_sensor_value(command)

    def get_line(self, dir:str='l') -> int:
        """
        Get Value using Line Tracer in BOWCAR.
        바우카에 있는 라인 트레이서 값을 가져옵니다.
        """
        command = "gt"+dir
        return self._get_sensor_value(command)

    def distance(self,type:str = 'u', thresehold:int = 10) -> int:
        """
        Check Distance Using ultrasonic Sensor with BOWCAR.
        바우카에 연결한 초음파센서로 거리를 측정합니다.
        """
        command = "rd"+f'{thresehold}'
        return self._get_sensor_value(command)

    def get_distance(self) -> float:
        """
        Get Distance Using ultrasonic Sensor with BOWCAR.
        바우카에 연결한 초음파센서로 거리 값을 가져옵니다.
        """
        return self._get_sensor_value_float("gd")

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