// Copyright (C) 2011  Davis E. King (davis@dlib.net)
// License: Boost Software License   See LICENSE.txt for the full license.
#ifndef DLIB_LABEL_CONNeCTED_BLOBS_H_
#define DLIB_LABEL_CONNeCTED_BLOBS_H_

#include "label_connected_blobs_abstract.h"
#include "../geometry.h"
#include <stack>
#include <vector>
#include "thresholding.h"
#include "assign_image.h"
#include <queue>

namespace dlib
{

// ----------------------------------------------------------------------------------------

    struct neighbors_24
    {
        void operator() (
            const point& p,
            std::vector<point>& neighbors
        ) const
        {
            for (long i = -2; i <= 2; ++i)
                for (long j = -2; j <= 2; ++j)
                    if (i!=0||j!=0)
                        neighbors.emplace_back(p.x()+i,p.y()+j);
        }
    };

    struct neighbors_8 
    {
        void operator() (
            const point& p,
            std::vector<point>& neighbors
        ) const
        {
            neighbors.emplace_back(p.x()+1,p.y()+1);
            neighbors.emplace_back(p.x()+1,p.y()  );
            neighbors.emplace_back(p.x()+1,p.y()-1);

            neighbors.emplace_back(p.x(),p.y()+1);
            neighbors.emplace_back(p.x(),p.y()-1);

            neighbors.emplace_back(p.x()-1,p.y()+1);
            neighbors.emplace_back(p.x()-1,p.y()  );
            neighbors.emplace_back(p.x()-1,p.y()-1);
        }
    };

    struct neighbors_4 
    {
        void operator() (
            const point& p,
            std::vector<point>& neighbors
        ) const
        {
            neighbors.emplace_back(p.x()+1,p.y());
            neighbors.emplace_back(p.x()-1,p.y());
            neighbors.emplace_back(p.x(),p.y()+1);
            neighbors.emplace_back(p.x(),p.y()-1);
        }
    };

// ----------------------------------------------------------------------------------------

    struct connected_if_both_not_zero
    {
        template <typename image_type>
        bool operator() (
            const image_type& img,
            const point& a,
            const point& b
        ) const
        {
            return (img[a.y()][a.x()] != 0 && img[b.y()][b.x()] != 0);
        }
    };

    struct connected_if_equal
    {
        template <typename image_type>
        bool operator() (
            const image_type& img,
            const point& a,
            const point& b
        ) const
        {
            return (img[a.y()][a.x()] == img[b.y()][b.x()]);
        }
    };

// ----------------------------------------------------------------------------------------

    struct zero_pixels_are_background
    {
        template <typename image_type>
        bool operator() (
            const image_type& img,
            const point& p
        ) const
        {
            return img[p.y()][p.x()] == 0;
        }

    };

    struct nothing_is_background 
    {
        template <typename image_type>
        bool operator() (
            const image_type&, 
            const point& 
        ) const
        {
            return false;
        }

    };

// ----------------------------------------------------------------------------------------

    template <
        typename image_type,
        typename label_image_type,
        typename background_functor_type,
        typename neighbors_functor_type,
        typename connected_functor_type
        >
    unsigned long label_connected_blobs (
        const image_type& img_,
        const background_functor_type& is_background,
        const neighbors_functor_type&  get_neighbors,
        const connected_functor_type&  is_connected,
        label_image_type& label_img_
    )
    {
        // make sure requires clause is not broken
        DLIB_ASSERT(is_same_object(img_, label_img_) == false,
            "\t unsigned long label_connected_blobs()"
            << "\n\t The input image and output label image can't be the same object."
            );

        const_image_view<image_type> img(img_);
        image_view<label_image_type> label_img(label_img_);

        std::stack<point> neighbors;
        label_img.set_size(img.nr(), img.nc());
        assign_all_pixels(label_img, 0);
        unsigned long next = 1;

        if (img.size() == 0)
            return 0;

        const rectangle area = get_rect(img);

        std::vector<point> window;

        for (long r = 0; r < img.nr(); ++r)
        {
            for (long c = 0; c < img.nc(); ++c)
            {
                // skip already labeled pixels or background pixels
                if (label_img[r][c] != 0 || is_background(img,point(c,r)))
                    continue;

                label_img[r][c] = next;

                // label all the neighbors of this point 
                neighbors.push(point(c,r));
                while (neighbors.size() > 0)
                {
                    const point p = neighbors.top();
                    neighbors.pop();

                    window.clear();
                    get_neighbors(p, window);

                    for (unsigned long i = 0; i < window.size(); ++i)
                    {
                        if (area.contains(window[i]) &&                     // point in image.
                            !is_background(img,window[i]) &&                // isn't background.
                            label_img[window[i].y()][window[i].x()] == 0 && // haven't already labeled it.
                            is_connected(img, p, window[i]))                // it's connected.
                        {
                            label_img[window[i].y()][window[i].x()] = next;
                            neighbors.push(window[i]);
                        }
                    }
                }

                ++next;
            }
        }

        return next;
    }

// ----------------------------------------------------------------------------------------

    template <
        typename in_image_type,
        typename out_image_type
        >
    unsigned long label_connected_blobs_watershed (
        const in_image_type& img_,
        out_image_type& labels_,
        typename pixel_traits<typename image_traits<in_image_type>::pixel_type>::basic_pixel_type background_thresh,
        const double smoothing = 0
    )
    {
        // make sure requires clause is not broken
        DLIB_ASSERT(is_same_object(img_, labels_) == false,
            "\t unsigned long segment_image_watersheds()"
            << "\n\t The input images can't be the same object."
            );

        using label_pixel_type = typename image_traits<out_image_type>::pixel_type;

        DLIB_ASSERT(smoothing >= 0);
        COMPILE_TIME_ASSERT(is_unsigned_type<label_pixel_type>::value);


        struct watershed_points
        {
            watershed_points() = default;
            watershed_points(const point& p_, float score_, label_pixel_type label_): p(p_), score(score_), label(label_) {}

            point p;
            float score = 0;
            label_pixel_type label = std::numeric_limits<label_pixel_type>::max();

            bool is_seed() const { return label == std::numeric_limits<label_pixel_type>::max(); }

            bool operator< (const watershed_points& rhs) const
            {
                // If two pixels have the same score then we take the one with the smallest 
                // label out of the priority queue first.  We do this so that seed points
                // that are downhill from some larger blob will be consumed by it if they
                // haven't grown before the larger blob's flooding reaches them.  Doing
                // this helps a lot to avoid spuriously splitting blobs.
                if (score == rhs.score)
                {
                    return label > rhs.label;
                }
                return score < rhs.score;
            }

        };

        const_image_view<in_image_type> img(img_);
        image_view<out_image_type> labels(labels_);
        
        labels.set_size(img.nr(), img.nc());
        // Initially, all pixels have the background label of 0.
        assign_all_pixels(labels, 0);

        std::priority_queue<watershed_points> next;


        // Note that we never blur the image values we use to check against the
        // background_thresh.  We do however blur, if smoothing!=0, the pixel values used
        // to do the watershed.
        in_image_type img2_;
        if (smoothing != 0)
            gaussian_blur(img_, img2_, smoothing); 
        const_image_view<in_image_type> img2view(img2_);
        // point us at img2 if we are doing smoothing, otherwise point us at the input
        // image.
        const auto& img2 = smoothing!=0?img2view:img;

        // first find all the local maxima 
        for (long r = 1; r+1 < img.nr(); ++r)
        {
            for (long c = 1; c+1 < img.nc(); ++c)
            {

                if (img[r][c] < background_thresh)
                    continue;

                auto val = img2[r][c];
                // if img2[r][c] isn't a local maximum then skip it
                if (val < img2[r+1][c] ||
                    val < img2[r-1][c] ||
                    val < img2[r][c+1] ||
                    val < img2[r][c-1]
                )
                {
                    continue;
                }

                next.push(watershed_points(point(c,r), val, std::numeric_limits<label_pixel_type>::max()));
            }
        }


        const rectangle area = get_rect(img);


        label_pixel_type next_label = 1;


        std::vector<point> neighbors;
        neighbors_8 get_neighbors;
        while(next.size() > 0)
        {
            auto p = next.top();
            next.pop();

            label_pixel_type label;
            // If the next pixel is a seed of a new blob and is still labeled as a
            // background pixel (i.e. it hasn't been flooded over by a neighboring blob and
            // consumed by it) then we create a new label for this new blob.
            if (p.is_seed() && labels[p.p.y()][p.p.x()] == 0)
            {
                label = next_label++;
                labels[p.p.y()][p.p.x()] = label;
            }
            else
            {
                label = p.label;
            }


            neighbors.clear();
            get_neighbors(p.p, neighbors);
            for (auto& n : neighbors)
            {
                if (!area.contains(n) || labels[n.y()][n.x()] != 0 || img[n.y()][n.x()] < background_thresh)
                    continue;

                labels[n.y()][n.x()] = label;
                next.push(watershed_points(n, img2[n.y()][n.x()], label));
            }
        }

        return next_label;
    }

    template <
        typename in_image_type,
        typename out_image_type
        >
    unsigned long label_connected_blobs_watershed (
        const in_image_type& img,
        out_image_type& labels
    )
    {
        return label_connected_blobs_watershed(img, labels, partition_pixels(img));
    }

// ----------------------------------------------------------------------------------------

}

#endif // DLIB_LABEL_CONNeCTED_BLOBS_H_

