package mara.mybox.image;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import mara.mybox.data.ConvolutionKernel;
import mara.mybox.tools.MatrixTools;
import static mara.mybox.value.AppVariables.logger;

/**
 * @Author Mara
 * @CreateDate 2018-11-10 19:35:49
 * @Version 1.0
 * @Description
 * @License Apache License Version 2.0
 */
public class ImageConvolution extends PixelsOperation {

    protected ConvolutionKernel kernel;
    protected int matrixWidth, matrixHeight, edge_op, radiusX, radiusY, maxX, maxY;
    protected boolean keepOpacity, isEmboss, isGray;
    protected float[][] matrix;
    protected int[][] intMatrix;
    protected int intScale;

    public static enum SmoothAlgorithm {
        GaussianBlur, AverageBlur
//        , MotionBlur
    }

    public static enum SharpenAlgorithm {
        UnsharpMasking, EightNeighborLaplace, FourNeighborLaplace
    }

    public ImageConvolution() {
        this.operationType = OperationType.Convolution;
    }

    public static ImageConvolution create() {
        return new ImageConvolution();
    }

    public ImageConvolution setKernel(ConvolutionKernel kernel) {
        this.kernel = kernel;
        matrix = kernel.getMatrix();
        matrixWidth = matrix[0].length;
        matrixHeight = matrix.length;
        intMatrix = new int[matrixHeight][matrixWidth];
        intScale = 10000; // Run integer calcualation instead of float/double calculation
        for (int matrixY = 0; matrixY < matrixHeight; matrixY++) {
            for (int matrixX = 0; matrixX < matrixWidth; matrixX++) {
                intMatrix[matrixY][matrixX] = Math.round(matrix[matrixY][matrixX] * intScale);
            }
        }
        edge_op = kernel.getEdge();
        radiusX = matrixWidth / 2;
        radiusY = matrixHeight / 2;
        maxX = image.getWidth() - 1;
        maxY = image.getHeight() - 1;
        isEmboss = (kernel.getType() == ConvolutionKernel.Convolution_Type.EMBOSS);
        isGray = (kernel.getGray() > 0);
        keepOpacity = (kernel.getType() != ConvolutionKernel.Convolution_Type.EMBOSS
                && kernel.getType() != ConvolutionKernel.Convolution_Type.EDGE_DETECTION);
        return this;
    }

    @Override
    public BufferedImage operate() {
        if (image == null || kernel == null || kernel.getMatrix() == null) {
            return image;
        }
        return super.operate();
    }

    @Override
    public BufferedImage operateImage() {
        if (image == null || operationType == null) {
            return image;
        }
        if (scope == null || scope.getScopeType() == ImageScope.ScopeType.All) {
            return applyConvolution(image, kernel);

        }
        return super.operateImage();
    }

    @Override
    protected Color operatePixel(BufferedImage target, Color color, int x, int y) {
        int pixel = image.getRGB(x, y);
        if (x < radiusX || x + radiusX > maxX
                || y < radiusY || y + radiusY > maxY) {
            if (edge_op == ConvolutionKernel.Edge_Op.COPY) {
                target.setRGB(x, y, pixel);
                return new Color(pixel, true);
            }
        }
        Color newColor = applyConvolution(x, y);
        if (isEmboss) {
            int v = 128, red, blue, green;
            red = Math.min(Math.max(newColor.getRed() + v, 0), 255);
            green = Math.min(Math.max(newColor.getGreen() + v, 0), 255);
            blue = Math.min(Math.max(newColor.getBlue() + v, 0), 255);
            newColor = new Color(red, green, blue, newColor.getAlpha());
            if (isGray) {
                newColor = ImageColor.RGB2Gray(newColor);
            }
        }
        target.setRGB(x, y, newColor.getRGB());
        return newColor;
    }

    public Color applyConvolution(int x, int y) {
        try {
            int red = 0, green = 0, blue = 0, opacity = 0;
            int convolveX, convolveY;
            for (int matrixY = 0; matrixY < matrixHeight; matrixY++) {
                for (int matrixX = 0; matrixX < matrixWidth; matrixX++) {
                    convolveX = x - radiusX + matrixX;
                    convolveY = y - radiusY + matrixY;
                    if (convolveX < 0 || convolveX > maxX || convolveY < 0 || convolveY > maxY) {
                        if (edge_op == ConvolutionKernel.Edge_Op.MOD) {
                            convolveX = (convolveX + imageWidth) % imageWidth;
                            convolveY = (convolveY + imageHeight) % imageHeight;
                        } else {
                            continue; // Fill_zero
                        }
                    }
                    Color color = new Color(image.getRGB(convolveX, convolveY));
                    red += color.getRed() * intMatrix[matrixY][matrixX];
                    green += color.getGreen() * intMatrix[matrixY][matrixX];
                    blue += color.getBlue() * intMatrix[matrixY][matrixX];
                    if (keepOpacity) {
                        opacity += color.getAlpha() * intMatrix[matrixY][matrixX];
                    }
                }
            }
            red = Math.min(Math.max(red / intScale, 0), 255);
            green = Math.min(Math.max(green / intScale, 0), 255);
            blue = Math.min(Math.max(blue / intScale, 0), 255);
            if (keepOpacity) {
                opacity = Math.min(Math.max(opacity / intScale, 0), 255);
            } else {
                opacity = 255;
            }
            Color color = new Color(red, green, blue, opacity);
            return color;
        } catch (Exception e) {
            logger.error(e.toString());
            return null;
        }

    }

    public static BufferedImage applyConvolution(BufferedImage source,
            ConvolutionKernel convolutionKernel) {
        BufferedImage clearedSource;
        int type = convolutionKernel.getType();
        if (type == ConvolutionKernel.Convolution_Type.EDGE_DETECTION
                || type == ConvolutionKernel.Convolution_Type.EMBOSS) {
            clearedSource = ImageManufacture.removeAlpha(source);
        } else {
            clearedSource = source;
        }
        float[] k = MatrixTools.matrix2Array(convolutionKernel.getMatrix());
        if (k == null) {
            return clearedSource;
        }
        int w = convolutionKernel.getWidth();
        int h = convolutionKernel.getHeight();
        Kernel kernel = new Kernel(w, h, k);
        ConvolveOp imageOp;
        if (convolutionKernel.getEdge() == ConvolutionKernel.Edge_Op.COPY) {
            imageOp = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
        } else {
            imageOp = new ConvolveOp(kernel, ConvolveOp.EDGE_ZERO_FILL, null);
        }
        BufferedImage target = applyConvolveOp(clearedSource, imageOp);
        if (type == ConvolutionKernel.Convolution_Type.EMBOSS) {
            PixelsOperation pixelsOperation = PixelsOperation.create(target, null,
                    OperationType.RGB, ColorActionType.Increase);
            pixelsOperation.setIntPara1(128);
            target = pixelsOperation.operate();
            if (convolutionKernel.getGray() > 0) {
                target = ImageGray.byteGray(target);
            }
        }
        return target;
    }

    public static BufferedImage applyConvolveOp(BufferedImage source, ConvolveOp imageOp) {
        if (source == null || imageOp == null) {
            return source;
        }
        int width = source.getWidth();
        int height = source.getHeight();
        int imageType = source.getType();
        if (imageType == BufferedImage.TYPE_CUSTOM) {
            imageType = BufferedImage.TYPE_INT_ARGB;
        }
        BufferedImage target = new BufferedImage(width, height, imageType);
        imageOp.filter(source, target);
        return target;
    }

    /*
        get/set
     */
    @Override
    public ImageConvolution setImage(BufferedImage image) {
        this.image = image;
        return this;
    }

    @Override
    public ImageConvolution setImage(Image image) {
        this.image = SwingFXUtils.fromFXImage(image, null);
        return this;
    }

    @Override
    public ImageConvolution setScope(ImageScope scope) {
        this.scope = scope;
        return this;
    }

    public ConvolutionKernel getKernel() {
        return kernel;
    }

    public int[][] getIntMatrix() {
        return intMatrix;
    }

    public ImageConvolution setIntMatrix(int[][] intMatrix) {
        this.intMatrix = intMatrix;
        return this;
    }

    public int getIntScale() {
        return intScale;
    }

    public ImageConvolution setSum(int sum) {
        this.intScale = sum;
        return this;
    }

    public int getMatrixWidth() {
        return matrixWidth;
    }

    public ImageConvolution setMatrixWidth(int matrixWidth) {
        this.matrixWidth = matrixWidth;
        return this;
    }

    public int getMatrixHeight() {
        return matrixHeight;
    }

    public ImageConvolution setMatrixHeight(int matrixHeight) {
        this.matrixHeight = matrixHeight;
        return this;
    }

    public int getEdge_op() {
        return edge_op;
    }

    public ImageConvolution setEdge_op(int edge_op) {
        this.edge_op = edge_op;
        return this;
    }

    public int getRadiusX() {
        return radiusX;
    }

    public ImageConvolution setRadiusX(int radiusX) {
        this.radiusX = radiusX;
        return this;
    }

    public int getRadiusY() {
        return radiusY;
    }

    public ImageConvolution setRadiusY(int radiusY) {
        this.radiusY = radiusY;
        return this;
    }

    public int getMaxX() {
        return maxX;
    }

    public ImageConvolution setMaxX(int maxX) {
        this.maxX = maxX;
        return this;
    }

    public int getMaxY() {
        return maxY;
    }

    public ImageConvolution setMaxY(int maxY) {
        this.maxY = maxY;
        return this;
    }

    public boolean isKeepOpacity() {
        return keepOpacity;
    }

    public ImageConvolution setKeepOpacity(boolean keepOpacity) {
        this.keepOpacity = keepOpacity;
        return this;
    }

    public boolean isIsEmboss() {
        return isEmboss;
    }

    public ImageConvolution setIsEmboss(boolean isEmboss) {
        this.isEmboss = isEmboss;
        return this;
    }

    public boolean isIsGray() {
        return isGray;
    }

    public ImageConvolution setIsGray(boolean isGray) {
        this.isGray = isGray;
        return this;
    }

    public float[][] getMatrix() {
        return matrix;
    }

    public ImageConvolution setMatrix(float[][] matrix) {
        this.matrix = matrix;
        return this;
    }

}