/*
 * Copyright 2015 Quan Nguyen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sourceforge.lept4j.util;

import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.Iterator;
import java.util.Locale;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageOutputStream;

import com.github.jaiimageio.plugins.tiff.TIFFImageWriteParam;
import com.sun.jna.ptr.PointerByReference;

import com.ochafik.lang.jnaerator.runtime.NativeSize;
import com.ochafik.lang.jnaerator.runtime.NativeSizeByReference;
import com.sun.jna.Structure;
import net.sourceforge.lept4j.*;
import static net.sourceforge.lept4j.ILeptonica.IFF_TIFF;

//import org.opencv.core.Mat;
//import org.opencv.core.MatOfByte;
//import org.opencv.imgcodecs.Imgcodecs;

/**
 * Various utility methods for Leptonica.
 *
 */
public class LeptUtils {

    final static String JAI_IMAGE_WRITER_MESSAGE = "Need to install JAI Image I/O package.\nhttps://github.com/jai-imageio/jai-imageio-core";
    final static String TIFF_FORMAT = "tiff";
    final static float deg2rad = (float) (3.14159 / 180.);

    /**
     * Converts Leptonica <code>Pix</code> to <code>BufferedImage</code>.
     *
     * @param pix source pix
     * @return BufferedImage output image
     * @throws IOException
     */
    public static BufferedImage convertPixToImage(Pix pix) throws IOException {
        PointerByReference pdata = new PointerByReference();
        NativeSizeByReference psize = new NativeSizeByReference();
        int format = IFF_TIFF;
        Leptonica1.pixWriteMem(pdata, psize, pix, format);
        byte[] b = pdata.getValue().getByteArray(0, psize.getValue().intValue());
        InputStream in = new ByteArrayInputStream(b);
        BufferedImage bi = ImageIO.read(in);
        in.close();
        Leptonica1.lept_free(pdata.getValue());
        return bi;
    }

    /**
     * Converts <code>BufferedImage</code> to Leptonica <code>Pix</code> .
     *
     * @param image source image
     * @return Pix output pix
     * @throws IOException
     */
    public static Pix convertImageToPix(BufferedImage image) throws IOException {
        ByteBuffer buff = getImageByteBuffer(image);
        Pix pix = Leptonica1.pixReadMem(buff, new NativeSize(buff.capacity()));
        return pix;
    }

    /**
     * Removes horizontal lines from a grayscale image. The algorithm is based
     * on Leptonica <code>lineremoval.c</code> example.
     * <br>
     * To remove vertical lines, rotate the image 90 degrees first, remove the
     * horizontal lines, and rotate it back.
     *
     * @see
     * <a href="http://www.leptonica.com/line-removal.html">line-removal</a>
     *
     * @param pixs input pix
     * @return pix with lines removed
     */
    public static Pix removeLines(Pix pixs) {
        float angle, conf;
        Pix pix1, pix2, pix3, pix4, pix5;
        Pix pix6, pix7, pix8, pix9;

        /* threshold to binary, extracting much of the lines */
        pix1 = Leptonica1.pixThresholdToBinary(pixs, 170);

        /* find the skew angle and deskew using an interpolated
         * rotator for anti-aliasing (to avoid jaggies) */
        FloatBuffer pangle = FloatBuffer.allocate(1);
        FloatBuffer pconf = FloatBuffer.allocate(1);
        Leptonica1.pixFindSkew(pix1, pangle, pconf);
        angle = pangle.get();
        conf = pconf.get();
        pix2 = Leptonica1.pixRotateAMGray(pixs, (float) (deg2rad * angle), (byte) 255);

        /* extract the lines to be removed */
        pix3 = Leptonica1.pixCloseGray(pix2, 51, 1);

        /* solidify the lines to be removed */
        pix4 = Leptonica1.pixErodeGray(pix3, 1, 5);

        /* clean the background of those lines */
        pix5 = Leptonica1.pixThresholdToValue(null, pix4, 210, 255);

        pix6 = Leptonica1.pixThresholdToValue(null, pix5, 200, 0);

        /* get paint-through mask for changed pixels */
        pix7 = Leptonica1.pixThresholdToBinary(pix6, 210);

        /* add the inverted, cleaned lines to orig.  Because
         * the background was cleaned, the inversion is 0,
         * so when you add, it doesn't lighten those pixels.
         * It only lightens (to white) the pixels in the lines! */
        Leptonica1.pixInvert(pix6, pix6);
        pix8 = Leptonica1.pixAddGray(null, pix2, pix6);

        pix9 = Leptonica1.pixOpenGray(pix8, 1, 9);

        Leptonica1.pixCombineMasked(pix8, pix9, pix7);

        // resource cleanup
        disposePix(pix1);
        disposePix(pix2);
        disposePix(pix3);
        disposePix(pix4);
        disposePix(pix5);
        disposePix(pix6);
        disposePix(pix7);
        disposePix(pix9);

        return pix8;
    }

    /**
     * HMT (with just misses) for speckle up to 2x2
     * <blockquote><pre>"oooo"
     *"oC o"
     *"o  o"
     *"oooo"</pre></blockquote>
     */
    public static final String SEL_STR2 = "oooooC oo  ooooo";
    /**
     * HMT (with just misses) for speckle up to 3x3
     * <blockquote><pre>"ooooo"
     *"oC  o"
     *"o   o"
     *"o   o"
     *"ooooo"</pre></blockquote>
     */
    public static final String SEL_STR3 = "ooooooC  oo   oo   oooooo";

    /**
     * Reduces speckle noise in image. The algorithm is based on Leptonica
     * <code>speckle_reg.c</code> example demonstrating morphological method of
     * removing speckle.
     *
     * @param pixs input pix
     * @param selStr hit-miss sels in 2D layout; SEL_STR2 and SEL_STR3 are
     * predefined values
     * @param selSize 2 for 2x2, 3 for 3x3
     * @return pix with speckle removed
     */
    public static Pix despeckle(Pix pixs, String selStr, int selSize) {
        Pix pix1, pix2, pix3;
        Pix pix4, pix5, pix6;
        Sel sel1, sel2;

        /*  Normalize for rapidly varying background */
        pix1 = Leptonica1.pixBackgroundNormFlex(pixs, 7, 7, 1, 1, 10);

        /* Remove the background */
        pix2 = Leptonica1.pixGammaTRCMasked(null, pix1, null, 1.0f, 100, 175);
        /* Binarize */
        pix3 = Leptonica1.pixThresholdToBinary(pix2, 180);

        /* Remove the speckle noise up to selSize x selSize */
        sel1 = Leptonica1.selCreateFromString(selStr, selSize + 2, selSize + 2, "speckle" + selSize);
        pix4 = Leptonica1.pixHMT(null, pix3, sel1.getPointer());
        sel2 = Leptonica1.selCreateBrick(selSize, selSize, 0, 0, ILeptonica.SEL_HIT);
        pix5 = Leptonica1.pixDilate(null, pix4, sel2.getPointer());
        pix6 = Leptonica1.pixSubtract(null, pix3, pix5);

        LeptUtils.dispose(sel1);
        LeptUtils.dispose(sel2);

        LeptUtils.dispose(pix1);
        LeptUtils.dispose(pix2);
        LeptUtils.dispose(pix3);
        LeptUtils.dispose(pix4);
        LeptUtils.dispose(pix5);

        return pix6;
    }

    /**
     * Disposes of Pix resource.
     *
     * @param pix
     */
    public static void disposePix(Pix pix) {
        if (pix == null) {
            return;
        }
        PointerByReference pRef = new PointerByReference();
        pRef.setValue(pix.getPointer());
        Leptonica1.pixDestroy(pRef);
    }

    /**
     * Disposes of Leptonica native resource.
     *
     * @param resource A Leptonica object, such as <code>Pix</code>,
     * <code>Pixa</code>, <code>Box</code>, <code>Boxa</code>,
     * <code>PixColormap</code>, etc.
     */
    public static void dispose(Structure resource) {
        if (resource == null) {
            return;
        }
        PointerByReference pRef = new PointerByReference();
        pRef.setValue(resource.getPointer());

        if (resource instanceof Pix) {
            Leptonica1.pixDestroy(pRef);
        } else if (resource instanceof Pixa) {
            Leptonica1.pixaDestroy(pRef);
        } else if (resource instanceof Box) {
            Leptonica1.boxDestroy(pRef);
        } else if (resource instanceof Boxa) {
            Leptonica1.boxaDestroy(pRef);
        } else if (resource instanceof L_Bmf) {
            Leptonica1.bmfDestroy(pRef);
        } else if (resource instanceof L_ByteBuffer) {
            Leptonica1.bbufferDestroy(pRef);
        } else if (resource instanceof Boxaa) {
            Leptonica1.boxaaDestroy(pRef);
        } else if (resource instanceof L_Bytea) {
            Leptonica1.l_byteaDestroy(pRef);
        } else if (resource instanceof CCBorda) {
            Leptonica1.ccbaDestroy(pRef);
        } else if (resource instanceof CCBord) {
            Leptonica1.ccbDestroy(pRef);
        } else if (resource instanceof PixColormap) {
            Leptonica1.pixcmapDestroy(pRef);
        } else if (resource instanceof L_Dewarp) {
            Leptonica1.dewarpDestroy(pRef);
        } else if (resource instanceof L_Dewarpa) {
            Leptonica1.dewarpaDestroy(pRef);
        } else if (resource instanceof L_Dna) {
            Leptonica1.l_dnaDestroy(pRef);
        } else if (resource instanceof L_Dnaa) {
            Leptonica1.l_dnaaDestroy(pRef);
        } else if (resource instanceof L_DnaHash) {
            Leptonica1.l_dnaHashDestroy(pRef);
        } else if (resource instanceof FPix) {
            Leptonica1.fpixDestroy(pRef);
        } else if (resource instanceof FPixa) {
            Leptonica1.fpixaDestroy(pRef);
        } else if (resource instanceof DPix) {
            Leptonica1.dpixDestroy(pRef);
        } else if (resource instanceof GPlot) {
            Leptonica1.gplotDestroy(pRef);
        } else if (resource instanceof JbClasser) {
            Leptonica1.jbClasserDestroy(pRef);
        } else if (resource instanceof JbData) {
            Leptonica1.jbDataDestroy(pRef);
        } else if (resource instanceof L_Kernel) {
            Leptonica1.kernelDestroy(pRef);
        } else if (resource instanceof Numa) {
            Leptonica1.numaDestroy(pRef);
        } else if (resource instanceof Numaa) {
            Leptonica1.numaaDestroy(pRef);
        } else if (resource instanceof Pixaa) {
            Leptonica1.pixaaDestroy(pRef);
        } else if (resource instanceof Pixacc) {
            Leptonica1.pixaccDestroy(pRef);
        } else if (resource instanceof PixComp) {
            Leptonica1.pixcompDestroy(pRef);
        } else if (resource instanceof PixaComp) {
            Leptonica1.pixacompDestroy(pRef);
        } else if (resource instanceof PixTiling) {
            Leptonica1.pixTilingDestroy(pRef);
        } else if (resource instanceof Pta) {
            Leptonica1.ptaDestroy(pRef);
        } else if (resource instanceof Ptaa) {
            Leptonica1.ptaaDestroy(pRef);
        } else if (resource instanceof L_Recog) {
            Leptonica1.recogDestroy(pRef);
        } else if (resource instanceof Sarray) {
            Leptonica1.sarrayDestroy(pRef);
        } else if (resource instanceof Sel) {
            Leptonica1.selDestroy(pRef);
        } else if (resource instanceof Sela) {
            Leptonica1.selaDestroy(pRef);
        } else if (resource instanceof L_Sudoku) {
            Leptonica1.sudokuDestroy(pRef);
        } else if (resource instanceof L_WShed) {
            Leptonica1.wshedDestroy(pRef);
        } else if (resource instanceof DoubleLinkedList) {
            Leptonica1.listDestroy(pRef);
        } else if (resource instanceof L_Rbtree) {
            Leptonica1.l_rbtreeDestroy(pRef);
        } else {
            throw new RuntimeException("Not supported.");
        }
    }

    /**
     * Gets image data of an <code>RenderedImage</code> object.
     *
     * @param image an <code>RenderedImage</code> object
     * @return a byte buffer of image data
     * @throws IOException
     */
    static ByteBuffer getImageByteBuffer(RenderedImage image) throws IOException {
        //Set up the writeParam
        TIFFImageWriteParam tiffWriteParam = new TIFFImageWriteParam(Locale.US);
        tiffWriteParam.setCompressionMode(ImageWriteParam.MODE_DISABLED);

        //Get tif writer and set output to file
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(TIFF_FORMAT);

        if (!writers.hasNext()) {
            throw new RuntimeException(JAI_IMAGE_WRITER_MESSAGE);
        }

        ImageWriter writer = writers.next();

        //Get the stream metadata
        IIOMetadata streamMetadata = writer.getDefaultStreamMetadata(tiffWriteParam);

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageOutputStream ios = ImageIO.createImageOutputStream(outputStream);
        writer.setOutput(ios);
        writer.write(streamMetadata, new IIOImage(image, null, null), tiffWriteParam);
//        writer.write(image);
        writer.dispose();
        ios.seek(0);
        byte[] b = new byte[(int) ios.length()];
        ios.read(b);
        ios.close();

        ByteBuffer buf = ByteBuffer.allocateDirect(b.length);
        buf.order(ByteOrder.nativeOrder());
        buf.put(b);
        ((Buffer) buf).flip();
        return buf;
    }

//    /**
//     * Converts OpenCV Mat to Leptonica Pix.
//     * 
//     * @param mat source mat
//     * @return output pix
//     */
//    public static Pix convertMatToPix(Mat mat) {
//        MatOfByte bytes = new MatOfByte();
//        Imgcodecs.imencode(".tif", mat, bytes);
//        ByteBuffer buff = ByteBuffer.wrap(bytes.toArray());
//        return Leptonica1.pixReadMem(buff, new NativeSize(buff.capacity()));
//    }
//
//    /**
//     * Converts Leptonica Pix to OpenCV Mat.
//     * @param pix source pix
//     * @return output mat
//     */
//    public static Mat convertPixToMat(Pix pix) {
//        PointerByReference pdata = new PointerByReference();
//        NativeSizeByReference psize = new NativeSizeByReference();
//        Leptonica1.pixWriteMem(pdata, psize, pix, ILeptonica.IFF_TIFF);
//        byte[] b = pdata.getValue().getByteArray(0, psize.getValue().intValue());
//        Leptonica1.lept_free(pdata.getValue());
//        return Imgcodecs.imdecode(new MatOfByte(b), Imgcodecs.CV_LOAD_IMAGE_UNCHANGED);
//    }
}