# Copyright 2017 The Tangram Developers. See the AUTHORS file at the
# top-level directory of this distribution and at
# https://github.com/renatoGarcia/tangram/blob/master/AUTHORS.
#
# This file is part of Tangram.
#
# Tangram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Tangram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Tangram in the COPYING and COPYING.LESSER files.
# If not, see <http://www.gnu.org/licenses/>.

import sys
from typing import Any, Callable
from copy import copy

import cairo
import numpy as np
import wrapt


class ReactiveWrapper(wrapt.ObjectProxy):
    def __init__(self, wrapped: Any, callback: Callable[[], Any]) -> None:
        if isinstance(wrapped, ReactiveWrapper):
            wrapped = wrapped.__wrapped__

        super().__init__(wrapped)
        self._self_callback = callback

    def __copy__(self) -> 'ReactiveWrapper':
        return ReactiveWrapper(copy(self.__wrapped__), self._self_callback)

    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        if name not in ['_self_callback', '__wrapped__']:
            self._self_callback()

    def __setitem__(self, key, value):
        self.__wrapped__[key] = value
        self._self_callback()
        return self

    def __iadd__(self, other):
        self.__wrapped__ += other
        self._self_callback()
        return self

    def __isub__(self, other):
        self.__wrapped__ -= other
        self._self_callback()
        return self

    def __imul__(self, other):
        self.__wrapped__ *= other
        self._self_callback()
        return self

    def __imatmul__(self, other):
        self.__wrapped__ @= other
        self._self_callback()
        return self

    def __itruediv__(self, other):
        self.__wrapped__ /= other
        self._self_callback()
        return self

    def __ifloordiv__(self, other):
        self.__wrapped__ //= other
        self._self_callback()
        return self

    def __imod__(self, other):
        self.__wrapped__ %= other
        self._self_callback()
        return self

    def __ipow__(self, other):
        self.__wrapped__ **= other
        self._self_callback()
        return self

    def __ilshift__(self, other):
        self.__wrapped__ <<= other
        self._self_callback()
        return self

    def __irshift__(self, other):
        self.__wrapped__ >>= other
        self._self_callback()
        return self

    def __iand__(self, other):
        self.__wrapped__ &= other
        self._self_callback()
        return self

    def __ixor__(self, other):
        self.__wrapped__ ^= other
        self._self_callback()
        return self

    def __ior__(self, other):
        self.__wrapped__ |= other
        self._self_callback()
        return self


def ndarray_to_surface(ndarray: np.ndarray) -> cairo.ImageSurface:
    assert sys.byteorder == 'little'

    shape = ndarray.shape
    width = shape[1]
    height = shape[0]

    if ndarray.dtype == np.uint8 and len(shape) == 3 and shape[2] == 4:
        pixel_format = cairo.FORMAT_ARGB32
        stride = cairo.Format.stride_for_width(pixel_format, width)
        if stride == -1:
            raise ValueError("Ndarray width is too large")
        assert stride%4 == 0
        data = np.empty((height, stride//4, 4), dtype=np.uint8)
        alpha = ndarray[:, :, 3] / 255.0
        data[:height, :width, 0] = ndarray[:, :, 2] * alpha
        data[:height, :width, 1] = ndarray[:, :, 1] * alpha
        data[:height, :width, 2] = ndarray[:, :, 0] * alpha
        data[:height, :width, 3] = ndarray[:, :, 3]
    elif ndarray.dtype == np.uint8 and len(shape) == 3 and shape[2] == 3:
        pixel_format = cairo.FORMAT_RGB24
        stride = cairo.Format.stride_for_width(pixel_format, width)
        if stride == -1:
            raise ValueError("Ndarray width is too large")
        assert stride%4 == 0
        data = np.empty((height, stride//4, 4), dtype=np.uint8)
        data[:height, :width, 0] = ndarray[:, :, 2]
        data[:height, :width, 1] = ndarray[:, :, 1]
        data[:height, :width, 2] = ndarray[:, :, 0]
    elif ndarray.dtype == np.uint8 and len(shape) == 2:
        pixel_format = cairo.FORMAT_A8
        stride = cairo.Format.stride_for_width(pixel_format, width)
        if stride == -1:
            raise ValueError("Ndarray width is too large")
        data = np.empty((height, stride), dtype=np.uint8)
        data[:height, :width] = ndarray[:, :]
    else:
        raise ValueError("Invalid image format.")

    if width == 0 or height == 0:
        return cairo.ImageSurface(pixel_format, width, height)
    else:
        return cairo.ImageSurface.create_for_data(data, pixel_format, width, height, stride)
