# -*- coding: utf-8 -*-
# pybec v0.1.0 Copyright © 2012
# Kevin C. Wright
# Licensed under the terms of the GNU Public License v 3.0 (see LICENSE.txt)

# pylint: disable-msg=C0103
"""
pybec.roi v0.1 
Kevin C. Wright - May 2011

Defines the ROI class, representing a region of interest in a Frame
!! Vertical coordinates (y) precede horizontal (x) to match Numpy and PIL
"""
from __future__ import division
from numpy import sort, clip
from pybec.exceptions import PyBECError

class ROIError(PyBECError):
    """Exception for pybec.ROI class"""
    pass

class ROI(object):
    """ 
===============================
ROI(\**roi_info)
===============================
Represents a rectangular region of interest in an image

--------------------
**IMPORTANT NOTES**:
--------------------

**Vertical coordinates (y) precede horizontal (x) in ALL data members
of this class, to match array/image slicing conventions for Numpy and
Python Imaging Library.** 

**The origin (0,0) is defined to be in the upper left corner.**

------------------------------
Usage
------------------------------

::

    > ROI( tblr = [top, bottom, left, right] )  # specify by edge coordinates   
    > ROI( center = (y0, x0), size = (dy, dx) ) # center coordinates and size
    > ROI( slices = (slice(top, bottom + 1), slice(left, right+1) )       

Additional options ::

    > ROI( tblr = [t,b,l,r],  framesize = (max_y, max_x) )
     Specifies the max allowed range of the ROI
     (defaults to (2048,2048) if not specified)
        
After the ROI is specified in one of these forms, the information
is internally converted between all three representations in a
consistent manner. 

-------------------------------
ROI Class Data Elements:
-------------------------------
    center : tuple (y0,x0)
         coordinates of the center point of the ROI
    size : tuple (dy, dx)
        edge width of the region in pixels
    tblr : list or tuple [top, bottom, left, right]
        Location of edges of the ROI 
    slices  : tuple of numpy.array.slice (slice(t, b), slice(l, r))
        needed for convenient cropping of image data arrays 
    framesize: tuple (maxy, maxx)
        size of the image frame - sets limit on ROI values
    area : float
        the ROI area in number of pixels (dx * dy)
    coerced: boolean 
        indicates whether the ROI specified was coerced (see below)
    

__init__() checks to see that input values are in order min, max where
min < max, makes sure no values are outside the frame size, and if any 
problems are found it tries to coerce the values to valid ones

If the value had to be coerced, then ROI.coreced is set to True

Any other initialization errors should raise a ValueError exception

"""
    # set the default range limit used if the frame size isn't specified
    max_framesize = (2048, 2048)
    
    def __init__(self, **roi_info):
        """ 
        The ROI can be specified using the following keywords
        1) 'tblr'     : edge coordinates [top, bottom, left, right]
        2) 'center'   : center point (y0, x0),
           'size'     : edge size (dy, dx)
        3) 'slices'   : ROI given as a tuple of array slice objects, e.g. 
                       (slice(top, bottom+1, 1),slice(left, right+1, 1))       
        Additional options:
        4) 'framesize': Specifies the max allowed range of the ROI
                     (defaults to (2048,2048) if not specified)
        
        After the ROI is specified in one of these forms, the information
        is internally converted between all three representations in a
        consistent manner 
        """
        # Get the input, assigning appropriate default values      
        self.tblr  = roi_info.get('tblr', None)
        self.center = roi_info.get('center', None)
        self.size = roi_info.get('size', None)
        self.slices = roi_info.get('slices', None)
        self.framesize = roi_info.get('framesize', ROI.max_framesize)
        # Check what keywords were actually given
        tblr_input_mode =  all([ self.tblr is not None, self.center is None,
                            self.size is None    , self.slices is None])
        centsize_input_mode =  all([ self.tblr is None, self.center is not None,
                                self.size is not None, self.slices is None])
        slices_input_mode =  all([ self.tblr is None, self.center is None,
                              self.size is None, self.slices is not None])
        # Check input depending on what was given
        if tblr_input_mode:
            if type(self.tblr) not in [ list, tuple ] or len(self.tblr) != 4 :
                raise ROIError('Invalid [top, bottom, left, right] input')
        elif centsize_input_mode:
            if not all([len(self.center) == 2 , len(self.size) == 2]):
                raise ROIError('badly formatted center/size input to ROI()')
            self.tblr = _cent2tblr(self.center, self.size)
        elif slices_input_mode:
            if type(self.slices) not in [list, tuple] \
                 or len(self.slices) != 2 \
                 or self.slices[0].__class__.__name__ != 'slice' \
                 or self.slices[1].__class__.__name__ != 'slice' :
                raise ROIError('badly formatted slice input to ROI()')
            self.tblr = _slic2tblr(self.slices)
        else:
            raise ROIError(""" Badly formatted input to ROI.__init__()\n%s"""\
                            % str(roi_info))
        
        # make sure the ROI edge values are OK, coerce and raise flag if not
        self.coerced = self._fix_invalid_tblr()
        
        # Using the validated edges, set the rest of the class attributes
        self.slices = _tblr2slic(self.tblr)
        self.center, self.size = _tblr2cent(self.tblr)
        self.area = self.size[0] * self.size[1]

    def resize(self, size):
        """Changes the size of the ROI without changing the center"""
        self.tblr = _cent2tblr(self.center, size)
        self.coerced = self._fix_invalid_tblr()
        self.slices = _tblr2slic(self.tblr)
        self.center, self.size = _tblr2cent(self.tblr)
        self.area = self.size[0] * self.size[1]
    
    def recenter(self, center):
        """Changes the center (x,y) of the ROI without changing the size"""
        self.tblr = _cent2tblr(center, self.size)
        self.coerced = self._fix_invalid_tblr()
        self.slices = _tblr2slic(self.tblr)
        self.center, self.size = _tblr2cent(self.tblr)
        self.area = self.size[0] * self.size[1]
        
    def square(self):
        """
        Makes the ROI into a square of edge length equal to its longest side
        """
        edge_length = max(self.size)
        self.resize((edge_length, edge_length))
        
    def scale(self, factor):
        """
        Scale the size of the ROI by a multiplicative factor
        """ 
        self.resize( (int(self.size[0] * factor), int(self.size[1] * factor)) )
                    
    def _fix_invalid_tblr(self):
        """ checks to see that input values arein order min, max where min<max,
        makes sure no values are outside the frame size, and if any 
        problems are found it tries to coerce the values to valid ones
        returns True if coerced, False otherwise"""
        vmax, hmax = self.framesize
        # Re-order the horizontal and vertical elements and clip to max values
        vert = clip(sort([self.tblr[0], self.tblr[1]]), 0, vmax)
        horiz = clip(sort([self.tblr[2], self.tblr[3]]), 0, hmax)
        # update the [top,bottom,left,right] information in the class data
        new_tblr = list(vert) + list(horiz)
        rangerr = new_tblr != self.tblr
        self.tblr = new_tblr
        return rangerr     

    def __add__(self, other):
        """
        Defines behavior for adding two ROI class instances:
        Takes this ROI and another and returns the smallest rectangular ROI containing both
        """
        if not isinstance(other, ROI):
            raise ROIError
        tcomp, bcomp, lcomp, rcomp = zip(self.tblr, other.tblr)
        combined_tblr = [min(tcomp), max(bcomp), min(lcomp), max(rcomp)]
        return ROI(tblr=combined_tblr)
    
    def __sub__(self, other):
        """
        Defines behavior for subtracting two ROI class instances:
        Takes this ROI and another and returns and ROI containing their intersection
        """
        if not isinstance(other, ROI):
            raise ROIError
        tcomp, bcomp, lcomp, rcomp = zip(self.tblr, other.tblr)
        combined_tblr = [max(tcomp), min(bcomp), max(lcomp), min(rcomp)]
        return ROI(tblr=combined_tblr)
    
    def __eq__(self,other):
        if not isinstance(other, ROI):
            raise ROIError
        selfdict = self.__dict__
        otherdict = other.__dict__
        selfdict.pop('coerced')
        otherdict.pop('coerced')
        return selfdict == otherdict        
                
    def __repr__(self):
        """
        Determines the representation of the ROI Class instance in the console
        """
        xc, yc = self.center
        dx, dy = self.size
        return 'ROI: xc=%s yc=%s dx=%s dy=%s' % (str(xc), str(yc), str(dx), str(dy))
#-----------------------------------------------------------------------        

def _slic2tblr(slices):
    """convert an image slice to [top, bottom, left, right] form"""
    vslice, hslice = slices
    tblr = [vslice.start, vslice.stop - 1, hslice.start, hslice.stop - 1]
    return tblr  
   
def _cent2tblr(center, size):
    """convert center,size information to [top, bottom, left, right] form"""
    y0, x0 = center
    dy, dx = size
    tblr_float = [y0 - (dy - 1) / 2., \
                  y0 + (dy - 1) / 2., \
                  x0 - (dx - 1) / 2., \
                  x0 + (dx - 1) / 2.  ]
    return [int(round(val)) for val in tblr_float] 

def _tblr2slic(tblr):
    """convert [top, bottom, left, right] information a image slice form"""
    t, b, l, r = tblr
    return (slice(t, b + 1, 1), slice(l, r + 1, 1))
    
def _tblr2cent(tblr):
    """convert [top, bottom, left, right] information center, size form"""
    t, b, l, r = tblr
    center = (t + (b - t) / 2.0, l + (r - l) / 2.0)
    size = ((b - t) + 1 , (r - l) + 1)
    return (center, size)

#-----------------------------------------------------------------------        

def sumROI(roi_list):
    """ takes a list of ROIs and returns an ROI that contains them all"""
    for each_roi in roi_list:
        if not isinstance(each_roi, ROI):
            raise ROIError
    if len(roi_list) == 1:
        return(roi_list)[0]    
    else:
        summed = roi_list[0]
        for each_roi in roi_list:
            summed += each_roi
        return summed
