import math
import cv2
import numpy as np


def wordSegmentation(img, kernelSize=25, sigma=11, theta=7, minArea=0):
    """Scale space technique for word segmentation proposed by R. Manmatha: http://ciir.cs.umass.edu/pubfiles/mm-27.pdf

	Args:
		img: grayscale uint8 image of the text-line to be segmented.
		kernelSize: size of filter kernel, must be an odd integer.
		sigma: standard deviation of Gaussian function used for filter kernel.
		theta: approximated width/height ratio of words, filter function is distorted by this factor.
		minArea: ignore word candidates smaller than specified area.

	Returns:
		List of tuples. Each tuple contains the bounding box and the image of the segmented word.
	"""

    # apply filter kernel
    kernel = createKernel(kernelSize, sigma, theta)
    # The function applies an arbitrary linear filter to an image.
    # int ddepth (=-1) - desired depth of the destination image
    # anchor - indicates the relative position of a filtered point within the kernel;
    # default value (-1,-1) means that the anchor is at the kernel center.
    # borderType - pixel extrapolation method:
    # cv2.BORDER_REPLICATE -  The row or column at the very edge of the original is replicated to the extra border.
    imgFiltered = cv2.filter2D(img, -1, kernel, borderType=cv2.BORDER_REPLICATE).astype(np.uint8)
    # threshold - If pixel value is greater than a threshold value, it is assigned one value, else it is assigned another value
    # img - source image, which should be a grayscale image.
    # Second argument is the threshold value which is used to classify the pixel values.
    # Third argument is the maxVal which represents the value to be given if pixel value is more than the threshold value.
    # Last - different styles of thresholding
    # Returns: threshold value computed, destination image
    (_, imgThres) = cv2.threshold(imgFiltered, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    imgThres = 255 - imgThres

    # find connected components. OpenCV: return type differs between OpenCV2 and 3
    # findContours - The function retrieves contours from the binary image
    # First argument is source image, second is contour retrieval mode, third is contour approximation method.
    if cv2.__version__.startswith('3.'):
        (_, components, _) = cv2.findContours(imgThres, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    else:
        # cv2.RETR_EXTERNAL or cv2.RETR_LIST - ???
        (components, _) = cv2.findContours(imgThres, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    # append components to result
    res = []
    for c in components:
        # skip small word candidates
        if cv2.contourArea(c) < minArea:
            continue
        # append bounding box and image of word to result list
        currBox = cv2.boundingRect(c)  # returns (x, y, w, h)
        (x, y, w, h) = currBox
        currImg = img[y:y + h, x:x + w]
        res.append((currBox, currImg))

    # return list of words, sorted by x-coordinate
    return sorted(res, key=lambda entry: entry[0][0])


def prepareImg(img, height):
    """ Convert given image to grayscale image (if needed) and resize to desired height. """
    assert img.ndim in (2, 3)
    if img.ndim == 3:
        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    h = img.shape[0]
    factor = height / h
    return cv2.resize(img, dsize=None, fx=factor, fy=factor)


def createKernel(kernelSize, sigma, theta):
    "create anisotropic filter kernel according to given parameters"
    assert kernelSize % 2  # must be odd size
    halfSize = kernelSize // 2  # get integer-valued resize of division

    kernel = np.zeros([kernelSize, kernelSize])  # kernel
    sigmaX = sigma  # scale factor for X dimension
    sigmaY = sigma * theta  # theta - multiplication factor = sigmaX/sigmaY

    for i in range(kernelSize):
        for j in range(kernelSize):
            x = i - halfSize
            y = j - halfSize

            expTerm = np.exp(-x ** 2 / (2 * sigmaX) - y ** 2 / (2 * sigmaY))
            xTerm = (x ** 2 - sigmaX ** 2) / (2 * math.pi * sigmaX ** 5 * sigmaY)
            yTerm = (y ** 2 - sigmaY ** 2) / (2 * math.pi * sigmaY ** 5 * sigmaX)

            kernel[i, j] = (xTerm + yTerm) * expTerm

    kernel = kernel / np.sum(kernel)
    return kernel
