package org.freehep.graphicsio.emf;

import org.freehep.graphicsio.emf.gdi.BitmapInfoHeader;
import org.freehep.graphicsio.emf.gdi.BlendFunction;

import java.awt.image.BufferedImage;
import java.awt.Color;
import java.io.IOException;
import java.util.Arrays;

/**
 * this class creates a BufferedImage from EMF imaga data stored in
 * a byte[].
 *
 * @author Steffen Greiffenberg
 * @version $Id: freehep-graphicsio-emf/src/main/java/org/freehep/graphicsio/emf/EMFImageLoader.java 10ec7516e3ce 2007/02/06 18:42:34 duns $
 */
public class EMFImageLoader {

    /**
     * creates a BufferedImage from an EMFInputStream using
     * BitmapInfoHeader data
     *
     * @param bmi BitmapInfoHeader storing Bitmap informations
     * @param width expected image width
     * @param height expected image height
     * @param emf EMF stream
     * @param len length of image data
     * @param blendFunction contains values for transparency
     * @return BufferedImage or null
     * @throws java.io.IOException thrown by EMFInputStream
     */
    public static BufferedImage readImage(
        BitmapInfoHeader bmi,
        int width,
        int height,
        EMFInputStream emf,
        int len,
        BlendFunction blendFunction) throws IOException {

        // 0    Windows 98/Me, Windows 2000/XP: The number of bits-per-pixel
        // is specified or is implied by the JPEG or PNG format.

        if (bmi.getBitCount() == 1) {
            // 1 	The bitmap is monochrome, and the bmiColors
            // member of BITMAPINFO contains two entries. Each
            // bit in the bitmap array represents a pixel. If
            // the bit is clear, the pixel is displayed with
            // the color of the first entry in the bmiColors
            // table; if the bit is set, the pixel has the color
            // of the second entry in the table.
            // byte[] bytes = emf.readByte(len);

            int blue = emf.readUnsignedByte();
            int green = emf.readUnsignedByte();
            int red = emf.readUnsignedByte();
            /*int unused =*/ emf.readUnsignedByte();

            int color1 = new Color(red, green, blue).getRGB();

            blue = emf.readUnsignedByte();
            green = emf.readUnsignedByte();
            red = emf.readUnsignedByte();
            /*unused = */ emf.readUnsignedByte();

            int color2 = new Color(red, green, blue).getRGB();

            BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

            int[] data = emf.readUnsignedByte(len - 8);

            // TODO: this is highly experimental and does
            // not work for the tested examples
            int strangeOffset = width % 8;
            if (strangeOffset != 0) {
                strangeOffset = 8 - strangeOffset;
            }

            // iterator for pixel data
            int pixel = 0;

            // mask for getting the bits from a pixel data byte
            int[] mask = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};

            // image data are swapped compared to java standard
            for (int y = height - 1; y > -1; y--) {
                for (int x = 0; x < width; x++) {
                    int pixelDataGroup = data[pixel / 8];
                    int pixelData = pixelDataGroup & mask[pixel % 8];
                    pixel ++;

                    if (pixelData > 0) {
                        result.setRGB(x, y, color2);
                    } else {
                        result.setRGB(x, y, color1);
                    }
                }
                // add the extra width
                pixel = pixel + strangeOffset;
            }

            /* for debugging: shows every loaded image
            javax.swing.JFrame f = new javax.swing.JFrame("test");
            f.getContentPane().setBackground(Color.green);
            f.getContentPane().setLayout(
                new java.awt.BorderLayout(0, 0));
            f.getContentPane().add(
                java.awt.BorderLayout.CENTER,
                new javax.swing.JLabel(
                    new javax.swing.ImageIcon(result)));
            f.setSize(new java.awt.Dimension(width + 20, height + 20));
            f.setVisible(true);*/

            return result;

        } else if ((bmi.getBitCount() == 8) &&
            (bmi.getCompression() == EMFConstants.BI_RGB)) {
            // 8 	The bitmap has a maximum of 256 colors, and the bmiColors member
            // of BITMAPINFO contains up to 256 entries. In this case, each byte in
            // the array represents a single pixel.

            // TODO has to be done in BitMapInfoHeader?
            // read the color table
            int colorsUsed = bmi.getClrUsed();

            // typedef struct tagRGBQUAD {
            //   BYTE    rgbBlue;
            //   BYTE    rgbGreen;
            //   BYTE    rgbRed;
            //   BYTE    rgbReserved;
            // } RGBQUAD;
            int[] colors = emf.readUnsignedByte(colorsUsed * 4);

            // data a indexes to a certain color in the colortable.
            // Each byte represents a pixel
            int[] data = emf.readUnsignedByte(len - (colorsUsed * 4));

            // convert it to a color table
            int[] colorTable = new int[256];
            // iterator for color data
            int color = 0;
            for (int i = 0; i < colorsUsed; i++, color = i * 4) {
                colorTable[i] = new Color(
                    colors[color + 2],
                    colors[color + 1],
                    colors[color]).getRGB();
            }

            // fill with black to avoid ArrayIndexOutOfBoundExceptions;
            // somme images seem to use more colors than stored in ClrUsed
            if (colorsUsed < 256) {
                Arrays.fill(colorTable, colorsUsed, 256, 0);
            }

            // don't know why, but the width has to be adjusted ...
            // it took more than an hour to determine the strangeOffset
            int strangeOffset = width % 4;
            if (strangeOffset != 0) {
                strangeOffset = 4 - strangeOffset;
            }

            // create the image
            BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

            // iterator for pixel data
            int pixel = 0;

            // image data are swapped compared to java standard
            for (int y = height - 1; y > -1; y--) {
                for (int x = 0; x < width; x++) {
                    result.setRGB(x, y, colorTable[data[pixel++]]);
                }
                // add the extra width
                pixel = pixel + strangeOffset;
            }

            return result;
        }

        // The bitmap has a maximum of 2^16 colors. If the biCompression member
        // of the BITMAPINFOHEADER is BI_RGB, the bmiColors member of BITMAPINFO is
        // NULL.
        else if ((bmi.getBitCount() == 16) &&
            (bmi.getCompression() == EMFConstants.BI_RGB)) {

            // Each WORD in the bitmap array represents a single pixel. The
            // relative intensities of red, green, and blue are represented with
            // five bits for each color component. The value for blue is in the least
            // significant five bits, followed by five bits each for green and red.
            // The most significant bit is not used. The bmiColors color table is used
            // for optimizing colors used on palette-based devices, and must contain
            // the number of entries specified by the biClrUsed member of the
            // BITMAPINFOHEADER.
            int[] data = emf.readDWORD(len / 4);

            // don't know why, by the width has to be the half ...
            // maybe that has something to do with sie HALFTONE rendering setting.
            width = (width + (width % 2)) / 2;
            // to avoid ArrayIndexOutOfBoundExcesptions
            height = data.length / width / 2;

            // create a non transparent image
            BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

            // found no sample and color model to mak this work
            // tag.image.setRGB(0, 0, tag.widthSrc, tag.heightSrc, data, 0, 0);

            // used in the loop
            int off = 0;
            int pixel, neighbor;

            // image data are swapped compared to java standard
            for (int y = height - 1; y > -1; y--, off = off + width) {
                for (int x = 0; x < width; x++) {
                    neighbor = data[off + width];
                    pixel = data[off++];

                    // compute the average of the pixel and it's neighbor
                    // and set the reulting color values
                    result.setRGB(x, y, new Color(
                        // 0xF800 = 2 * 0x7C00
                        (float)((pixel & 0x7C00) + (neighbor & 0x7C00)) / 0xF800,
                        (float)((pixel & 0x3E0) + (neighbor & 0x3E0)) / 0x7C0,
                        (float)((pixel & 0x1F) + (neighbor & 0x1F)) / 0x3E).getRGB());
                }
            }

            /* for debugging: shows every loaded image
            javax.swing.JFrame f = new javax.swing.JFrame("test");
            f.getContentPane().setBackground(Color.green);
            f.getContentPane().setLayout(
                new java.awt.BorderLayout(0, 0));
            f.getContentPane().add(
                java.awt.BorderLayout.CENTER,
                new javax.swing.JLabel(
                    new javax.swing.ImageIcon(result)));
            f.pack();
            f.setVisible(true);*/

            return result;
        }
        // The bitmap has a maximum of 2^32 colors. If the biCompression member of the
        // BITMAPINFOHEADER is BI_RGB, the bmiColors member of BITMAPINFO is NULL.
        else if ((bmi.getBitCount() == 32) &&
            (bmi.getCompression() == EMFConstants.BI_RGB)) {
            // Each DWORD in the bitmap array represents the relative intensities of blue,
            // green, and red, respectively, for a pixel. The high byte in each DWORD is not
            // used. The bmiColors color table is used for optimizing colors used on
            // palette-based devices, and must contain the number of entries specified
            // by the biClrUsed member of the BITMAPINFOHEADER.

            width = (width + (width % 20)) / 20;
            height = (height + (height % 20)) / 20;

            // create a transparent image
            BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            // read the image data
            int[] data = emf.readDWORD(len / 4);

            // used to iterate the pixels later
            int off = 0;
            int pixel;
            int alpha;

            // The SourceConstantaAlpha member of BLENDFUNCTION specifies an alpha transparency
            // value to be used on the entire source bitmap. The SourceConstantAlpha value is
            // combined with any per-pixel alpha values. If SourceConstantAlpha is 0, it is
            // assumed that the image is transparent. Set the SourceConstantAlpha value to 255
            // (which indicates that the image is opaque) when you only want to use per-pixel
            // alpha values.
            int sourceConstantAlpha = blendFunction.getSourceConstantAlpha();

            if (blendFunction.getAlphaFormat() != EMFConstants.AC_SRC_ALPHA) {
                // If the source bitmap has no per-pixel alpha value (that is, AC_SRC_ALPHA is not
                // set), the SourceConstantAlpha value determines the blend of the source and
                // destination bitmaps, as shown in the following table. Note that SCA is used
                // for SourceConstantAlpha here. Also, SCA is divided by 255 because it has a
                // value that ranges from 0 to 255.

                // Dst.Red 	= Src.Red * (SCA/255.0) 	+ Dst.Red * (1.0 - (SCA/255.0))
                // Dst.Green 	= Src.Green * (SCA/255.0) 	+ Dst.Green * (1.0 - (SCA/255.0))
                // Dst.Blue 	= Src.Blue * (SCA/255.0) 	+ Dst.Blue * (1.0 - (SCA/255.0))

                // If the destination bitmap has an alpha channel, then the blend is as follows.
                // Dst.Alpha 	= Src.Alpha * (SCA/255.0) 	+ Dst.Alpha * (1.0 - (SCA/255.0))

                for (int y = height - 1; y > -1 && off < data.length; y--) {
                    for (int x = 0; x < width && off < data.length; x++) {
                        pixel = data[off++];

                        result.setRGB(x, y, new Color(
                            (pixel & 0xFF0000) >> 16,
                            (pixel & 0xFF00) >> 8,
                            (pixel & 0xFF),
                            // TODO not tested
                            sourceConstantAlpha
                        ).getRGB());
                    }
                }
            }
            // When the BlendOp parameter is AC_SRC_OVER , the source bitmap is placed over
            // the destination bitmap based on the alpha values of the source pixels.
            else {
                // If the source bitmap does not use SourceConstantAlpha (that is, it equals
                // 0xFF), the per-pixel alpha determines the blend of the source and destination
                // bitmaps, as shown in the following table.
                if (sourceConstantAlpha == 0xFF) {
                    // Dst.Red 	= Src.Red 	+ (1 - Src.Alpha) * Dst.Red
                    // Dst.Green 	= Src.Green 	+ (1 - Src.Alpha) * Dst.Green
                    // Dst.Blue 	= Src.Blue 	+ (1 - Src.Alpha) * Dst.Blue

                    // If the destination bitmap has an alpha channel, then the blend is as follows.
                    // Dest.alpha 	= Src.Alpha 	+ (1 - SrcAlpha) * Dst.Alpha

                    // image data are swapped compared to java standard
                    for (int y = height - 1; y > -1 && off < data.length; y--) {
                        for (int x = 0; x < width && off < data.length; x++) {
                            pixel = data[off++];
                            alpha = (pixel & 0xFF000000) >> 24;
                            if (alpha == -1) {
                                alpha = 0xFF;
                            }

                            result.setRGB(x, y, new Color(
                                (pixel & 0xFF0000) >> 16,
                                (pixel & 0xFF00) >> 8,
                                (pixel & 0xFF),
                                alpha
                            ).getRGB());
                        }
                    }
                }

                // If the source has both the SourceConstantAlpha (that is, it is not 0xFF)
                // and per-pixel alpha, the source is pre-multiplied by the SourceConstantAlpha
                // and then the blend is based on the per-pixel alpha. The following tables show
                // this. Note that SourceConstantAlpha is divided by 255 because it has a value
                // that ranges from 0 to 255.
                else {
                    // Src.Red 	= Src.Red 	* SourceConstantAlpha / 255.0;
                    // Src.Green 	= Src.Green 	* SourceConstantAlpha / 255.0;
                    // Src.Blue 	= Src.Blue 	* SourceConstantAlpha / 255.0;
                    // Src.Alpha 	= Src.Alpha 	* SourceConstantAlpha / 255.0;

                    // Dst.Red 	= Src.Red 	+ (1 - Src.Alpha) * Dst.Red
                    // Dst.Green 	= Src.Green 	+ (1 - Src.Alpha) * Dst.Green
                    // Dst.Blue 	= Src.Blue 	+ (1 - Src.Alpha) * Dst.Blue
                    // Dst.Alpha 	= Src.Alpha 	+ (1 - Src.Alpha) * Dst.Alpha

                    for (int y = height - 1; y > -1 && off < data.length; y--) {
                        for (int x = 0; x < width && off < data.length; x++) {
                            pixel = data[off++];

                            alpha = (pixel & 0xFF000000) >> 24;
                            if (alpha == -1) {
                                alpha = 0xFF;
                            }

                            // TODO not tested
                            alpha = alpha * sourceConstantAlpha / 0xFF;

                            result.setRGB(x, y, new Color(
                                (pixel & 0xFF0000) >> 16,
                                (pixel & 0xFF00) >> 8,
                                (pixel & 0xFF),
                                alpha
                            ).getRGB());
                        }
                    }
                }
            }
                        
            /* for debugging: shows every loaded image
            javax.swing.JFrame f = new javax.swing.JFrame("test");
            f.getContentPane().setBackground(Color.green);
            f.getContentPane().setLayout(
                new java.awt.BorderLayout(0, 0));
            f.getContentPane().add(
                java.awt.BorderLayout.CENTER,
                new javax.swing.JLabel(
                    new javax.swing.ImageIcon(result)));
            f.setSize(new java.awt.Dimension(width + 20, height + 20));
            f.setVisible(true);*/
            
            return result;
        }
        // If the biCompression member of the BITMAPINFOHEADER is BI_BITFIELDS,
        // the bmiColors member contains three DWORD color masks that specify the
        // red, green, and blue components, respectively, of each pixel. Each DWORD
        // in the bitmap array represents a single pixel.

        // Windows NT/ 2000: When the biCompression member is BI_BITFIELDS, bits set in
        // each DWORD mask must be contiguous and should not overlap the bits of
        // another mask. All the bits in the pixel do not need to be used.

        // Windows 95/98/Me: When the biCompression member is BI_BITFIELDS, the system
        // supports only the following 32-bpp color mask: The blue mask is 0x000000FF,
        // the green mask is 0x0000FF00, and the red mask is 0x00FF0000.
        else if ((bmi.getBitCount() == 32) &&
            (bmi.getCompression() == EMFConstants.BI_BITFIELDS)) {
            /* byte[] bytes =*/ emf.readByte(len);
            return null;
        } else {
            /* byte[] bytes =*/ emf.readByte(len);
            return null;
        }
    }
}
