#pragma once

/**
 * @file image.h
 * @brief Core Image class
 */

#include <cstddef>
#include <cstdint>
#include <vector>

namespace satdump
{
    namespace image
    {
        class Image
        {
        protected:
            // Image buffer
            size_t data_size = 0;
            size_t type_size = 0;
            void *d_data = nullptr;

            // Size and parameters
            int d_depth = 0;
            int d_maxv = 0;
            size_t d_width = 0;
            size_t d_height = 0;
            int d_channels = 0;

        public:
            // Metadata stuff
            void *metadata_obj = nullptr; // DO NOT USE DIRECTLY!
        protected:
            void copy_meta(const Image &img);

        public:
            /**
             * @brief Init null image, with no data buffer
             */
            Image();

            /**
             * @brief Init emtpy image, set to 0 but with an allocated data buffer
             * @param bit_depth depth of the image (usually 8 or 16)
             * @param width Image width in pixels
             * @param height Image height in pixels
             * @param channels Number of channels. See init() for more info
             */
            Image(int bit_depth, size_t width, size_t height, int channels);

            /**
             * @brief Init an image, reading the contents of the data buffer from
             * a provided buffer. This is useful to for example turn a uint16_t buffer
             * directly into an usable image.
             * @param buffer pointer to read from
             * @param bit_depth depth of the image (usually 8 or 16)
             * @param width Image width in pixels
             * @param height Image height in pixels
             * @param channels Number of channels. See init() for more info
             */
            Image(void *buffer, int bit_depth, size_t width, size_t height, int channels);

            Image(const Image &img); // Copy constructor
            ~Image();                // Destructor

            Image &operator=(const Image &img);

        public:
            /**
             * @brief Convert this image from B&W to RGB (if it is B&W / RGBA)
             */
            void to_rgb();

            /**
             * @brief Convert this image from to RGBA (if it is B&W / RGB)
             */
            void to_rgba();

            /**
             * @brief Convert to 8-bits. Returns the current image if it's already 8-bits
             */
            Image to8bits();

            /**
             * @brief Convert to 16-bits. Returns the current image if it's already 16-bits
             */
            Image to16bits();

            /**
             * @brief Converts to a specific target bit depth
             * @param bit_depth to convert to
             * @return converted image
             */
            Image to_depth(int bit_depth);

        public:
            /**
             * @brief Draw a B&W Image onto an image channel, or if channel
             * is kept at 0, RGB or RGBA images as well.
             * @param channel index. If 0, draws as color image
             * @param image the image to draw
             * @param x offset
             * @param y offset
             */
            void draw_image(int channel, Image image, int x = 0, int y = 0);

            /**
             * @brief Draw an alpha Image onto an image or if channel
             * is kept at 0, RGB or RGBA images as well.
             * Respects the image's Alpha channel.
             * @param channel index. If 0, draws as color image
             * @param image the image to draw
             * @param x offset
             * @param y offset
             */
            void draw_image_alpha(Image image, int x = 0, int y = 0);

            /**
             * @brief Set a pixel's color
             * @param x position
             * @param y position
             * @param color of the pixel
             */
            void draw_pixel(size_t x, size_t y, std::vector<double> color);

            /**
             * @brief Get a pixel's color
             * @param x position
             * @param y position
             * @return color of the pixel
             */
            std::vector<double> get_pixel_color(size_t x, size_t y);

            /**
             * @brief Draw a line with Bresenham's algorithm
             * @param x0 line start X position
             * @param y0 line start Y position
             * @param x1 line stop X position
             * @param y1 line stop Y position
             * @param color of the line
             */
            void draw_line(int x0, int y0, int x1, int y1, std::vector<double> color);

            /**
             * @brief Draw an anti-aliased line
             * @param x0 line start X position
             * @param y0 line start Y position
             * @param x1 line stop X position
             * @param y1 line stop Y position
             * @param color of the line
             */
            void draw_aa_line(float x0, float y0, float x1, float y1, std::vector<double> color);

            /**
             * @brief Draw a circle with Bresenham's Midpoint algorithm
             * @param x0 center X position
             * @param y0 center Y position
             * @param radius in pixels
             * @param color of the circle
             * @param fill should be true if the circle is to be filled
             */
            void draw_circle(int x0, int y0, int radius, std::vector<double> color, bool fill = false);

            /**
             * @brief Draw a rectangle onto the image
             * @param x0 rectangle start X position
             * @param y0 rectangle start Y position
             * @param x1 rectangle stop X position
             * @param y1 rectangle stop Y position
             * @param color of the rectangle
             * @param fill should be true if the rectangle is to be filled
             */
            void draw_rectangle(int x0, int y0, int x1, int y1, std::vector<double> color, bool fill = true);

        public:
            /**
             * @brief Init (empty, filled with zeroes) image buffer
             * @param bit_depth Image bit depth. Usually should be 8 or 16
             * @param width Image width in pixels
             * @param height Image height in pixels
             * @param channels Number of channels (1 = Grayscale, 2 = GA, 3 = RGB, 4 = RGBA)
             */
            void init(int bit_depth, size_t width, size_t height, int channels);

            /**
             * @brief Delete current buffer (and set size to 0)
             */
            void clear();

        public:
            /**
             * @brief Clamp input value to what this image can handle
             * @param input the input value
             * @return clamped value (depends on bit depth)
             */
            int clamp(int input);

            /**
             * @brief Clamp input value to what this image can handle as float (0, 1)
             * @param input the input value
             * @return value between 0 and 1
             */
            double clampf(double input);

            /**
             * @brief Returns the RAW void pointer. MUST ONLY BE USED IF YOU KNOW WHAT YOU ARE DOING!
             * @return the void pointer. Be. Careful.
             */
            void *raw_data() const { return d_data; }

            /**
             * @brief Returns image bit depth
             * @return bit depth
             */
            int depth() const { return d_depth; }

            /**
             * @brief Returns image type size (in bytes)
             * @return type size, bytes
             */
            int typesize() const { return type_size; }

            /**
             * @brief Returns image width
             * @return image width in pixels
             */
            size_t width() const { return d_width; }

            /**
             * @brief Returns image height
             * @return image height in pixels
             */
            size_t height() const { return d_height; }

            /**
             * @brief Returns image channel count
             * @return number of image channels
             */
            int channels() const { return d_channels; }

            /**
             * @brief Returns image data size
             * @return Image data size, in pixels
             */
            size_t size() const { return data_size; }

            /**
             * @brief Returns image max value
             * @return maximum int value of an image pixels
             */
            int maxval() const { return d_maxv; }

        public:
            /**
             * @brief Crop an image region. Must be with x0 <= x1 and y0 <= y1
             * @param x0 x position of the first pixel
             * @param y0 y position of the first pixel
             * @param x1 x position of the second pixel
             * @param y1 y position of the second pixel
             */
            void crop(int x0, int y0, int x1, int y1);

            /**
             * @brief Crop an image region, x axis only
             * @param x0 x position of the first column
             * @param x1 x position of the second column
             */
            void crop(int x0, int x1);

            /**
             * @brief Crop an image region. Must be with x0 <= x1 and y0 <= y1.
             * Provides image as a return value
             * @param x0 x position of the first pixel
             * @param y0 y position of the first pixel
             * @param x1 x position of the second pixel
             * @param y1 y position of the second pixel
             * @return Cropped image
             */
            Image crop_to(int x0, int y0, int x1, int y1);

            /**
             * @brief Crop an image region, x asus only
             * Provides image as a return value
             * @param x0 x position of the first column
             * @param x1 x position of the second column
             * @return Cropped image
             */
            Image crop_to(int x0, int x1);

        public:
            /**
             * @brief Mirror the image
             * @param x mirror on X axis
             * @param y mirror on Y axis
             */
            void mirror(bool x, bool y);

        public:
            /**
             * @brief Resize image, using a simple pixel scaling attribution
             * (not the best, but fast)
             * @param width x size to resize to
             * @param height y size to resize to
             */
            void resize(int width, int height);

            /**
             * @brief Resize image, to another image. Same as resize otherwise
             * @param width x size to resize to
             * @param height y size to resize to
             * @return output image
             */
            Image resize_to(int width, int height);

            /**
             * @brief Resize image, using a bilinear algorithm
             * @param width x size to resize to
             * @param height y size to resize to
             * @param text_mode makes any pixel that is no 0 the maximum value of the pixel
             */
            void resize_bilinear(int width, int height, bool text_mode = false);

            /**
             * @brief Get a pixel from the image, but with the option to interpolate
             * fractional pixels.
             * @param channel index of the channel to use
             * @param x position
             * @param y position
             */
            int get_pixel_bilinear(int channel, double x, double y);

        public:
            /**
             * @brief Fill image with a single value
             * @param val the value
             */
            void fill(int val);

            /**
             * @brief Fill image with a color
             * @param color the color, must match the number of image channels
             */
            void fill_color(std::vector<double> color);

        public:
            /**
             * @brief Standard int set. No bound check!
             * Variants to pass a channel index and X / Y position
             * are also available.
             * @param p index of the pixel to set
             * @param v value to set
             */
            void set(size_t p, int v)
            {
                if (d_depth > 8)
                    ((uint16_t *)d_data)[p] = v;
                else
                    ((uint8_t *)d_data)[p] = v;
            }

            /**
             * @brief Standard int get. No bound check!
             * Variants to pass a channel index and X / Y position
             * are also available.
             * @param p index of the pixel to get
             * @return pixel value
             */
            int get(size_t p)
            {
                if (d_depth > 8)
                    return ((uint16_t *)d_data)[p];
                else
                    return ((uint8_t *)d_data)[p];
            }

            /**
             * @brief Standard float set. No bound check!
             * Variants to pass a channel index and X / Y position
             * are also available.
             * @param p index of the pixel to set
             * @param v value to set between 0 and 1
             */
            void setf(size_t p, double v)
            {
                if (d_depth > 8)
                    ((uint16_t *)d_data)[p] = v * (double)d_maxv;
                else
                    ((uint8_t *)d_data)[p] = v * (double)d_maxv;
            }

            /**
             * @brief Standard float get. No bound check!
             * Variants to pass a channel index and X / Y position
             * are also available.
             * @param p index of the pixel to get
             * @return pixel value between 0 and 1
             */
            double getf(size_t p)
            {
                if (d_depth > 8)
                    return (double)((uint16_t *)d_data)[p] / (double)d_maxv;
                else
                    return (double)((uint8_t *)d_data)[p] / (double)d_maxv;
            }

            // Easier int set/get
            void set(size_t channel, size_t p, int v) { set(channel * d_width * d_height + p, v); }
            int get(size_t channel, size_t p) { return get(channel * d_width * d_height + p); }
            void set(size_t channel, size_t x, size_t y, int v) { set(channel * d_width * d_height + y * d_width + x, v); }
            int get(size_t channel, size_t x, size_t y) { return get(channel * d_width * d_height + y * d_width + x); }

            // Easier double setf/getf
            void setf(size_t channel, size_t p, double v) { setf(channel * d_width * d_height + p, v); }
            double getf(size_t channel, size_t p) { return getf(channel * d_width * d_height + p); }
            void setf(size_t channel, size_t x, size_t y, double v) { setf(channel * d_width * d_height + y * d_width + x, v); }
            double getf(size_t channel, size_t x, size_t y) { return getf(channel * d_width * d_height + y * d_width + x); }
        };

        /**
         * @brief Copy part of an image over. Safe way to do it with variable bit
         * depths & channels
         * @param img1 image to copy to
         * @param pos1 position to copy to
         * @param img2 image to copy from
         * @param pos2 position to copy from
         * @param px_size number of pixels to copy
         */
        void imemcpy(Image &img1, size_t pos1, Image &img2, size_t pos2, size_t px_size);

        /**
         * @brief Convert an image to a 8-bit RGBA buffer for OpenGL/etc textures
         * @param img image to convert
         * @param output output buffer
         */
        void image_to_rgba(Image &img, uint32_t *output);
    } // namespace image
} // namespace satdump

// TODOREWORK!!!!! REMOVE. Temporary namespace hack until everything is moved over!
namespace image
{
    using namespace satdump::image;
} // namespace image