#   Copyright (C) 2012, Almar Klein
#
#   This code is subject to the (new) BSD license:
#
#   Redistribution and use in source and binary forms, with or without
#   modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the <organization> nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Module images2ims

Use PIL to create a series of images.

"""

import os
from operator import itemgetter
from string import digits
from pathlib import Path

try:
    import numpy as np
except ImportError:
    np = None

try:
    import PIL
except ImportError:
    PIL = None


def checkImages(images):
    """Check numpy images and correct intensity range etc.
    The same for all movie formats.

    :param images:
    """
    # Init results
    images2 = []

    for im in images:
        if PIL and isinstance(im, PIL.Image.Image):
            # We assume PIL images are alright
            images2.append(im)

        elif np and isinstance(im, np.ndarray):
            # Check and convert dtype
            if im.dtype == np.uint8:
                images2.append(im)  # Ok
            elif im.dtype in {np.float32, np.float64}:
                theMax = im.max()
                if 128 < theMax < 300:
                    pass  # assume 0:255
                else:
                    im = im.copy()
                    im[im < 0] = 0
                    im[im > 1] = 1
                    im *= 255
                images2.append(im.astype(np.uint8))
            else:
                im = im.astype(np.uint8)
                images2.append(im)
            # Check size
            if im.ndim == 2:
                pass  # ok
            elif im.ndim == 3:
                if im.shape[2] not in {3, 4}:
                    msg = "This array can not represent an image."
                    raise ValueError(msg)
            else:
                msg = "This array can not represent an image."
                raise ValueError(msg)
        else:
            raise ValueError("Invalid image type: " + str(type(im)))

    # Done
    return images2


def _getFilenameParts(filename):
    if "*" in filename:
        return tuple(filename.split("*", 1))
    return os.path.splitext(filename)


def _getFilenameWithFormatter(filename, N):
    # Determine sequence number formatter
    formatter = "%04i"
    if N < 10:
        formatter = "%i"
    elif N < 100:
        formatter = "%02i"
    elif N < 1000:
        formatter = "%03i"

    # Insert sequence number formatter
    part1, part2 = _getFilenameParts(filename)
    return part1 + formatter + part2


def _getSequenceNumber(filename, part1, part2):
    # Get string bit
    seq = filename[len(part1) : -len(part2)]
    # Get all numeric chars
    seq2 = ""
    for c in seq:
        if c not in digits:
            break
        seq2 += c
    # Make int and return
    return int(seq2)


def writeIms(filename, images):
    """Export movie to a series of image files. If the filenenumber
    contains an asterix, a sequence number is introduced at its
    location. Otherwise the sequence number is introduced right
    before the final dot.

    To enable easy creation of a new directory with image files,
    it is made sure that the full path exists.

    Images should be a list consisting of PIL images or numpy arrays.
    The latter should be between 0 and 255 for integer types, and
    between 0 and 1 for float types.

    :param filename:
    :param images:
    """

    # Check PIL
    if PIL is None:
        msg = "Need PIL to write series of image files."
        raise RuntimeError(msg)

    # Check images
    images = checkImages(images)

    # Get dirname and filename
    filename = os.path.abspath(filename)
    dirname, filename = os.path.split(filename)

    # Create dir(s) if we need to
    Path(dirname).mkdir(parents=True, exist_ok=True)

    # Insert formatter
    filename = _getFilenameWithFormatter(filename, len(images))

    # Write
    seq = 0
    for frame in images:
        seq += 1
        # Get filename
        fname = os.path.join(dirname, filename % seq)
        # Write image
        if np and isinstance(frame, np.ndarray):
            frame = PIL.Image.fromarray(frame)
        frame.save(fname)


def readIms(filename, asNumpy=True):
    """Read images from a series of images in a single directory. Returns a
    list of numpy arrays, or, if asNumpy is false, a list if PIL images.

    :param filename:
    :param bool asNumpy:
    """

    # Check PIL
    if PIL is None:
        msg = "Need PIL to read a series of image files."
        raise RuntimeError(msg)

    # Check NumPy
    if asNumpy and np is None:
        msg = "Need NumPy to return numpy arrays."
        raise RuntimeError(msg)

    # Get dirname and filename
    filename = os.path.abspath(filename)
    dirname, filename = os.path.split(filename)

    # Check dir exists
    if not Path(dirname).is_dir():
        raise OSError("Directory not found: " + str(dirname))

    # Get two parts of the filename
    part1, part2 = _getFilenameParts(filename)

    # Init images
    images = []

    # Get all files in directory
    for fname in os.listdir(dirname):
        if fname.startswith(part1) and fname.endswith(part2):
            # Get sequence number
            nr = _getSequenceNumber(fname, part1, part2)
            # Get Pil image and store copy (to prevent keeping the file)
            im = PIL.Image.open(os.path.join(dirname, fname))
            images.append((im.copy(), nr))

    # Sort images
    images.sort(key=itemgetter(1))
    images = [im[0] for im in images]

    # Convert to numpy if needed
    if asNumpy:
        images2 = images
        images = []
        for im in images2:
            # Make without palette
            if im.mode == "P":
                im = im.convert()
            # Make numpy array
            a = np.asarray(im)
            if len(a.shape) == 0:
                msg = "Too little memory to convert PIL image to array"
                raise MemoryError(msg)
            # Add
            images.append(a)

    # Done
    return images
