# encoding=UTF-8

# Copyright © 2009-2015 Jakub Wilk <jwilk@jwilk.net>
#
# This file is part of didjvu.
#
# didjvu is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# didjvu 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 General Public License
# for more details.

'''bridge to the Gamera framework'''

import collections
import ctypes
import math
import re
import sys
import warnings

from . import utils

try:
    from PIL import Image as PIL
except ImportError as ex:  # <no-coverage>
    utils.enhance_import_error(ex,
        'Python Imaging Library',
        'python-imaging',
        'http://www.pythonware.com/products/pil/'
    )
    raise
else:
    # Gamera (<< 3.4.0) still expects that PIL can be imported as Image
    sys.modules['Image'] = PIL

try:
    import gamera
except ImportError as ex:  # <no-coverage>
    utils.enhance_import_error(ex,
        'Gamera',
        'python-gamera',
        'http://gamera.informatik.hsnr.de/'
    )
    raise
del gamera  # quieten pyflakes

from gamera import __version__ as version
from gamera.core import load_image as _load_image
from gamera.core import init_gamera as _init
from gamera.core import Image, RGB, GREYSCALE, ONEBIT, Point, Dim, RGBPixel
from gamera.plugins.pil_io import from_pil as _from_pil
import gamera.args

def has_version(*req_version):
    return tuple(map(int, version.split('.'))) >= req_version

def load_image(filename):
    pil_image = PIL.open(filename)
    [xdpi, ydpi] = pil_image.info.get('dpi', (0, 0))
    if xdpi <= 1 or ydpi <= 1:
        # not reliable
        dpi = None
    else:
        dpi = int(round(
            math.hypot(xdpi, ydpi) /
            math.hypot(1, 1)
        ))
    try:
        if pil_image.format == 'TIFF':
            # Gamera handles only a few TIFF color modes correctly.
            # https://bugs.debian.org/784374
            gamera_modes = ['1', 'I;16', 'L', 'RGB']
        elif pil_image.format == 'PNG':
            # Gamera doesn't handle 16-bit grayscale PNG images correctly.
            # https://groups.yahoo.com/neo/groups/gamera-devel/conversations/messages/2425
            gamera_modes = ['1', 'L', 'RGB']
        else:
            gamera_modes = []
        if pil_image.mode not in gamera_modes:
            raise IOError
        # Gamera supports more TIFF compression formats that PIL.
        # https://mail.python.org/pipermail/image-sig/2003-July/002354.html
        image = _load_image(filename)
    except IOError:
        # Gamera supports importing only 8-bit and RGB from PIL:
        if pil_image.mode[:2] in ('1', '1;', 'I', 'I;', 'L;'):
            pil_image = pil_image.convert('L')
        elif pil_image.mode not in ('RGB', 'L'):
            pil_image = pil_image.convert('RGB')
        assert pil_image.mode in ('RGB', 'L')
        try:
            # Gamera still uses tostring(), which was deprecated,
            # and finally removed in Pillow 3.0.0.
            # https://pillow.readthedocs.io/en/3.0.x/releasenotes/3.0.0.html#deprecated-methods
            pil_image.tostring = pil_image.tobytes
        except AttributeError:  # <no-coverage>
            pass
        image = _from_pil(pil_image)
    image.dpi = dpi
    return image

class Argument(object):

    _type_map = {
        gamera.args.Int: int,
        gamera.args.Real: float,
        gamera.args.Check: bool,
    }

    def __init__(self, arg):
        self.name = arg.name.replace(' ', '-').replace('_', '-')
        for gtype, ptype in self._type_map.iteritems():
            if isinstance(arg, gtype):
                self.type = ptype
                break
        else:
            raise NotImplementedError(
                'argument {0}: unsupported type {1!r}'.format(self.name, arg)
            )  # <no-coverage>
        if self.type in (int, float):
            [self.min, self.max] = arg.rng
            if self.min == -gamera.args.DEFAULT_MAX_ARG_NUMBER:
                self.min = None
            if self.max == gamera.args.DEFAULT_MAX_ARG_NUMBER:
                self.max = None
        else:
            self.min = self.max = None
        self.has_default = arg.has_default
        self.default = arg.default
        if isinstance(self.default, gamera.args.CNoneDefault):
            self.default = None
        elif self.has_default:
            if not isinstance(self.default, self.type):
                raise TypeError(
                    'argument {0}: type({1!r}) should be {2}'.format(self.name, self.default, self.type.__name__)
                )  # <no-coverage>
        else:
            self.default = None

class Plugin(object):

    def __init__(self, plugin, name):
        self._plugin = plugin
        self._pixel_types = plugin.self_type.pixel_types
        self._method = None
        self.name = name
        if sys.version_info >= (2, 7):
            self.args = collections.OrderedDict()
        else:  # <no-coverage>
            # Oh well.
            self.args = {}
        for arg in plugin.args:
            if arg.name == 'storage format':
                continue
            arg = Argument(arg)
            self.args[arg.name] = arg

    def __call__(self, image, **kwargs):
        kwargs = dict(
            (key.replace('-', '_'), value)
            for key, value
            in kwargs.iteritems()
        )
        pixel_types = self._pixel_types
        if image.data.pixel_type not in pixel_types:
            if RGB in pixel_types:
                image = image.to_rgb()
            elif GREYSCALE in pixel_types:
                image = image.to_greyscale()
            else:
                raise NotImplementedError(
                    'method {method} does not support pixel type {pt}'.format(method=self.name, pt=image.pixel_type_name)
                )  # <no-coverage>
        assert image.data.pixel_type in pixel_types
        if self._method is None:
            self._method = self._plugin()
        return self._method(image, **kwargs)

def _load_methods():
    replace_suffix = re.compile('_threshold$').sub
    class _methods:
        from gamera.plugins.threshold import abutaleb_threshold
        from gamera.plugins.threshold import bernsen_threshold
        from gamera.plugins.threshold import djvu_threshold
        from gamera.plugins.threshold import otsu_threshold
        from gamera.plugins.threshold import threshold as global_threshold
        from gamera.plugins.threshold import tsai_moment_preserving_threshold as tsai
        # TODO: from gamera.plugins.binarization import gatos_threshold
        from gamera.plugins.binarization import niblack_threshold
        from gamera.plugins.binarization import sauvola_threshold
        from gamera.plugins.binarization import white_rohrer_threshold
        if has_version(3, 3, 1):
            from gamera.plugins.binarization import shading_subtraction
        if has_version(3, 4, 0):
            from gamera.plugins.binarization import brink_threshold
    methods = {}
    for name, plugin in vars(_methods).items():
        if name.startswith('_'):
            continue
        name = replace_suffix('', name)
        name = name.replace('_', '-')
        method = Plugin(plugin, name)
        methods[name] = method
    return methods

methods = _load_methods()

def to_pil_rgb(image):
    # About 20% faster than the standard .to_pil() method of Gamera 3.2.6.
    buffer = ctypes.create_string_buffer(3 * image.ncols * image.nrows)
    image.to_buffer(buffer)
    return PIL.frombuffer('RGB', (image.ncols, image.nrows), buffer, 'raw', 'RGB', 0, 1)

def to_pil_1bpp(image):
    if image.data.pixel_type != GREYSCALE:
        image = image.to_greyscale()
    return image.to_pil()

def _decref(o):  # <no-coverage>
    '''
    Forcibly decrease refcount of the object by 1.
    '''
    libc = ctypes.cdll.LoadLibrary(None)
    memmove = libc.memmove
    memmove.argtypes = [ctypes.py_object, ctypes.py_object, ctypes.c_size_t]
    memmove.restype = ctypes.py_object
    return memmove(o, None, 0)

def _monkeypatch_to_raw_string():  # <no-coverage>
    '''
    Monkey-patch to _to_raw_string plugin function to return objects with
    correct refcounts.
    '''
    from gamera.plugins import _string_io
    old_to_raw_string = _string_io._to_raw_string
    def fixed_to_raw_string(*args, **kwargs):
        o = old_to_raw_string(*args, **kwargs)
        assert sys.getrefcount(o) == 3
        o = _decref(o)
        assert sys.getrefcount(o) == 2
        return o
    _string_io._to_raw_string = fixed_to_raw_string
    Image._to_raw_string = fixed_to_raw_string

def init():
    sys.modules['numpy'] = None
    result = _init()
    test_image = Image((0, 0), (5, 5), RGB)
    test_string = test_image._to_raw_string()
    refcount = sys.getrefcount(test_string)
    if refcount == 3:  # <no-coverage>
        # See: https://tech.groups.yahoo.com/group/gamera-devel/message/2068
        warnings.warn(RuntimeWarning('Working around memory leak in the Gamera library'), stacklevel=2)
        _monkeypatch_to_raw_string()
        fixed_test_string = test_image._to_raw_string()
        assert sys.getrefcount(fixed_test_string) == 2
        assert test_string == fixed_test_string
    else:
        assert refcount == 2
    try:
        PIL.fromstring = PIL.frombytes
        # Gamera still uses fromstring(), which was deprecated,
        # and finally removed in Pillow 3.0.0.
        # https://pillow.readthedocs.io/en/3.0.x/releasenotes/3.0.0.html#deprecated-methods
    except AttributeError:
        pass
    return result

__all__ = [
    # classes:
    'Dim',
    'Image',
    'Point',
    'RGBPixel',
    # pixel types:
    'ONEBIT',
    'GREYSCALE',
    'RGB',
    # functions:
    'init',
    'load_image',
    'methods',
    'to_pil_1bpp',
    'to_pil_rgb',
]

# vim:ts=4 sts=4 sw=4 et
