import warnings
import threading
import subprocess
from PIL import Image
import RPi.GPIO as io
import cv2
import time

class _TV:
    def __init__(self):
        print("PiHomeTV is running")
        print("v1.0")
        io.setmode(io.BCM)
        io.setup(17, io.OUT)
        io.setup(27, io.OUT)
        io.setup(22, io.OUT)
        io.setup(4, io.OUT)
        io.setup(23, io.OUT)
        io.setup(18, io.IN)
        self.vidthread = None
        self.audiothread = None
        self.ScreenX = 720
        self.ScreenY = 576
        self.RFcolors = {
            1: (255, 255, 255),  # White
            2: (255, 255, 0),  # Yellow
            3: (0, 255, 255),  # Cyan
            4: (0, 255, 0),  # Green
            5: (255, 0, 255),  # Magenta
            6: (255, 0, 0),  # Red
            7: (0, 0, 255),  # Blue
            8: (0, 0, 0)  # Black
        }

    def _waitForColRQ(self):
        io.wait_for_edge(18, io.BOTH)

    def _writeTVcolor(self, colorI):
        color = self.RFcolors[colorI]
        pins = [17, 27, 22]  # R, G, B

        for i, component in enumerate(color):
            io.output(pins[i], component == 255)

    def _newTVLine(self):
        io.output(4, True)
        io.output(4, False)

    def _newTVFrame(self):
        io.output(23, True)
        io.output(23, False)

    def stop(self):
        if self.vidthread:
            self.vidthread.join()
        if self.audiothread:
            self.audiothread.join()

    def drawImage(self, imgFile):
        if self.vidthread:
            self.vidthread.join()
        if self.audiothread:
            self.audiothread.join()

        RFimage = {}

        try:
            with Image.open(imgFile) as v:
                imgRGB = v.convert('RGB')
                print("Converted to RGB...")

            original_width, original_height = imgRGB.size

            ratio = min(self.ScreenX / original_width, self.ScreenY / original_height)
            new_width = int(original_width * ratio)
            new_height = int(original_height * ratio)

            img_resized = imgRGB.resize((new_width, new_height), Image.Resampling.LANCZOS)
            img = Image.new('RGB', (self.ScreenX, self.ScreenY), (0, 0, 0))

            x_offset = (self.ScreenX - new_width) // 2
            y_offset = (self.ScreenY - new_height) // 2
            img.paste(img_resized, (x_offset, y_offset))

            width, height = img.size

            print(f"Resized to {width}x{height}...")

            for y in range(height):
                for x in range(width):
                    r, g, b = img.getpixel((x, y))

                    min_distance = float('inf')
                    closest_color_index = 1

                    for index, (rf_r, rf_g, rf_b) in self.RFcolors.items():
                        distance = ((r - rf_r) ** 2 + (g - rf_g) ** 2 + (b - rf_b) ** 2)

                        if distance < min_distance:
                            min_distance = distance
                            closest_color_index = index

                    RFimage[(x, y)] = closest_color_index
        except FileNotFoundError:
            warnings.warn("Cannot find selected file")
            return
        except Exception as e:
            warnings.warn(f"Unexpected error: {e}")
            return

        print(f"Decoded {width * height} pixels of Image...")

        def thread():
            while True:
                for y in range(height):
                    for x in range(width):
                        self._waitForColRQ()
                        colorI = RFimage[(x, y)]
                        self._writeTVcolor(colorI)
                    self._newTVLine()
                self._newTVFrame()

        self.vidthread = threading.Thread(target=thread)
        self.vidthread.start()

    def play(self, vidFile, loop=False, fps=20):
        if self.vidthread:
            self.vidthread.join()
        if self.audiothread:
            self.audiothread.join()

        cap = cv2.VideoCapture(vidFile)

        def getFrame(ms):
            cap.set(cv2.CAP_PROP_POS_MSEC, ms)
            success, frame = cap.read()

            if success:
                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                imgRGB = Image.fromarray(frame_rgb)

                original_width, original_height = imgRGB.size
                ratio = min(self.ScreenX / original_width, self.ScreenY / original_height)
                new_width = int(original_width * ratio)
                new_height = int(original_height * ratio)

                img_resized = imgRGB.resize((new_width, new_height), Image.Resampling.LANCZOS)
                img = Image.new('RGB', (self.ScreenX, self.ScreenY), (0, 0, 0))

                x_offset = (self.ScreenX - new_width) // 2
                y_offset = (self.ScreenY - new_height) // 2
                img.paste(img_resized, (x_offset, y_offset))

                return img
            else:
                return None

        RFVideo = {}
        framenum = 0
        frameTOffset = 1000 / fps
        print("Decoding Video...")
        while True:
            img = getFrame(framenum * frameTOffset)
            if img is None:
                break

            RFVideo[framenum] = {}

            for y in range(self.ScreenY):
                for x in range(self.ScreenX):
                    r, g, b = img.getpixel((x, y))

                    min_distance = float('inf')
                    closest_color_index = 1

                    for index, (rf_r, rf_g, rf_b) in self.RFcolors.items():
                        distance = ((r - rf_r) ** 2 + (g - rf_g) ** 2 + (b - rf_b) ** 2)

                        if distance < min_distance:
                            min_distance = distance
                            closest_color_index = index

                    RFVideo[framenum][(x, y)] = closest_color_index
            print(f"Decoded {framenum} frames of Video...")

            framenum += 1

        cap.release()
        print(f"Decoded {framenum} frames of Video...")

        def videothread():
            start_time = time.time() * 1000

            while True:
                for frame in range(framenum):
                    frame_start_time = start_time + (frame * frameTOffset)
                    current_time = time.time() * 1000

                    if current_time < frame_start_time:
                        time_to_wait = (frame_start_time - current_time) / 1000.0
                        time.sleep(time_to_wait)

                    RFimage = RFVideo[frame]
                    for y in range(self.ScreenY):
                        for x in range(self.ScreenX):
                            self._waitForColRQ()
                            colorI = RFimage[(x, y)]
                            self._writeTVcolor(colorI)
                        self._newTVLine()
                    self._newTVFrame()

                if not loop:
                    break
                else:
                    start_time = time.time() * 1000

        def audiothread():
            cmd = [
                'ffmpeg',
                '-i', vidFile,
                '-f', 'alsa',
                '-ac', '2',
                '-ar', '44100',
                'default'
            ]
            subprocess.run(cmd)

        self.vidthread = threading.Thread(target=videothread)
        self.vidthread.start()
        self.audiothread = threading.Thread(target=audiothread)
        self.audiothread.start()

    def fill(self, colorI=8):
        if self.vidthread:
            self.vidthread.join()
        if self.audiothread:
            self.audiothread.join()

        def thread():
            while True:
                for y in range(self.ScreenY):
                    for x in range(self.ScreenX):
                        self._waitForColRQ()
                        self._writeTVcolor(colorI)
                    self._newTVLine()
                self._newTVFrame()

        self.vidthread = threading.Thread(target=thread)
        self.vidthread.start()

    def setResolution(self, x, y):
        if x > 720:
            x = 720
            warnings.warn("Too High Resolution!")

        if y > 576:
            y = 576
            warnings.warn("Too High Resolution!")

        if x < 144:
            x = 144
            warnings.warn("Too Low Resolution!")

        if y < 288:
            y = 288
            warnings.warn("Too Low Resolution!")

        self.ScreenX = x
        self.ScreenY = y
        print(f"Set resolution to {x}x{y}")


def tv():
    return _TV()