import cv2
import numpy as np

class vimorse:
    '''
    Encode and decode a custom binary-morse-like format using black/white frames.

    This class contains two main utilities:
    1. bi2vid(bi): Convert a bimorse string into a sequence of black/white frames
       and store the result as a video file. Used for encoding.
    2. vid2bi(path): Read a video previously encoded with bi2vid() and reconstruct
       the original bimorse string. Used for decoding.
    '''

    @staticmethod
    def bi2vid(bi):
        '''
        Convert a bimorse string (consisting of '1', '0', and '.') into a video.

        Args:
            bi (str): The bimorse string to encode.
                      '1' -> specific pattern of black/white frames
                      '0' -> slightly longer pattern
                      '.' -> timing gaps represented by blocks of black frames

        Returns:
            None
            (Creates a file named "wb_video.mp4")

        Why this function exists:
            The goal is to serialize data into a visual signal, where timing and
            frame sequences represent bits. This lets you embed binary-like
            messages inside raw video streams.

        Example:
            vimorse.bi2vid("10..01")
        '''
        width, height = 640, 640
        fps = 120
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        out = cv2.VideoWriter("wb_video.mp4", fourcc, fps, (width, height))

        white = np.full((height, width, 3), 255, np.uint8)
        black = np.zeros((height, width, 3), np.uint8)

        i = 0
        l = len(bi)

        while i < l :
            ch = bi[i]

            if ch == '1':
                out.write(black); out.write(black)
                out.write(white)
                out.write(black); out.write(black)
                i += 1

            elif ch == '0':
                out.write(black); out.write(black); out.write(black)
                out.write(white)
                out.write(black)
                out.write(white)
                out.write(black); out.write(black); out.write(black)
                i += 1

            elif ch == '.':
                j = i
                while j < l and bi[j] == '.':
                    j += 1

                n = j - i
                for _ in range(8 * n):
                    out.write(black)
                i = j

            else:
                i += 1

        out.release()

    @staticmethod
    def vid2bi(path):
        '''
        Decode a video generated by bi2vid() back into a bimorse string.

        Args:
            path (str): Path to the encoded video file.

        Returns:
            str: The reconstructed bimorse string, including leading and trailing █ markers.

        How it works:
            • Every frame is reduced to a single 'b' (black) or 'w' (white) symbol.
            • Patterns of frames are matched against the known structure for '1' and '0'.
            • Remaining black-frame sequences that don’t match bit patterns are
              grouped into '.' characters.

        Example:
            decoded = vimorse.vid2bi("wb_video.mp4")
        '''
        cap = cv2.VideoCapture(path)
        if not cap.isOpened():
            raise Exception('Could not open the video file')

        frames = []
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            b, g, r = frame[0, 0]
            frames.append('w' if (b != 0 or g != 0 or r != 0) else 'b')

        cap.release()

        bi = ['█']
        i = 0
        l = len(frames)

        while i < l:
            # detect '1'
            if i + 4 < l and frames[i:i+5] == ['b','b','w','b','b']:
                bi.append('1')
                i += 5
                continue
            # detect '0'
            elif i + 8 < l and frames[i:i+9] == ['b','b','b','w','b','w','b','b','b']:
                bi.append('0')
                i += 9
                continue
            # leftover blacks -> dots
            elif frames[i] == 'b':
                count = 0
                while i < l and frames[i] == 'b':
                    # peek ahead to avoid breaking '1' or '0'
                    if i + 4 < l and frames[i:i+5] == ['b','b','w','b','b']:
                        break
                    if i + 8 < l and frames[i:i+9] == ['b','b','b','w','b','w','b','b','b']:
                        break
                    count += 1
                    i += 1
                if count > 0:
                    dots = (count + 7) // 8  # round up to ensure last frames counted
                    bi.append('.' * dots)
                continue
            else:
                i += 1

        bi.append('█')
        return ''.join(bi)
