import numpy as np
import scipy.ndimage as ndi
from skimage.util.dtype import dtype_range

from .plotplugin import PlotPlugin


__all__ = ['LineProfile']

#TODO: Extract line tool and add it to a new `canvastools` subpackage.


class LineProfile(PlotPlugin):
    """Plugin to compute interpolated intensity under a scan line on an image.

    See PlotPlugin and Plugin classes for additional details.

    Parameters
    ----------
    linewidth : float
        Line width for interpolation. Wider lines average over more pixels.
    epsilon : float
        Maximum pixel distance allowed when selecting end point of scan line.
    limits : tuple or {None, 'image', 'dtype'}
        (minimum, maximum) intensity limits for plotted profile. The following
        special values are defined:

            None : rescale based on min/max intensity along selected scan line.
            'image' : fixed scale based on min/max intensity in image.
            'dtype' : fixed scale based on min/max intensity of image dtype.
    """
    name = 'Line Profile'
    draws_on_image = True

    def __init__(self, linewidth=1, epsilon=5, limits='image', **kwargs):
        super(LineProfile, self).__init__(**kwargs)
        self.linewidth = linewidth
        self.epsilon = epsilon
        self._active_pt = None
        self._limit_type = limits
        self.line_kwargs = dict(color='y', lw=linewidth, alpha=0.5, marker='s',
                                markersize=5, solid_capstyle='butt')
        print(self.help())

    def attach(self, image_viewer):
        super(LineProfile, self).attach(image_viewer)

        image = image_viewer.original_image

        if self._limit_type == 'image':
            self.limits = (np.min(image), np.max(image))
        elif self._limit_type == 'dtype':
            self.self._limit_type = dtype_range[image.dtype.type]
        elif self._limit_type is None or len(self._limit_type) == 2:
            self.limits = self._limit_type
        else:
            raise ValueError("Unrecognized `limits`: %s" % self._limit_type)

        if not self._limit_type is None:
            self.ax.set_ylim(self.limits)

        h, w = image.shape
        self._init_end_pts = np.array([[w / 3, h / 2], [2 * w / 3, h / 2]])
        self.end_pts = self._init_end_pts.copy()

        x, y = np.transpose(self.end_pts)
        self.scan_line = image_viewer.ax.plot(x, y, **self.line_kwargs)[0]
        self.artists.append(self.scan_line)

        scan_data = profile_line(image, self.end_pts)
        self.profile = self.ax.plot(scan_data, 'k-')[0]
        self._autoscale_view()

        self.connect_image_event('key_press_event', self.on_key_press)
        self.connect_image_event('button_press_event', self.on_mouse_press)
        self.connect_image_event('button_release_event', self.on_mouse_release)
        self.connect_image_event('motion_notify_event', self.on_move)
        self.connect_image_event('scroll_event', self.on_scroll)

        self.image_viewer.redraw()

    def help(self):
        helpstr = ("Line profile tool",
                   "+ and - keys or mouse scroll changes width of scan line.",
                   "Select and drag ends of the scan line to adjust it.")
        return '\n'.join(helpstr)

    def get_profile(self):
        """Return intensity profile of the selected line.

        Returns
        -------
        end_pts: (2, 2) array
            The positions ((x1, y1), (x2, y2)) of the line ends.
        profile: 1d array
            Profile of intensity values.
        """
        end_pts = self.scan_line.get_xydata()
        profile = self.profile.get_ydata()
        return end_pts, profile

    def on_scroll(self, event):
        if not event.inaxes:
            return
        if event.button == 'up':
            self._thicken_scan_line()
        elif event.button == 'down':
            self._shrink_scan_line()

    def on_key_press(self, event):
        if not event.inaxes:
            return
        elif event.key == '+':
            self._thicken_scan_line()
        elif event.key == '-':
            self._shrink_scan_line()
        elif event.key == 'r':
            self.reset()

    def _thicken_scan_line(self):
        self.linewidth += 1
        self.line_changed(None, None)

    def _shrink_scan_line(self):
        if self.linewidth > 1:
            self.linewidth -= 1
            self.line_changed(None, None)

    def _autoscale_view(self):
        if self.limits is None:
            self.ax.autoscale_view(tight=True)
        else:
            self.ax.autoscale_view(scaley=False, tight=True)

    def get_pt_under_cursor(self, event):
        """Return index of the end point under cursor, if sufficiently close"""
        xy = np.asarray(self.scan_line.get_xydata())
        xyt = self.scan_line.get_transform().transform(xy)
        xt, yt = xyt[:, 0], xyt[:, 1]
        d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
        indseq = np.nonzero(np.equal(d, np.amin(d)))[0]
        ind = indseq[0]
        if d[ind] >= self.epsilon:
            ind = None
        return ind

    def on_mouse_press(self, event):
        if event.button != 1:
            return
        if event.inaxes == None:
            return
        self._active_pt = self.get_pt_under_cursor(event)

    def on_mouse_release(self, event):
        if event.button != 1:
            return
        self._active_pt = None

    def on_move(self, event):
        if event.button != 1:
            return
        if self._active_pt is None:
            return
        if not self.image_viewer.ax.in_axes(event):
            return
        x, y = event.xdata, event.ydata
        self.line_changed(x, y)

    def reset(self):
        self.end_pts = self._init_end_pts.copy()
        self.scan_line.set_data(np.transpose(self.end_pts))
        self.line_changed(None, None)

    def line_changed(self, x, y):
        if x is not None:
            self.end_pts[self._active_pt, :] = x, y
        self.scan_line.set_data(np.transpose(self.end_pts))
        self.scan_line.set_linewidth(self.linewidth)

        scan = profile_line(self.image_viewer.original_image, self.end_pts,
                            linewidth=self.linewidth)
        self.profile.set_xdata(np.arange(scan.shape[0]))
        self.profile.set_ydata(scan)

        self.ax.relim()

        if self.useblit:
            self.image_viewer.canvas.restore_region(self.img_background)
            self.ax.draw_artist(self.scan_line)
            self.ax.draw_artist(self.profile)
            self.image_viewer.canvas.blit(self.image_viewer.ax.bbox)

        self._autoscale_view()

        self.image_viewer.redraw()
        self.redraw()


def profile_line(img, end_pts, linewidth=1):
    """Return the intensity profile of an image measured along a scan line.

    Parameters
    ----------
    img : 2d array
        The image.
    end_pts: (2, 2) list
        End points ((x1, y1), (x2, y2)) of scan line.
    linewidth: int
        Width of the scan, perpendicular to the line

    Returns
    -------
    return_value : array
        The intensity profile along the scan line. The length of the profile
        is the ceil of the computed length of the scan line.
    """
    point1, point2 = end_pts
    x1, y1 = point1 = np.asarray(point1, dtype=float)
    x2, y2 = point2 = np.asarray(point2, dtype=float)
    dx, dy = point2 - point1

    # Quick calculation if perfectly horizontal or vertical (remove?)
    if x1 == x2:
        pixels = img[min(y1, y2): max(y1, y2) + 1,
                     x1 - linewidth / 2:  x1 + linewidth / 2 + 1]
        intensities = pixels.mean(axis=1)
        return intensities
    elif y1 == y2:
        pixels = img[y1 - linewidth / 2:  y1 + linewidth / 2 + 1,
                     min(x1, x2): max(x1, x2) + 1]
        intensities = pixels.mean(axis=0)
        return intensities

    theta = np.arctan2(dy, dx)
    a = dy / dx
    b = y1 - a * x1
    length = np.hypot(dx, dy)

    line_x = np.linspace(min(x1, x2), max(x1, x2), np.ceil(length))
    line_y = line_x * a + b
    y_width = abs(linewidth * np.cos(theta) / 2)
    perp_ys = np.array([np.linspace(yi - y_width,
                                    yi + y_width, linewidth) for yi in line_y])
    perp_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis]

    perp_lines = np.array([perp_ys, perp_xs])
    pixels = ndi.map_coordinates(img, perp_lines)
    intensities = pixels.mean(axis=1)

    return intensities
