/* Copyright (C) 2005-2011 Fabio Riccardi */

package com.lightcrafts.image.types;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.awt.image.renderable.ParameterBlock;
import java.io.IOException;
import java.io.File;

import Jama.Matrix;

import sun.awt.image.ShortInterleavedRaster;

import com.lightcrafts.image.BadImageFileException;
import com.lightcrafts.image.ColorProfileException;
import com.lightcrafts.image.ImageInfo;
import com.lightcrafts.image.metadata.*;
import com.lightcrafts.image.UnknownImageTypeException;
import com.lightcrafts.image.libs.LCTIFFReader;
import com.lightcrafts.image.libs.LCImageLibException;
import com.lightcrafts.jai.JAIContext;
import com.lightcrafts.jai.utils.Functions;
import com.lightcrafts.jai.opimage.CachedImage;
import com.lightcrafts.jai.opimage.RGBDemosaicOpImage;
import com.lightcrafts.mediax.jai.*;
import com.lightcrafts.utils.thread.ProgressThread;
import com.lightcrafts.utils.*;
import com.lightcrafts.utils.filecache.FileCache;

/**
 * A <code>RawImageType</code> is-an {@link ImageType} that is the base class
 * for raw images.  It factors out common code that uses Dave Coffin's
 * <i>dcraw</i> to extract information from camera raw image files.
 *
 * @author Paul J. Lucas [paul@lightcrafts.com]
 */
public abstract class RawImageType extends ImageType {

    private static final boolean CACHE_CONVERSION = true;

    static final boolean USE_EMBEDDED_PREVIEW = false;

    ////////// public /////////////////////////////////////////////////////////

    /**
     * {@inheritDoc}
     */
    public Dimension getDimension( ImageInfo imageInfo )
        throws BadImageFileException, IOException, UnknownImageTypeException
    {
        final AuxiliaryImageInfo auxInfo = imageInfo.getAuxiliaryInfo();
        assert auxInfo instanceof RawImageInfo;
        final DCRaw dcRaw = ((RawImageInfo)auxInfo).getDCRaw();
        return new Dimension( dcRaw.getImageWidth(), dcRaw.getImageHeight() );
    }

    static Matrix pseudoinverse(Matrix m) {
        Matrix t = m.transpose();
        return t.times(m).inverse().times(t).transpose();
    }

    // specialized to 3 x 3 so we can use the real inverse matrix instead of a pseudoinverse
    // some day we should go back to the more generic 3 x 4 case
    static void cam_xyz_coeff(float cam_xyz[][], float rgb_cam[][], float pre_mul[]) {
        double cam_rgb[][] = new double[3][3];

        for (int i = 0; i < 3; i++)             /* Multiply out XYZ colorspace */
            for (int j = 0; j < 3; j++)
                for (int k = 0; k < 3; k++)
                    cam_rgb[i][j] += cam_xyz[i][k] * ColorScience.XYZToRGBMat[k][j]; // xyz_rgb[k][j];

        for (int i = 0; i < 3; i++) {           /* Normalize cam_rgb so that */
            double num = 0;                     /* cam_rgb * (1,1,1) is (1,1,1,1) */
            for (int j = 0; j < 3; j++)
                num += cam_rgb[i][j];
            for (int j = 0; j < 3; j++)
                cam_rgb[i][j] /= num;
            pre_mul[i] = (float) (1 / num);
        }

        float inverse[][] = new Matrix(cam_rgb).inverse().transpose().getArrayFloat();
        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++)
                rgb_cam[i][j] = inverse[j][i];
    }

    /**
     * {@inheritDoc}
     */
    public synchronized PlanarImage getImage( ImageInfo imageInfo, ProgressThread thread )
        throws BadImageFileException, ColorProfileException, IOException,
               UnknownImageTypeException, UserCanceledException
    {
        long startTime = System.currentTimeMillis();

        final RawImageInfo rawInfo = (RawImageInfo)imageInfo.getAuxiliaryInfo();
        final DCRaw dcRaw = rawInfo.getDCRaw();

        if (!dcRaw.decodable() || dcRaw.rawColors() != 3)
            throw new UnknownImageTypeException("Unsupported Camera");

        final ColorModel colorModel = RasterFactory.createComponentColorModel(
            DataBuffer.TYPE_USHORT, JAIContext.linearColorSpace, false, false,
            Transparency.OPAQUE
        );

        final ImageMetadata metadata = imageInfo.getMetadata();
        final int imageWidth = metadata.getImageWidth();
        final int imageHeight = metadata.getImageHeight();

        String cacheKey = null;
        File imageFile = null;
        final FileCache fileCache = RawImageCache.getCacheFor( imageInfo );

        if ( CACHE_CONVERSION && fileCache != null ) {
            System.out.println("Checking cache for: " + imageInfo);
            final long t1 = System.currentTimeMillis();
            cacheKey = RawImageCache.getCacheKeyFor( imageInfo );
            if ( cacheKey != null )
                imageFile = RawImageCache.getCachedImageFileFor( cacheKey );
            if (imageFile != null) {
                final String fileName = imageFile.getAbsolutePath();
                try {
                    /* final LCTIFFReader reader = new LCTIFFReader( fileName, true );
                    final PlanarImage image = reader.getImage( thread ); */
                    final PlanarImage image = new LCTIFFReader.TIFFImage(fileName);
                    final long t2 = System.currentTimeMillis();
                    System.out.println("Retrieved Cached image in " + (t2 - t1) + "ms");
                    return image;
                } catch (LCImageLibException e) {
                    // never mind, don't use the cache
                    e.printStackTrace();
                }
            } else
                System.out.println("File not in cache.");
        }

        System.out.println("metadata width: " + imageWidth + ", height: " + imageHeight);

        ProgressIndicator indicator = null;
        if ( thread != null )
            indicator = thread.getProgressIndicator();
        if ( indicator != null ) {
            indicator.setMinimum( 0 );
            indicator.setMaximum( 3 );
        }

        final int filters = dcRaw.getFilters();

        final BufferedImage dcrawImage = (BufferedImage) dcRaw.runDCRaw(DCRaw.dcrawMode.full, false);

        System.out.println("dcraw width: " + dcrawImage.getWidth() + ", height: " + dcrawImage.getHeight());

        // Merge the secondary pixels of Fuji S3 and S5
        if (this instanceof RAFImageType && dcRaw.getSecondaryPixels() &&
            (dcRaw.getCameraMake(true).endsWith("S3PRO") || dcRaw.getCameraMake(true).endsWith("S5PRO")))
        {
            final int rawWidth = dcrawImage.getWidth();
            final int rawHeight = dcrawImage.getHeight();

            int rx, ry, bx, by;

            System.out.println("Filters: 0x" + Long.toString(0xffffffffL & filters, 16));

            switch (filters) {
                case 0x16161616:
                    rx=1; ry=1; bx=0; by=0;
                    break;
                case 0x61616161:
                    rx=1; ry=0; bx=0; by=1;
                    break;
                case 0x49494949:
                    rx=0; ry=1; bx=1; by=0;
                    break;
                case 0x94949494:
                    rx=0; ry=0; bx=1; by=1;
                    break;
                case 0:
                case -1:
                    rx=0; ry=0; bx=0; by=0;
                    break;
                default:
                    throw new UnknownImageTypeException("Unknown Bayer filter pattern.");
            }

            final ShortInterleavedRaster dcrawRaster = (ShortInterleavedRaster) dcrawImage.getRaster();

            final BufferedImage dcrawImage2 = (BufferedImage) dcRaw.runDCRaw(DCRaw.dcrawMode.full, true);
            final ShortInterleavedRaster dcrawRaster2 = (ShortInterleavedRaster) dcrawImage2.getRaster();

            short data[] = dcrawRaster.getDataStorage();
            short data2[] = dcrawRaster2.getDataStorage();

            float[] cameraMultipliers = dcRaw.getCameraMultipliers();
            float[] secondaryCameraMultipliers = dcRaw.getSecondaryCameraMultipliers();

            float redRatio = secondaryCameraMultipliers[0] / cameraMultipliers[0];
            float blueRatio = secondaryCameraMultipliers[2] / cameraMultipliers[2];

            final int tl = (int) (0x6000/12.73f); // 3.5 stops
            final int th = (int) (0x8000/12.73f);

            for (int y = 0; y < rawHeight; y++) {
                for (int x = 0; x < rawWidth; x++) {
                    int d = (int) ((0xffff & data[y * rawWidth + x]) / 12.73f);
                    if (x >= 0 && y > 0 && x < rawWidth - 1 && y < rawHeight - 1 && d > tl) {
                        int d2 = 0xffff & data2[(y-1) * (rawWidth-1) + x];
                        if ((x&1) == rx && (y&1) == ry)
                            d2 = (int) (redRatio * d2);
                        if ((x&1) == bx && (y&1) == by)
                            d2 = (int) (blueRatio * d2);

                        if (d < th) {
                            float m = (th - d) / (float) (th - tl);
                            d = (int) (m * d + (1 - m) * d2);
                        } else
                            d = d2;
                    }

                    data[y * rawWidth + x] = (short) (0xffff & d);
                }
            }
        }

        long dcrawTime = System.currentTimeMillis();

        if ( thread != null && thread.isCanceled() )
            return null;

        if ( indicator != null )
            indicator.incrementBy(1);

        PlanarImage rgbImage;

        if (dcrawImage.getSampleModel().getNumBands() == 1 && filters != 0 && filters != -1) {
            ImageLayout demosaicLayout = new ImageLayout(0, 0, dcrawImage.getWidth(), dcrawImage.getHeight(),
                                                 0, 0, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
                                                 colorModel.createCompatibleSampleModel(JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT),
                                                 colorModel);

            rgbImage = new RGBDemosaicOpImage(dcrawImage, null, demosaicLayout, filters);

            if ((this instanceof RAFImageType
                 || (this instanceof DNGImageType && dcRaw.getCameraMake(false).startsWith("FUJI")))
                && dcRaw.getImageWidth() != dcRaw.getRawWidth()) {
                final Interpolation interp =
                        Interpolation.getInstance(Interpolation.INTERP_BILINEAR);

                double angle = Math.PI / 4;

                if (dcRaw.getModel().equals("FinePix S2Pro"))
                    angle = 3 * Math.PI / 4;

                AffineTransform rotation = AffineTransform.getRotateInstance(angle,
                                                                             rgbImage.getWidth() / 2,
                                                                             rgbImage.getHeight() / 2);

                SampleModel sm = colorModel.createCompatibleSampleModel(JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT);

                final ParameterBlock pb2 = new ParameterBlock();
                pb2.addSource(rgbImage);
                pb2.add(rotation);
                pb2.add(interp);
                RenderedOp rotated = JAI.create("Affine", pb2, null);

                ImageLayout rotatedLayout = new ImageLayout(rotated.getBounds().x, rotated.getBounds().y,
                                                            rotated.getBounds().width, rotated.getBounds().height,
                                                            rotated.getBounds().x, rotated.getBounds().y,
                                                            JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
                                                            sm, colorModel);

                final RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, rotatedLayout);

                rotated = JAI.create("Affine", pb2, hints);

                int width = dcRaw.getImageWidth();
                int height = dcRaw.getImageHeight();

                rgbImage = Functions.crop(rotated,
                                          rotated.getMinX() + (rotated.getWidth() - width) / 2 + 2,
                                          rotated.getMinY() + (rotated.getHeight() - height) / 2 + 2,
                                          width - 4, height - 4, JAIContext.noCacheHint);
            } else if (dcRaw.getMake().equals("NIKON") && dcRaw.getModel().equals("D1X")) {
//                rgbImage = Functions.crop(rgbImage,
//                                          rgbImage.getMinX() + 2,
//                                          rgbImage.getMinY() + 2,
//                                          rgbImage.getWidth() - 6,
//                                          rgbImage.getHeight() - 6, null);
                rgbImage = new TiledImage(rgbImage, true).getSubImage(
                        rgbImage.getMinX() + 5,
                        rgbImage.getMinY() + 5,
                        rgbImage.getWidth() - 10,
                        rgbImage.getHeight() - 10);

                final RenderingHints hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
                                                                BorderExtender.createInstance(BorderExtender.BORDER_COPY));
                final Interpolation interp =
                        Interpolation.getInstance(Interpolation.INTERP_BILINEAR);

                SampleModel sm = colorModel.createCompatibleSampleModel(JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT);

                ImageLayout newLayout = new ImageLayout(0, 0, 3 * rgbImage.getWidth() / 4, 3 * rgbImage.getHeight() / 2,
                                                        0, 0, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
                                                        sm, colorModel);

                hints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, newLayout));

                final ParameterBlock pb2 = new ParameterBlock();
                pb2.addSource(rgbImage);
                pb2.add(AffineTransform.getScaleInstance(3.0 / 4.0, 3.0 / 2.0));
                pb2.add(interp);
                rgbImage = JAI.create("Affine", pb2, hints);
            } else {
                rgbImage = Functions.crop(rgbImage,
                                          rgbImage.getMinX() + 5,
                                          rgbImage.getMinY() + 5,
                                          rgbImage.getWidth() - 10,
                                          rgbImage.getHeight() - 10, JAIContext.noCacheHint);
            }

            ImageLayout cacheLayout = new ImageLayout(0, 0, rgbImage.getWidth(), rgbImage.getHeight(),
                                     0, 0, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
                                     colorModel.createCompatibleSampleModel(JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT),
                                     colorModel);

            final CachedImage cache = new CachedImage(cacheLayout, JAIContext.fileCache);

            long tilingTime = System.currentTimeMillis();

            Rectangle bounds = cache.getBounds();
            bounds.translate(rgbImage.getMinX(), rgbImage.getMinY());
            final Point[] tileIndices = rgbImage.getTileIndices(bounds);

            // rgbImage.queueTiles(tileIndices);

            int processors = Runtime.getRuntime().availableProcessors();

            if (processors > 1) {
                Thread threads[] = new Thread[processors];

                final PlanarImage image = rgbImage;

                final Point jobs[][] = new Point[processors][];
                int k = 0;

                for (int i = 0; i < processors; i++) {
                    if (i < processors - 1)
                        jobs[i] = new Point[tileIndices.length/processors];
                    else
                        jobs[i] = new Point[tileIndices.length - (processors - 1) * (tileIndices.length/processors)];

                    for (int j = 0; j < jobs[i].length; j++)
                        jobs[i][j] = tileIndices[k++];
                }

                for (int i = 0; i < processors; i++) {
                    final Point job[] = jobs[i];
                    Runnable runnable = new Runnable() {
                        public void run() {
                            for (Point ti : job) {
                                Raster tile = image.getTile(ti.x, ti.y);
                                tile = tile.createTranslatedChild(tile.getMinX()-image.getMinX(),
                                                                  tile.getMinY()-image.getMinY());
                                cache.setData(tile);
                            }
                        }
                    };

                    threads[i] = new Thread(runnable, "RAW Processor " + i);
                    threads[i].start();
                }
                for (int i = 0; i < processors; i++) {
                    try {
                        threads[i].join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else {
                for (Point ti : tileIndices) {
                    Raster tile = rgbImage.getTile(ti.x, ti.y);
                    tile = tile.createTranslatedChild(tile.getMinX()-rgbImage.getMinX(), tile.getMinY()-rgbImage.getMinY());
                    cache.setData(tile);
                }
            }

            System.out.println("retiling: " + (System.currentTimeMillis() - tilingTime));

            rgbImage = cache;
        } else {
            ImageLayout layout = new ImageLayout(0, 0, dcrawImage.getWidth(), dcrawImage.getHeight(),
                                     0, 0, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
                                     colorModel.createCompatibleSampleModel(JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT),
                                     colorModel);

            CachedImage cache = new CachedImage(layout, JAIContext.fileCache);

            for (int x = 0; x <= cache.getMaxTileX(); x++)
                for (int y = 0; y <= cache.getMaxTileY(); y++) {
                    Functions.copyData(cache.getWritableTile(x, y), dcrawImage.getRaster());
                }

             rgbImage = cache;
        }

        if (indicator != null)
            indicator.incrementBy(1);

        long demosaicTime = System.currentTimeMillis();

        if (indicator != null)
            indicator.incrementBy(1);

        System.out.println("dcraw: " + (dcrawTime - startTime) + ", demosaic: " + (demosaicTime - dcrawTime));

        if (CACHE_CONVERSION && fileCache != null && imageFile == null) {
            if ( rgbImage != null && cacheKey != null)
                RawImageCache.add(cacheKey,  rgbImage);
        }

        return  rgbImage;
    }

    /**
     * {@inheritDoc}
     */
    public RenderedImage getPreviewImage( ImageInfo imageInfo, int maxWidth,
                                          int maxHeight )
        throws BadImageFileException, IOException, UnknownImageTypeException
    {
        final RawImageInfo rawInfo = (RawImageInfo)imageInfo.getAuxiliaryInfo();
        final DCRaw dcRaw = rawInfo.getDCRaw();

        if (dcRaw.getThumbHeight() >= 400 && dcRaw.getThumbWidth() >= 600)
            return dcRaw.runDCRaw(DCRaw.dcrawMode.thumb);
        else
        return dcRaw.runDCRaw(DCRaw.dcrawMode.preview);
    }

    public RenderedImage getThumbnailImage( ImageInfo imageInfo )
        throws BadImageFileException, IOException, UnknownImageTypeException
    {
        final RawImageInfo rawInfo = (RawImageInfo)imageInfo.getAuxiliaryInfo();
        final DCRaw dcRaw = rawInfo.getDCRaw();
        return dcRaw.runDCRaw(DCRaw.dcrawMode.thumb);
    }

    /**
     * {@inheritDoc}
     */
    public RawImageInfo newAuxiliaryInfo( ImageInfo imageInfo ) {
        return new RawImageInfo( imageInfo );
    }

    /**
     * Reads all the image metadata provided by dcraw.
     *
     * @param imageInfo The image to read the metadata from.
     */
    public void readMetadata( ImageInfo imageInfo )
        throws BadImageFileException, IOException, UnknownImageTypeException
    {
        new DCRawMetadataReader( imageInfo ).readMetadata();
    }

    /**
     * Writes the metadata for raw files to an XMP sidecar file.
     *
     * @param imageInfo The image to write the metadata for.
     */
    public void writeMetadata( ImageInfo imageInfo )
        throws BadImageFileException, IOException, UnknownImageTypeException
    {
        final File xmpFile = new File( imageInfo.getXMPFilename() );
        final ImageMetadata metadata = imageInfo.getCurrentMetadata();
        XMPMetadataWriter.mergeInto( metadata, xmpFile );
        metadata.clearEdited();
    }

    ////////// protected //////////////////////////////////////////////////////

    /**
     * Construct a <code>RawImageType</code>.
     */
    protected RawImageType() {
        // do nothing
    }
}
/* vim:set et sw=4 ts=4: */
