"""
vidformer.cv2 is the cv2 frontend for [vidformer](https://github.com/ixlab/vidformer).

**Quick links:**
* [📦 PyPI](https://pypi.org/project/vidformer/)
* [📘 Documentation - vidformer-py](https://ixlab.github.io/vidformer/vidformer-py/)
* [📘 Documentation - vidformer.cv2](https://ixlab.github.io/vidformer/vidformer-py/vidformer/cv2.html)
* [📘 Documentation - vidformer.supervision](https://ixlab.github.io/vidformer/vidformer-py/vidformer/supervision.html)
* [🧑‍💻 Source Code](https://github.com/ixlab/vidformer/tree/main/vidformer-py/)
"""

import vidformer as vf

try:
    import cv2 as _opencv2
except Exception:
    _opencv2 = None

import re
import zlib
from bisect import bisect_right
from fractions import Fraction
from urllib.parse import unquote
import os

import numpy as np

CAP_PROP_POS_MSEC = 0
CAP_PROP_POS_FRAMES = 1
CAP_PROP_FRAME_WIDTH = 3
CAP_PROP_FRAME_HEIGHT = 4
CAP_PROP_FPS = 5
CAP_PROP_FRAME_COUNT = 7

FONT_HERSHEY_SIMPLEX = 0
FONT_HERSHEY_PLAIN = 1
FONT_HERSHEY_DUPLEX = 2
FONT_HERSHEY_COMPLEX = 3
FONT_HERSHEY_TRIPLEX = 4
FONT_HERSHEY_COMPLEX_SMALL = 5
FONT_HERSHEY_SCRIPT_SIMPLEX = 6
FONT_HERSHEY_SCRIPT_COMPLEX = 7
FONT_ITALIC = 16

FILLED = -1
LINE_4 = 4
LINE_8 = 8
LINE_AA = 16

MARKER_CROSS = 0
MARKER_TILTED_CROSS = 1
MARKER_STAR = 2
MARKER_DIAMOND = 3
MARKER_SQUARE = 4
MARKER_TRIANGLE_UP = 5
MARKER_TRIANGLE_DOWN = 6

INTER_NEAREST = 0
INTER_LINEAR = 1
INTER_CUBIC = 2
INTER_AREA = 3
INTER_LANCOZOS4 = 4
INTER_LINEAR_EXACT = 5
INTER_NEAREST_EXACT = 6
INTER_MAX = 7

# Rotation flags
ROTATE_90_CLOCKWISE = 0
ROTATE_180 = 1
ROTATE_90_COUNTERCLOCKWISE = 2

# Border types
BORDER_CONSTANT = 0
BORDER_REPLICATE = 1
BORDER_REFLECT = 2
BORDER_WRAP = 3
BORDER_REFLECT_101 = 4
BORDER_TRANSPARENT = 5
BORDER_REFLECT101 = BORDER_REFLECT_101
BORDER_DEFAULT = BORDER_REFLECT_101
BORDER_ISOLATED = 16

_inline_mat = vf.Filter("_inline_mat")
_slice_mat = vf.Filter("_slice_mat")
_slice_write_mat = vf.Filter("_slice_write_mat")
_black = vf.Filter("_black")
_solid = vf.Filter("_solid")


_filter_scale = vf.Filter("Scale")
_filter_rectangle = vf.Filter("cv2.rectangle")
_filter_putText = vf.Filter("cv2.putText")
_filter_arrowedLine = vf.Filter("cv2.arrowedLine")
_filter_line = vf.Filter("cv2.line")
_filter_circle = vf.Filter("cv2.circle")
_filter_addWeighted = vf.Filter("cv2.addWeighted")
_filter_ellipse = vf.Filter("cv2.ellipse")
_filter_polylines = vf.Filter("cv2.polylines")
_filter_fillPoly = vf.Filter("cv2.fillPoly")
_filter_fillConvexPoly = vf.Filter("cv2.fillConvexPoly")
_filter_drawContours = vf.Filter("cv2.drawContours")
_filter_drawMarker = vf.Filter("cv2.drawMarker")
_set_to = vf.Filter("cv2.setTo")
_filter_flip = vf.Filter("cv2.flip")
_filter_rotate = vf.Filter("cv2.rotate")
_filter_copyMakeBorder = vf.Filter("cv2.copyMakeBorder")
_filter_hconcat = vf.Filter("cv2.hconcat")
_filter_vconcat = vf.Filter("cv2.vconcat")


def _ts_to_fps(timestamps):
    if len(timestamps) < 2:
        return 0
    fps = Fraction(len(timestamps), timestamps[-1] - timestamps[0])
    if fps.denominator == 1:
        return fps.numerator
    return float(fps)


def _fps_to_ts(fps, n_frames):
    assert type(fps) is int
    return [Fraction(i, fps) for i in range(n_frames)]


_global_cv2_server = None


def _server():
    global _global_cv2_server
    if _global_cv2_server is None:
        if "VF_IGNI_ENDPOINT" in os.environ:
            server_endpoint = os.environ["VF_IGNI_ENDPOINT"]
            if "VF_IGNI_API_KEY" not in os.environ:
                raise Exception("VF_IGNI_API_KEY must be set")
            api_key = os.environ["VF_IGNI_API_KEY"]
            _global_cv2_server = vf.Server(server_endpoint, api_key)
        else:
            raise Exception(
                "No server set for the cv2 frontend (https://ixlab.github.io/vidformer/docs/install.html). Set VF_IGNI_ENDPOINT and VF_IGNI_API_KEY environment variables or use cv2.set_server() before use."
            )
    return _global_cv2_server


def set_server(server):
    """Set the server to use for the cv2 frontend."""
    global _global_cv2_server
    assert isinstance(server, vf.Server)
    _global_cv2_server = server


def get_server():
    """Get the server used by the cv2 frontend."""
    return _server()


_PIX_FMT_MAP = {
    "rgb24": "rgb24",
    "yuv420p": "rgb24",
    "yuv422p": "rgb24",
    "yuv422p10le": "rgb24",
    "yuv444p": "rgb24",
    "yuvj420p": "rgb24",
    "yuvj422p": "rgb24",
    "yuvj444p": "rgb24",
    "gray": "gray",
}


def _top_level_pix_fmt(pix_fmt):
    if pix_fmt in _PIX_FMT_MAP:
        return _PIX_FMT_MAP[pix_fmt]
    raise Exception(f"Unsupported pix_fmt {pix_fmt}")


class Frame:
    def __init__(self, f, fmt, parent=None, slice_bounds=None):
        self._f_internal = f
        self._fmt = fmt
        channels = 3 if _top_level_pix_fmt(fmt["pix_fmt"]) == "rgb24" else 1
        self.shape = (fmt["height"], fmt["width"], channels)

        # denotes that the frame has not yet been modified
        # when a frame is modified, it is converted to rgb24 first
        self._modified = False

        # For slice views: track parent frame and bounds for write-back
        self._parent = parent
        self._slice_bounds = slice_bounds  # (miny, maxy, minx, maxx)

    @property
    def _f(self):
        return self._f_internal

    @_f.setter
    def _f(self, value):
        self._f_internal = value
        # If this is a slice view, propagate changes back to parent
        if self._parent is not None:
            miny, maxy, minx, maxx = self._slice_bounds
            self._parent._mut()
            self._parent._f = _slice_write_mat(
                self._parent._f, self._f_internal, miny, maxy, minx, maxx
            )

    def _mut(self):
        if self._modified:
            assert self._fmt["pix_fmt"] in ["rgb24", "gray"]
            return

        self._modified = True
        if (
            self._fmt["pix_fmt"] != "rgb24"
            and _top_level_pix_fmt(self._fmt["pix_fmt"]) == "rgb24"
        ):
            self._f = _filter_scale(self._f, pix_fmt="rgb24")
            self._fmt["pix_fmt"] = "rgb24"
        elif (
            self._fmt["pix_fmt"] != "gray"
            and _top_level_pix_fmt(self._fmt["pix_fmt"]) == "gray"
        ):
            self._f = _filter_scale(self._f, pix_fmt="gray")
            self._fmt["pix_fmt"] = "gray"

    def copy(self):
        # Copy creates an independent frame (no parent reference)
        return Frame(self._f, self._fmt.copy())

    def numpy(self):
        """
        Return the frame as a numpy array.
        """

        self._mut()
        server = _server()
        frame = server.frame(
            self.shape[1], self.shape[0], self._fmt["pix_fmt"], self._f
        )
        assert type(frame) is bytes
        assert len(frame) == self.shape[0] * self.shape[1] * self.shape[2]
        raw_data_array = np.frombuffer(frame, dtype=np.uint8)
        frame = raw_data_array.reshape(self.shape)
        if self.shape[2] == 3:
            frame = frame[:, :, ::-1]  # convert RGB to BGR
        return frame

    def __getitem__(self, key):
        if not isinstance(key, tuple):
            raise NotImplementedError("Only 2D slicing is supported")

        if len(key) != 2:
            raise NotImplementedError("Only 2D slicing is supported")

        if not all(isinstance(x, slice) for x in key):
            raise NotImplementedError("Only 2D slicing is supported")

        miny = key[0].start if key[0].start is not None else 0
        maxy = key[0].stop if key[0].stop is not None else self.shape[0]
        minx = key[1].start if key[1].start is not None else 0
        maxx = key[1].stop if key[1].stop is not None else self.shape[1]

        # handle negative indices
        if miny < 0:
            miny = self.shape[0] + miny
        if maxy < 0:
            maxy = self.shape[0] + maxy
        if minx < 0:
            minx = self.shape[1] + minx
        if maxx < 0:
            maxx = self.shape[1] + maxx

        if (
            maxy <= miny
            or maxx <= minx
            or miny < 0
            or minx < 0
            or maxy > self.shape[0]
            or maxx > self.shape[1]
        ):
            raise NotImplementedError("Invalid slice")

        f = _slice_mat(self._f, miny, maxy, minx, maxx)
        fmt = self._fmt.copy()
        fmt["width"] = maxx - minx
        fmt["height"] = maxy - miny
        # Create slice with parent reference for write-back propagation
        return Frame(f, fmt, parent=self, slice_bounds=(miny, maxy, minx, maxx))

    def __setitem__(self, key, value):
        if type(key) is tuple:
            if len(key) != 2:
                raise NotImplementedError("Only 2D slicing is supported")

            if not all(isinstance(x, slice) for x in key):
                raise NotImplementedError("Only 2D slicing is supported")

            miny = key[0].start if key[0].start is not None else 0
            maxy = key[0].stop if key[0].stop is not None else self.shape[0]
            minx = key[1].start if key[1].start is not None else 0
            maxx = key[1].stop if key[1].stop is not None else self.shape[1]

            # handle negative indices
            if miny < 0:
                miny = self.shape[0] + miny
            if maxy < 0:
                maxy = self.shape[0] + maxy
            if minx < 0:
                minx = self.shape[1] + minx
            if maxx < 0:
                maxx = self.shape[1] + maxx

            if (
                maxy <= miny
                or maxx <= minx
                or miny < 0
                or minx < 0
                or maxy > self.shape[0]
                or maxx > self.shape[1]
            ):
                raise NotImplementedError("Invalid slice")

            # Check if value is a color tuple/list or scalar (solid fill)
            if isinstance(value, (int, float)):
                # Scalar value - treat as grayscale
                height = maxy - miny
                width = maxx - minx
                v = int(value)
                value = solid((height, width, self.shape[2]), (v, v, v))
            elif isinstance(value, (tuple, list)) and len(value) in [3, 4]:
                # Create a solid color frame of the right size
                height = maxy - miny
                width = maxx - minx
                value = solid((height, width, self.shape[2]), value[:3])
            else:
                value = frameify(value, "value")

            if value.shape[0] != maxy - miny or value.shape[1] != maxx - minx:
                raise NotImplementedError("Shape mismatch")

            self._mut()
            value._mut()

            self._f = _slice_write_mat(self._f, value._f, miny, maxy, minx, maxx)
        elif type(key) is Frame or type(key) is np.ndarray:
            key = frameify(key, "key")

            if key.shape[0] != self.shape[0] or key.shape[1] != self.shape[1]:
                raise NotImplementedError("Shape mismatch")

            if key.shape[2] != 1:
                raise NotImplementedError("Only 1-channel mask frames are supported")

            # Value should be a bgr or bgra color
            if (type(value) is not list and type(value) is not tuple) or len(
                value
            ) not in [3, 4]:
                raise NotImplementedError(
                    "Value should be a 3 or 4 element list or tuple"
                )
            value = [float(x) for x in value]
            if len(value) == 3:
                value.append(255.0)

            self._mut()
            key._mut()

            self._f = _set_to(self._f, value, key._f)
        else:
            raise NotImplementedError(
                "__setitem__ only supports slicing by a 2d tuple or a mask frame"
            )


def _inline_frame(arr):
    if arr.dtype != np.uint8:
        raise Exception("Only uint8 arrays are supported")
    if len(arr.shape) != 3:
        raise Exception("Only 3D arrays are supported")
    if arr.shape[2] != 3:
        raise Exception("To inline a frame, the array must have 3 channels")

    arr = arr[:, :, ::-1]
    if not arr.flags["C_CONTIGUOUS"]:
        arr = np.ascontiguousarray(arr)

    width = arr.shape[1]
    height = arr.shape[0]
    pix_fmt = "rgb24"

    data_gzip = zlib.compress(memoryview(arr), level=1)

    f = _inline_mat(
        data_gzip, width=width, height=height, pix_fmt=pix_fmt, compression="zlib"
    )
    fmt = {"width": width, "height": height, "pix_fmt": pix_fmt}

    # Return the resulting Frame object
    return Frame(f, fmt)


def _check_opencv2(method_name):
    if _opencv2 is None:
        raise NotImplementedError(
            f"{method_name} requires Python OpenCV cv2. Either it's not installed or the import failed (such as a missing libGL.so.1)."
        )


class VideoCapture:
    def __init__(self, path: str):
        server = _server()
        if type(path) is str:
            match = re.match(r"(http|https)://([^/]+)(.*)", path)
            if match is not None:
                endpoint = f"{match.group(1)}://{match.group(2)}"
                path = unquote(match.group(3))
                if path.startswith("/"):
                    path = path[1:]
                self._path = path
                self._source = server.source(path, 0, "http", {"endpoint": endpoint})
            else:
                self._path = path
                self._source = server.source(path, 0, "fs", {"root": "."})
        elif isinstance(path, vf.Source):
            assert isinstance(server, vf.Server)
            self._path = path._name
            self._source = path
        self._next_frame_idx = 0

    def isOpened(self) -> bool:
        return True

    def get(self, prop):
        if prop == CAP_PROP_FPS:
            return _ts_to_fps(self._source.ts())
        elif prop == CAP_PROP_FRAME_WIDTH:
            return self._source.fmt()["width"]
        elif prop == CAP_PROP_FRAME_HEIGHT:
            return self._source.fmt()["height"]
        elif prop == CAP_PROP_FRAME_COUNT:
            return len(self._source)
        elif prop == CAP_PROP_POS_FRAMES:
            return self._next_frame_idx
        elif prop == CAP_PROP_POS_MSEC:
            ts = self._source.ts()
            if self._next_frame_idx >= len(ts):
                # Past the end, return the last timestamp
                if len(ts) > 0:
                    return float(ts[-1] * 1000)
                return 0.0
            return float(ts[self._next_frame_idx] * 1000)

        raise Exception(f"Unknown property {prop}")

    def set(self, prop, value):
        if prop == CAP_PROP_POS_FRAMES:
            assert value >= 0 and value < len(self._source.ts())
            self._next_frame_idx = value
        elif prop == CAP_PROP_POS_MSEC:
            t = Fraction(int(value), 1000)
            ts = self._source.ts()
            next_frame_idx = bisect_right(ts, t)
            self._next_frame_idx = next_frame_idx
        else:
            raise Exception(f"Unsupported property {prop}")

    def read(self):
        if self._next_frame_idx >= len(self._source):
            return False, None
        frame = self._source.iloc[self._next_frame_idx]
        self._next_frame_idx += 1
        frame = Frame(frame, self._source.fmt())
        return True, frame

    def __getitem__(self, key):
        if not isinstance(key, int):
            raise NotImplementedError("Only integer indexing is supported")
        if key < 0:
            key = len(self._source) + key
        if key < 0 or key >= len(self._source):
            raise IndexError("Index out of bounds")
        frame = self._source.iloc[key]
        frame = Frame(frame, self._source.fmt())
        return frame

    def __len__(self):
        return len(self._source)

    def release(self):
        pass


class VideoWriter:
    def __init__(
        self,
        path,
        fourcc,
        fps,
        size,
        batch_size=1024,
        initial_batch_size=100,
        compression="gzip",
        ttl=3600,
        pix_fmt="yuv420p",
        vod_segment_length=Fraction(2, 1),
    ):
        server = _server()
        assert isinstance(server, vf.Server)
        assert path is None or type(path) is str
        if path is not None and server.is_vod_only():
            path = None

        self._path = path
        if isinstance(fps, int):
            self._f_time = Fraction(1, fps)
        elif isinstance(fps, Fraction):
            self._f_time = 1 / fps
        elif isinstance(fps, float):
            # 29.97
            if abs(fps - 30000 / 1001) < 1e-6:
                self._f_time = Fraction(1001, 30000)
            # 23.976
            elif abs(fps - 24000 / 1001) < 1e-6:
                self._f_time = Fraction(1001, 24000)
            # 59.94
            elif abs(fps - 60000 / 1001) < 1e-6:
                self._f_time = Fraction(1001, 60000)
            else:
                # Round to nearest integer fps
                self._f_time = Fraction(1, int(round(fps)))
        else:
            raise Exception("fps must be an integer, float, or Fraction")

        assert isinstance(size, tuple) or isinstance(size, list)
        assert len(size) == 2
        width, height = size
        assert ttl is None or isinstance(ttl, int)
        self._spec = server.create_spec(
            width, height, pix_fmt, vod_segment_length, 1 / self._f_time, ttl=ttl
        )
        self._batch_size = batch_size
        self._initial_batch_size = initial_batch_size
        assert compression is None or compression in ["gzip"]
        self._compression = compression
        self._idx = 0
        self._feb = vf._FrameExpressionBlock()
        self._first_push_done = False

        # writer_init_callback
        if server.cv2_writer_init_callback() is not None:
            server.cv2_writer_init_callback()(self)

    def _flush(self, terminal=False):
        server = _server()
        if len(self._feb) > 0:
            server.push_spec_part_block(
                self._spec,
                self._idx - len(self._feb),
                [self._feb],
                terminal=terminal,
                compression=self._compression,
            )
            self._feb = vf._FrameExpressionBlock()
        else:
            server.push_spec_part_block(
                self._spec,
                self._idx - len(self._feb),
                [],
                terminal=terminal,
            )

        # First push callback (called once after first successful push)
        if not self._first_push_done:
            self._first_push_done = True
            if server.cv2_writer_first_push_callback() is not None:
                server.cv2_writer_first_push_callback()(self)

    def spec(self):
        return self._spec

    def write(self, frame):
        if frame is not None:
            frame = frameify(frame, "frame")
            if frame._fmt["width"] != self._spec._fmt["width"]:
                raise Exception(
                    f"Frame type error; expected width {self._spec._fmt['width']}, got {frame._fmt['width']}"
                )
            if frame._fmt["height"] != self._spec._fmt["height"]:
                raise Exception(
                    f"Frame type error; expected height {self._spec._fmt['height']}, got {frame._fmt['height']}"
                )
            if frame._fmt["pix_fmt"] != self._spec._fmt["pix_fmt"]:
                f_obj = _filter_scale(frame._f, pix_fmt=self._spec._fmt["pix_fmt"])
                frame = Frame(f_obj, self._spec._fmt)
        self._feb.insert_frame(frame._f if frame is not None else None)
        self._idx += 1

        # Use smaller initial batch for faster first response
        threshold = (
            self._initial_batch_size if not self._first_push_done else self._batch_size
        )
        if len(self._feb) >= threshold:
            self._flush()

    def isOpened(self):
        return True

    def release(self):
        self._flush(True)
        if self._path is not None:
            server = _server()
            server.export_spec(self._spec.id(), self._path)


class VideoWriter_fourcc:
    def __init__(self, *args):
        self._args = args


def frameify(obj, field_name=None):
    """
    Turn an object (e.g., ndarray) into a Frame.
    """

    if isinstance(obj, Frame):
        return obj
    elif isinstance(obj, np.ndarray):
        return _inline_frame(obj)
    else:
        if field_name is not None:
            raise Exception(
                f"Unsupported type for field {field_name}, expected Frame or np.ndarray"
            )
        else:
            raise Exception("Unsupported type, expected Frame or np.ndarray")


def imread(path, *args):
    if len(args) > 0:
        raise NotImplementedError("imread does not support additional arguments")
    assert path.lower().endswith((".jpg", ".jpeg", ".png"))
    server = _server()

    cap = VideoCapture(path)
    assert cap.isOpened()
    assert len(cap._source) == 1
    ret, frame = cap.read()
    assert ret
    cap.release()
    return frame


def imwrite(path, img, *args):
    if len(args) > 0:
        raise NotImplementedError("imwrite does not support additional arguments")

    img = frameify(img)
    fmt = img._fmt.copy()
    width = fmt["width"]
    height = fmt["height"]

    if path.lower().endswith(".png"):
        out_pix_fmt = "rgb24"
        encoder = "png"
    elif path.lower().endswith((".jpg", ".jpeg")):
        encoder = "mjpeg"
        if img._fmt["pix_fmt"] not in ["yuvj420p", "yuvj422p", "yuvj444p"]:
            out_pix_fmt = "yuvj420p"
        else:
            out_pix_fmt = img._fmt["pix_fmt"]
    else:
        raise Exception("Unsupported image format")

    if img._fmt["pix_fmt"] != out_pix_fmt:
        f = _filter_scale(img._f, pix_fmt=out_pix_fmt)
        img = Frame(f, {"width": width, "height": height, "pix_fmt": out_pix_fmt})

    writer = VideoWriter(None, None, 1, (width, height), pix_fmt=out_pix_fmt)
    writer.write(img)
    writer.release()

    spec = writer.spec()
    server = _server()
    server.export_spec(spec.id(), path, encoder=encoder)


def vidplay(video, method="display"):
    """
    Play a vidformer video specification.
    """
    if isinstance(video, VideoWriter):
        return video.spec().play(method=method)
    elif isinstance(video, vf.Spec):
        return video.play(method=method)
    else:
        raise Exception("Unsupported video type to vidplay")


def zeros(shape, dtype=np.uint8):
    """
    Create a black frame. Mimics numpy.zeros.
    """
    assert isinstance(shape, tuple) or isinstance(shape, list)
    assert len(shape) == 3
    assert shape[2] in [1, 3]
    assert dtype == np.uint8

    height, width, channels = shape
    if channels == 1:
        pix_fmt = "gray"
    else:
        pix_fmt = "rgb24"

    f = _black(width=width, height=height, pix_fmt=pix_fmt)
    fmt = {"width": width, "height": height, "pix_fmt": pix_fmt}
    return Frame(f, fmt)


def solid(shape, color, dtype=np.uint8):
    """
    Create a solid color frame.

    Parameters:
        shape: (height, width, channels) tuple
        color: (B, G, R) tuple for BGR color (OpenCV convention)
        dtype: must be np.uint8
    """
    assert isinstance(shape, tuple) or isinstance(shape, list)
    assert len(shape) == 3
    assert shape[2] in [1, 3]
    assert dtype == np.uint8

    height, width, channels = shape
    if channels == 1:
        pix_fmt = "gray"
    else:
        pix_fmt = "rgb24"

    # Convert BGR to RGB for internal storage
    if len(color) == 3:
        r, g, b = color[2], color[1], color[0]
    else:
        r, g, b = color[0], color[1], color[2]

    f = _solid(width=width, height=height, pix_fmt=pix_fmt, color=[r, g, b])
    fmt = {"width": width, "height": height, "pix_fmt": pix_fmt}
    return Frame(f, fmt)


def resize(src, dsize, interpolation=None):
    src = frameify(src)
    src._mut()

    assert isinstance(dsize, tuple) or isinstance(dsize, list)
    assert len(dsize) == 2
    width, height = dsize

    # TODO: We don't do anything with interpolation yet
    assert interpolation is None or (
        interpolation >= INTER_NEAREST and interpolation <= INTER_MAX
    )

    f = _filter_scale(src._f, width=width, height=height)
    fmt = {"width": width, "height": height, "pix_fmt": src._fmt["pix_fmt"]}
    return Frame(f, fmt)


def rectangle(img, pt1, pt2, color, thickness=None, lineType=None, shift=None):
    """
    cv.rectangle(	img, pt1, pt2, color[, thickness[, lineType[, shift]]]	)
    """

    img = frameify(img)
    img._mut()

    assert len(pt1) == 2
    assert len(pt2) == 2
    pt1 = [int(x) for x in pt1]
    pt2 = [int(x) for x in pt2]

    assert len(color) == 3 or len(color) == 4
    color = [float(x) for x in color]
    if len(color) == 3:
        color.append(255.0)

    if shift is not None and lineType is None:
        lineType = LINE_8
    if lineType is not None and thickness is None:
        thickness = 1

    args = []
    if thickness is not None:
        assert isinstance(thickness, int)
        args.append(thickness)
    if lineType is not None:
        assert isinstance(lineType, int)
        args.append(lineType)
    if shift is not None:
        assert isinstance(shift, int)
        args.append(shift)

    img._f = _filter_rectangle(img._f, pt1, pt2, color, *args)
    return img


def putText(
    img,
    text,
    org,
    fontFace,
    fontScale,
    color,
    thickness=None,
    lineType=None,
    bottomLeftOrigin=None,
):
    """
    cv.putText(	img, text, org, fontFace, fontScale, color[, thickness[, lineType[, bottomLeftOrigin]]]	)
    """

    img = frameify(img)
    img._mut()

    assert isinstance(text, str)

    assert len(org) == 2
    org = [int(x) for x in org]

    assert isinstance(fontFace, int)
    assert isinstance(fontScale, float) or isinstance(fontScale, int)
    fontScale = float(fontScale)

    assert len(color) == 3 or len(color) == 4
    color = [float(x) for x in color]
    if len(color) == 3:
        color.append(255.0)

    if bottomLeftOrigin is not None and lineType is None:
        lineType = LINE_8
    if lineType is not None and thickness is None:
        thickness = 1

    args = []
    if thickness is not None:
        assert isinstance(thickness, int)
        args.append(thickness)
    if lineType is not None:
        assert isinstance(lineType, int)
        args.append(lineType)
    if bottomLeftOrigin is not None:
        assert isinstance(bottomLeftOrigin, bool)
        args.append(bottomLeftOrigin)

    img._f = _filter_putText(img._f, text, org, fontFace, fontScale, color, *args)
    return img


def arrowedLine(
    img, pt1, pt2, color, thickness=None, line_type=None, shift=None, tipLength=None
):
    """
    cv.arrowedLine(	img, pt1, pt2, color[, thickness[, line_type[, shift[, tipLength]]]]	)
    """
    img = frameify(img)
    img._mut()

    assert len(pt1) == 2
    assert len(pt2) == 2
    assert all(isinstance(x, int) for x in pt1)
    assert all(isinstance(x, int) for x in pt2)

    assert len(color) == 3 or len(color) == 4
    color = [float(x) for x in color]
    if len(color) == 3:
        color.append(255.0)

    if tipLength is not None and shift is None:
        shift = 0
    if shift is not None and line_type is None:
        line_type = LINE_8
    if line_type is not None and thickness is None:
        thickness = 1

    args = []
    if thickness is not None:
        assert isinstance(thickness, int)
        args.append(thickness)
    if line_type is not None:
        assert isinstance(line_type, int)
        args.append(line_type)
    if shift is not None:
        assert isinstance(shift, int)
        args.append(shift)
    if tipLength is not None:
        assert isinstance(tipLength, (int, float))
        args.append(float(tipLength))

    img._f = _filter_arrowedLine(img._f, pt1, pt2, color, *args)
    return img


def line(img, pt1, pt2, color, thickness=None, lineType=None, shift=None):
    img = frameify(img)
    img._mut()

    assert len(pt1) == 2
    assert len(pt2) == 2
    pt1 = [int(x) for x in pt1]
    pt2 = [int(x) for x in pt2]

    assert len(color) == 3 or len(color) == 4
    color = [float(x) for x in color]
    if len(color) == 3:
        color.append(255.0)

    if shift is not None and lineType is None:
        lineType = LINE_8
    if lineType is not None and thickness is None:
        thickness = 1

    args = []
    if thickness is not None:
        assert isinstance(thickness, int)
        args.append(thickness)
    if lineType is not None:
        assert isinstance(lineType, int)
        args.append(lineType)
    if shift is not None:
        assert isinstance(shift, int)
        args.append(shift)

    img._f = _filter_line(img._f, pt1, pt2, color, *args)
    return img


def circle(img, center, radius, color, thickness=None, lineType=None, shift=None):
    img = frameify(img)
    img._mut()

    assert len(center) == 2
    center = [int(x) for x in center]

    assert isinstance(radius, int)

    assert len(color) == 3 or len(color) == 4
    color = [float(x) for x in color]
    if len(color) == 3:
        color.append(255.0)

    if shift is not None and lineType is None:
        lineType = LINE_8
    if lineType is not None and thickness is None:
        thickness = 1

    args = []
    if thickness is not None:
        assert isinstance(thickness, int)
        args.append(thickness)
    if lineType is not None:
        assert isinstance(lineType, int)
        args.append(lineType)
    if shift is not None:
        assert isinstance(shift, int)
        args.append(shift)

    img._f = _filter_circle(img._f, center, radius, color, *args)
    return img


def getFontScaleFromHeight(*args, **kwargs):
    """
    cv.getFontScaleFromHeight(	fontFace, pixelHeight[, thickness]	)
    """
    _check_opencv2("getFontScaleFromHeight")
    return _opencv2.getFontScaleFromHeight(*args, **kwargs)


def getTextSize(*args, **kwargs):
    """
    cv.getTextSize(	text, fontFace, fontScale, thickness	)
    """
    _check_opencv2("getTextSize")
    return _opencv2.getTextSize(*args, **kwargs)


def addWeighted(src1, alpha, src2, beta, gamma, dst=None, dtype=-1):
    """
    cv.addWeighted(	src1, alpha, src2, beta, gamma[, dst[, dtype]]	) -> 	dst
    """
    src1 = frameify(src1, "src1")
    src2 = frameify(src2, "src2")
    src1._mut()
    src2._mut()

    if dst is None:
        dst = Frame(src1._f, src1._fmt.copy())
    else:
        assert isinstance(dst, Frame), "dst must be a Frame"
    dst._mut()

    assert isinstance(alpha, float) or isinstance(alpha, int)
    assert isinstance(beta, float) or isinstance(beta, int)
    assert isinstance(gamma, float) or isinstance(gamma, int)
    alpha = float(alpha)
    beta = float(beta)
    gamma = float(gamma)

    if dtype != -1:
        raise Exception("addWeighted does not support the dtype argument")

    dst._f = _filter_addWeighted(src1._f, alpha, src2._f, beta, gamma)
    return dst


def ellipse(
    img,
    center,
    axes,
    angle,
    startAngle,
    endAngle,
    color,
    thickness=1,
    lineType=LINE_8,
    shift=0,
):
    img = frameify(img)
    img._mut()

    assert len(center) == 2
    center = [int(x) for x in center]

    assert len(axes) == 2
    axes = [int(x) for x in axes]

    assert isinstance(angle, float) or isinstance(angle, int)
    assert isinstance(startAngle, float) or isinstance(startAngle, int)
    assert isinstance(endAngle, float) or isinstance(endAngle, int)
    angle = float(angle)
    startAngle = float(startAngle)
    endAngle = float(endAngle)

    assert len(color) == 3 or len(color) == 4
    color = [float(x) for x in color]
    if len(color) == 3:
        color.append(255.0)

    assert isinstance(thickness, int)
    assert isinstance(lineType, int)
    assert isinstance(shift, int)

    img._f = _filter_ellipse(
        img._f,
        center,
        axes,
        angle,
        startAngle,
        endAngle,
        color,
        thickness,
        lineType,
        shift,
    )
    return img


# Stubs for unimplemented functions


def clipLine(*args, **kwargs):
    """
    cv.clipLine(imgRect, pt1, pt2) -> retval, pt1, pt2
    """
    _check_opencv2("clipLine")
    return _opencv2.clipLine(*args, **kwargs)


def drawContours(
    img,
    contours,
    contourIdx,
    color,
    thickness=None,
    lineType=None,
    hierarchy=None,
    maxLevel=None,
    offset=None,
):
    """
    cv.drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]]) -> image
    """
    img = frameify(img)
    img._mut()

    # Convert contours to list format
    contours_converted = []
    for contour in contours:
        contours_converted.append(_convert_single_polygon(contour))

    assert isinstance(contourIdx, int)
    color = _convert_color(color)

    if offset is not None and maxLevel is None:
        maxLevel = 0
    if maxLevel is not None and hierarchy is None:
        hierarchy = []
    if hierarchy is not None and lineType is None:
        lineType = LINE_8
    if lineType is not None and thickness is None:
        thickness = 1

    args = []
    if thickness is not None:
        assert isinstance(thickness, int)
        args.append(thickness)
    if lineType is not None:
        assert isinstance(lineType, int)
        args.append(lineType)
    if hierarchy is not None:
        if isinstance(hierarchy, np.ndarray):
            hierarchy = hierarchy.tolist()
        args.append(hierarchy)
    if maxLevel is not None:
        assert isinstance(maxLevel, int)
        args.append(maxLevel)
    if offset is not None:
        assert isinstance(offset, (list, tuple)) and len(offset) == 2
        args.append([int(offset[0]), int(offset[1])])

    img._f = _filter_drawContours(img._f, contours_converted, contourIdx, color, *args)
    return img


def drawMarker(
    img,
    position,
    color,
    markerType=None,
    markerSize=None,
    thickness=None,
    line_type=None,
):
    """
    cv.drawMarker(img, position, color[, markerType[, markerSize[, thickness[, line_type]]]]) -> img
    """
    img = frameify(img)
    img._mut()

    assert isinstance(position, (list, tuple)) and len(position) == 2
    position = [int(position[0]), int(position[1])]
    color = _convert_color(color)

    if line_type is not None and thickness is None:
        thickness = 1
    if thickness is not None and markerSize is None:
        markerSize = 20
    if markerSize is not None and markerType is None:
        markerType = MARKER_CROSS

    args = []
    if markerType is not None:
        assert isinstance(markerType, int)
        args.append(markerType)
    if markerSize is not None:
        assert isinstance(markerSize, int)
        args.append(markerSize)
    if thickness is not None:
        assert isinstance(thickness, int)
        args.append(thickness)
    if line_type is not None:
        assert isinstance(line_type, int)
        args.append(line_type)

    img._f = _filter_drawMarker(img._f, position, color, *args)
    return img


def ellipse2Poly(*args, **kwargs):
    """
    cv.ellipse2Poly(center, axes, angle, arcStart, arcEnd, delta) -> pts
    """
    _check_opencv2("ellipse2Poly")
    return _opencv2.ellipse2Poly(*args, **kwargs)


def _convert_polygon_list(pts):
    """Convert a list of polygons to the internal format."""
    assert isinstance(pts, list) or isinstance(pts, np.ndarray)
    pts_converted = []
    for poly in pts:
        pts_converted.append(_convert_single_polygon(poly))
    return pts_converted


def _convert_single_polygon(poly):
    """Convert a single polygon to the internal format."""
    if isinstance(poly, np.ndarray):
        poly = poly.tolist()
    # Flatten if shape is (N, 1, 2)
    poly_flat = []
    for pt in poly:
        if isinstance(pt, list) and len(pt) == 1:
            pt = pt[0]
        poly_flat.append([int(pt[0]), int(pt[1])])
    return poly_flat


def _convert_color(color):
    """Convert color to the internal format (4 floats)."""
    assert len(color) == 3 or len(color) == 4
    color = [float(x) for x in color]
    if len(color) == 3:
        color.append(255.0)
    return color


def fillConvexPoly(img, points, color, lineType=None, shift=None):
    """
    cv.fillConvexPoly(img, points, color[, lineType[, shift]]) -> img
    """
    img = frameify(img)
    img._mut()

    points_converted = _convert_single_polygon(points)
    color = _convert_color(color)

    if shift is not None and lineType is None:
        lineType = LINE_8

    args = []
    if lineType is not None:
        assert isinstance(lineType, int)
        args.append(lineType)
    if shift is not None:
        assert isinstance(shift, int)
        args.append(shift)

    img._f = _filter_fillConvexPoly(img._f, points_converted, color, *args)
    return img


def fillPoly(img, pts, color, lineType=None, shift=None, offset=None):
    """
    cv.fillPoly(img, pts, color[, lineType[, shift[, offset]]]) -> img
    """
    img = frameify(img)
    img._mut()

    pts_converted = _convert_polygon_list(pts)
    color = _convert_color(color)

    if offset is not None and shift is None:
        shift = 0
    if shift is not None and lineType is None:
        lineType = LINE_8

    args = []
    if lineType is not None:
        assert isinstance(lineType, int)
        args.append(lineType)
    if shift is not None:
        assert isinstance(shift, int)
        args.append(shift)
    if offset is not None:
        assert isinstance(offset, (list, tuple)) and len(offset) == 2
        args.append([int(offset[0]), int(offset[1])])

    img._f = _filter_fillPoly(img._f, pts_converted, color, *args)
    return img


def polylines(img, pts, isClosed, color, thickness=None, lineType=None, shift=None):
    """
    cv.polylines(img, pts, isClosed, color[, thickness[, lineType[, shift]]]) -> img
    """
    img = frameify(img)
    img._mut()

    pts_converted = _convert_polygon_list(pts)
    assert isinstance(isClosed, bool)
    color = _convert_color(color)

    if shift is not None and lineType is None:
        lineType = LINE_8
    if lineType is not None and thickness is None:
        thickness = 1

    args = []
    if thickness is not None:
        assert isinstance(thickness, int)
        args.append(thickness)
    if lineType is not None:
        assert isinstance(lineType, int)
        args.append(lineType)
    if shift is not None:
        assert isinstance(shift, int)
        args.append(shift)

    img._f = _filter_polylines(img._f, pts_converted, isClosed, color, *args)
    return img


def flip(src, flipCode):
    """
    cv.flip(src, flipCode[, dst]) -> dst

    Flips a 2D array around vertical, horizontal, or both axes.

    Parameters:
        src: input array
        flipCode: a flag to specify how to flip the array:
            0 means flipping around the x-axis (vertical flip)
            positive value (e.g., 1) means flipping around y-axis (horizontal flip)
            negative value (e.g., -1) means flipping around both axes
    """
    src = frameify(src)
    src._mut()

    assert isinstance(flipCode, int)

    f = _filter_flip(src._f, flipCode)
    return Frame(f, src._fmt.copy())


def rotate(src, rotateCode):
    """
    cv.rotate(src, rotateCode[, dst]) -> dst

    Rotates a 2D array in multiples of 90 degrees.

    Parameters:
        src: input array
        rotateCode: an enum to specify how to rotate the array:
            cv2.ROTATE_90_CLOCKWISE (0) - rotate 90 degrees clockwise
            cv2.ROTATE_180 (1) - rotate 180 degrees
            cv2.ROTATE_90_COUNTERCLOCKWISE (2) - rotate 90 degrees counter-clockwise
    """
    src = frameify(src)
    src._mut()

    assert isinstance(rotateCode, int)
    assert rotateCode in [ROTATE_90_CLOCKWISE, ROTATE_180, ROTATE_90_COUNTERCLOCKWISE]

    # Calculate new dimensions
    width = src._fmt["width"]
    height = src._fmt["height"]

    if rotateCode == ROTATE_180:
        new_width, new_height = width, height
    else:
        # 90 degree rotations swap width and height
        new_width, new_height = height, width

    f = _filter_rotate(src._f, rotateCode)
    fmt = {"width": new_width, "height": new_height, "pix_fmt": src._fmt["pix_fmt"]}
    return Frame(f, fmt)


def copyMakeBorder(src, top, bottom, left, right, borderType, value=None):
    """
    cv.copyMakeBorder(src, top, bottom, left, right, borderType[, dst[, value]]) -> dst

    Forms a border around an image.

    Parameters:
        src: input array
        top: number of pixels for the top border
        bottom: number of pixels for the bottom border
        left: number of pixels for the left border
        right: number of pixels for the right border
        borderType: border type (cv2.BORDER_CONSTANT, cv2.BORDER_REPLICATE, etc.)
        value: border value if borderType == cv2.BORDER_CONSTANT (default: black)
    """
    src = frameify(src)
    src._mut()

    assert isinstance(top, int) and top >= 0
    assert isinstance(bottom, int) and bottom >= 0
    assert isinstance(left, int) and left >= 0
    assert isinstance(right, int) and right >= 0
    assert isinstance(borderType, int)

    # Handle value parameter
    if value is None:
        value = [0.0, 0.0, 0.0, 255.0]
    else:
        assert len(value) == 3 or len(value) == 4
        value = [float(x) for x in value]
        if len(value) == 3:
            value.append(255.0)

    # Calculate new dimensions
    new_width = src._fmt["width"] + left + right
    new_height = src._fmt["height"] + top + bottom

    f = _filter_copyMakeBorder(src._f, top, bottom, left, right, borderType, value)
    fmt = {"width": new_width, "height": new_height, "pix_fmt": src._fmt["pix_fmt"]}
    return Frame(f, fmt)


def hconcat(src):
    """
    cv.hconcat(src[, dst]) -> dst

    Concatenates arrays horizontally.

    Parameters:
        src: sequence of arrays to concatenate. All arrays must have the same height.
    """
    assert isinstance(src, (list, tuple)) and len(src) > 0

    frames = []
    height = None
    pix_fmt = None

    for item in src:
        item = frameify(item)
        item._mut()

        if height is None:
            height = item._fmt["height"]
            pix_fmt = item._fmt["pix_fmt"]
        else:
            assert (
                item._fmt["height"] == height
            ), "All frames must have the same height for hconcat"

        frames.append(item._f)

    # Calculate total width
    total_width = sum(frameify(item)._fmt["width"] for item in src)

    f = _filter_hconcat([frame for frame in frames])
    fmt = {"width": total_width, "height": height, "pix_fmt": pix_fmt}
    return Frame(f, fmt)


def vconcat(src):
    """
    cv.vconcat(src[, dst]) -> dst

    Concatenates arrays vertically.

    Parameters:
        src: sequence of arrays to concatenate. All arrays must have the same width.
    """
    assert isinstance(src, (list, tuple)) and len(src) > 0

    frames = []
    width = None
    pix_fmt = None

    for item in src:
        item = frameify(item)
        item._mut()

        if width is None:
            width = item._fmt["width"]
            pix_fmt = item._fmt["pix_fmt"]
        else:
            assert (
                item._fmt["width"] == width
            ), "All frames must have the same width for vconcat"

        frames.append(item._f)

    # Calculate total height
    total_height = sum(frameify(item)._fmt["height"] for item in src)

    f = _filter_vconcat([frame for frame in frames])
    fmt = {"width": width, "height": total_height, "pix_fmt": pix_fmt}
    return Frame(f, fmt)
