# The MIT License (MIT)
#
# Copyright (c) 2014-2016 WUSTL ZPLAB
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Authors: Erik Hvatum <ice.rikh@gmail.com>

import ctypes
import numpy
import OpenGL
import OpenGL.GL as PyGL
from PyQt5 import Qt
import textwrap
from .async_texture import AsyncTexture
from .ndimage_statistics import ndimage_statistics

class Image(Qt.QObject):
    """An instance of the Image class is a wrapper around a Numpy ndarray representing a single image, optional mask data, plus some related attributes and
    a .name.

    If an ndarray of supported dtype, shape, and striding is supplied as the data argument to Image's constructor or set method,
    a reference to that ndarray is kept rather than a copy of it.  In such cases, if the wrapped data is subsequently modified,
    care must be taken to call the Image's .refresh method before querying, for example, its .histogram property as changes to the
    data are not automatically detected.

    The attributes maintained by an Image instance fall into the following categories:
        * Properties that represent aspects of ONLY .data: .dtype, .strides.  For non-floating-point Images, .range is also in this category.  The
        .data_changed signal is emitted when a new value is assigned to .data or the .refresh method is called with data_changed=True in order to indicate
        that the contents of .data have been modified in place.
        * Plain instance attributes updated by .refresh() and .set(..): .size, .is_grayscale, .num_channels, .has_alpha_channel, .is_twelve_bit.  Although
        nothing prevents assigning over these attributes, doing so is not advised.  The .data_changed signal is emitted to indicate that the value of any
        or all of these attributes has changed.
        * Properties computed from the combination of .data, .mask, and .imposed_float_range.  These include .histogram, .histogram_max_bin, .extremae.
        * .mask: None or a 2D bool or uint8 ndarray with neither dimension smaller than the corresponding dimension of .data.  If mask is not None, only
        image pixels with non-zero mask counterparts contribute to the histogram and extremae.  Mask pixels outside of the image have no impact.  If mask
        is None, all image pixels are included.  The .mask_changed signal is emitted to indicate that .mask has been replaced or that the contents of .mask
        have changed.
        * Properties with individual change signals: .name.  It is safe to assign None in addition anything else that str(..) accepts as its argument to
        .name.  When the value of .name or .imposed_float_range is modified, .name_changed is emitted.

    Additionally, emission of .data_changed, .mask_changed, or .name_changed causes emission of .changed.

    An Image instance should only be manipulated by the thread that owns it.  Imposing this restriction simplifies Image's implementation while improving
    performance.  For example, the main thread may modify image.data in place while texture upload and ndimage statistic calculations are ongoing in background
    threads, with the result that the content of image.stats_future.result() - used by image.extremae, image.histogram, and image.max_histogram_bin -  and the
    texture bound by "image.bind_texture(n, estack):" become undefined.  However, so long as the main thread calls image.refresh() immediately after modifying
    the contents of image.data, causing image.stats_future and image.async_texture to be replaced, there is never an opportunity for these undefined results
    to be used."""
    changed = Qt.pyqtSignal(object)
    data_changed = Qt.pyqtSignal(object)
    mask_changed = Qt.pyqtSignal(object)
    name_changed = Qt.pyqtSignal(object)

    IMAGE_TYPE_TO_QOGLTEX_TEX_FORMAT = {
        'G': Qt.QOpenGLTexture.R32F,
        'Ga': Qt.QOpenGLTexture.RG32F,
        'rgb': Qt.QOpenGLTexture.RGB32F,
        'rgba': Qt.QOpenGLTexture.RGBA32F}
    NUMPY_DTYPE_TO_GL_PIXEL_TYPE = {
        numpy.bool8  : PyGL.GL_UNSIGNED_BYTE,
        numpy.uint8  : PyGL.GL_UNSIGNED_BYTE,
        numpy.uint16 : PyGL.GL_UNSIGNED_SHORT,
        numpy.float32: PyGL.GL_FLOAT}
    IMAGE_TYPE_TO_GL_PIX_FORMAT = {
        'G'   : PyGL.GL_RED,
        'Ga'  : PyGL.GL_RG,
        'rgb' : PyGL.GL_RGB,
        'rgba': PyGL.GL_RGBA}

    def __init__(
            self,
            data,
            mask=None,
            parent=None,
            is_twelve_bit=False,
            imposed_float_range=None,
            name=None,
            immediate_texture_upload=True,
            use_open_mp=False):
        """
        The shape of image and mask data is interpreted as (x,y) for 2-d arrays and (x,y,c) for 3-d arrays.  If your image or mask was loaded as (y,x),
        array.T will produce an (x,y)-shaped array.  In case of (y,x,c) image data, array.swapaxes(0,1) is required."""
        super().__init__(parent)
        if data is None:
            raise ValueError('The "data" argument supplied to Image.__init__(..) must not be None.')
        self.data_changed.connect(self.changed)
        self.objectNameChanged.connect(self._onObjectNameChanged)
        self.name_changed.connect(self.changed)
        self.set(
            data=data,
            mask=mask,
            is_twelve_bit=is_twelve_bit,
            imposed_float_range=imposed_float_range,
            name=name,
            immediate_texture_upload=immediate_texture_upload,
            use_open_mp=use_open_mp)

    def _onObjectNameChanged(self):
        self.name_changed.emit(self)

    @classmethod
    def from_qimage(cls, qimage, parent=None, is_twelve_bit=False, name=None, use_open_mp=False):

        if not qimage.isNull() and qimage.format() != Qt.QImage.Format_Invalid:
            if qimage.hasAlphaChannel():
                desired_format = Q
t.QImage.Format_RGBA8888
                channel_count = 4
            else:
                desired_format = Qt.QImage.Format_RGB888
                channel_count = 3
            if qimage.format() != desired_format:
                qimage = qimage.convertToFormat(desired_format)
            if channel_count == 3:
                # 24-bit RGB QImage rows are padded to 32-bit chunks, which we must match
                row_stride = qimage.width() * 3
                row_stride += 4 - (row_stride % 4)
                padded = numpy.ctypeslib.as_array(ctypes.cast(int(qimage.bits()), ctypes.POINTER(ctypes.c_uint8)), shape=(qimage.height(), row_stride))
                padded = padded[:, qimage.width() * 3].reshape((qimage.height(), qimage.width(), 3))
                npyimage = numpy.empty((qimage.height(), qimage.width(), 3), dtype=numpy.uint8)
                npyimage.flat = padded.flat
            else:
                npyimage = numpy.ctypeslib.as_array(
                    ctypes.cast(int(qimage.bits()), ctypes.POINTER(ctypes.c_uint8)),
                    shape=(qimage.height(), qimage.width(), channel_count))
            if qimage.isGrayscale():
                # Note: Qt does not support grayscale with alpha channels, so we don't need to worry about that case
                npyimage=npyimage[...,0]
            retur