/*
 * Copyright 2014 The Android Open Source Project
 *
 * 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 android.hardware.camera2;

import android.graphics.Bitmap;
import android.graphics.ImageFormat;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.location.Location;
import android.media.ExifInterface;
import android.media.Image;
import android.os.SystemClock;
import android.util.Size;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;

/**
 * The {@link DngCreator} class provides functions to write raw pixel data as a DNG file.
 *
 * <p>
 * This class is designed to be used with the {@link android.graphics.ImageFormat#RAW_SENSOR}
 * buffers available from {@link android.hardware.camera2.CameraDevice}, or with Bayer-type raw
 * pixel data that is otherwise generated by an application.  The DNG metadata tags will be
 * generated from a {@link android.hardware.camera2.CaptureResult} object or set directly.
 * </p>
 *
 * <p>
 * The DNG file format is a cross-platform file format that is used to store pixel data from
 * camera sensors with minimal pre-processing applied.  DNG files allow for pixel data to be
 * defined in a user-defined colorspace, and have associated metadata that allow for this
 * pixel data to be converted to the standard CIE XYZ colorspace during post-processing.
 * </p>
 *
 * <p>
 * For more information on the DNG file format and associated metadata, please refer to the
 * <a href=
 * "https://wwwimages2.adobe.com/content/dam/Adobe/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf">
 * Adobe DNG 1.4.0.0 specification</a>.
 * </p>
 */
public final class DngCreator implements AutoCloseable {

    private static final String TAG = "DngCreator";
    /**
     * Create a new DNG object.
     *
     * <p>
     * It is not necessary to call any set methods to write a well-formatted DNG file.
     * </p>
     * <p>
     * DNG metadata tags will be generated from the corresponding parameters in the
     * {@link android.hardware.camera2.CaptureResult} object.  This removes or overrides
     * all previous tags set.
     * </p>
     *
     * @param characteristics an object containing the static
     *          {@link android.hardware.camera2.CameraCharacteristics}.
     * @param metadata a metadata object to generate tags from.
     */
    public DngCreator(CameraCharacteristics characteristics, CaptureResult metadata) {
        if (characteristics == null || metadata == null) {
            throw new NullPointerException("Null argument to DngCreator constructor");
        }

        // Find current time
        long currentTime = System.currentTimeMillis();

        // Find boot time
        long bootTimeMillis = currentTime - SystemClock.elapsedRealtime();

        // Find capture time (nanos since boot)
        Long timestamp = metadata.get(CaptureResult.SENSOR_TIMESTAMP);
        long captureTime = currentTime;
        if (timestamp != null) {
            captureTime = timestamp / 1000000 + bootTimeMillis;
        }

        // Format for metadata
        String formattedCaptureTime = sDateTimeStampFormat.format(captureTime);

        nativeInit(characteristics.getNativeCopy(), metadata.getNativeCopy(),
                formattedCaptureTime);
    }

    /**
     * Set the orientation value to write.
     *
     * <p>
     * This will be written as the TIFF "Orientation" tag {@code (0x0112)}.
     * Calling this will override any prior settings for this tag.
     * </p>
     *
     * @param orientation the orientation value to set, one of:
     *                    <ul>
     *                      <li>{@link android.media.ExifInterface#ORIENTATION_NORMAL}</li>
     *                      <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_HORIZONTAL}</li>
     *                      <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_180}</li>
     *                      <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_VERTICAL}</li>
     *                      <li>{@link android.media.ExifInterface#ORIENTATION_TRANSPOSE}</li>
     *                      <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_90}</li>
     *                      <li>{@link android.media.ExifInterface#ORIENTATION_TRANSVERSE}</li>
     *                      <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_270}</li>
     *                    </ul>
     * @return this {@link #DngCreator} object.
     * @hide
     */
    public DngCreator setOrientation(int orientation) {

        if (orientation < ExifInterface.ORIENTATION_UNDEFINED ||
                orientation > ExifInterface.ORIENTATION_ROTATE_270) {
            throw new IllegalArgumentException("Orientation " + orientation +
                    " is not a valid EXIF orientation value");
        }
        nativeSetOrientation(orientation);
        return this;
    }

    /**
     * Set the thumbnail image.
     *
     * <p>
     * Pixel data will be converted to a Baseline TIFF RGB image, with 8 bits per color channel.
     * The alpha channel will be discarded.
     * </p>
     *
     * <p>
     * The given bitmap should not be altered while this object is in use.
     * </p>
     *
     * @param pixels a {@link android.graphics.Bitmap} of pixel data.
     * @return this {@link #DngCreator} object.
     * @hide
     */
    public DngCreator setThumbnail(Bitmap pixels) {
        if (pixels == null) {
            throw new NullPointerException("Null argument to setThumbnail");
        }

        Bitmap.Config config = pixels.getConfig();

        if (config != Bitmap.Config.ARGB_8888) {
            pixels = pixels.copy(Bitmap.Config.ARGB_8888, false);
            if (pixels == null) {
                throw new IllegalArgumentException("Unsupported Bitmap format " + config);
            }
            nativeSetThumbnailBitmap(pixels);
        }

        return this;
    }

    /**
     * Set the thumbnail image.
     *
     * <p>
     * Pixel data is interpreted as a {@link android.graphics.ImageFormat#YUV_420_888} image.
     * </p>
     *
     * <p>
     * The given image should not be altered while this object is in use.
     * </p>
     *
     * @param pixels an {@link android.media.Image} object with the format
     *               {@link android.graphics.ImageFormat#YUV_420_888}.
     * @return this {@link #DngCreator} object.
     * @hide
     */
    public DngCreator setThumbnail(Image pixels) {
        if (pixels == null) {
            throw new NullPointerException("Null argument to setThumbnail");
        }

        int format = pixels.getFormat();
        if (format != ImageFormat.YUV_420_888) {
            throw new IllegalArgumentException("Unsupported image format " + format);
        }

        Image.Plane[] planes = pixels.getPlanes();
        nativeSetThumbnailImage(pixels.getWidth(), pixels.getHeight(), planes[0].getBuffer(),
                planes[0].getRowStride(), planes[0].getPixelStride(), planes[1].getBuffer(),
                planes[1].getRowStride(), planes[1].getPixelStride(), planes[1].getBuffer(),
                planes[1].getRowStride(), planes[1].getPixelStride());

        return this;
    }


    /**
     * Set image location metadata.
     *
     * <p>
     * The given location object must contain at least a valid time, latitude, and longitude
     * (equivalent to the values returned by {@link android.location.Location#getTime()},
     * {@link android.location.Location#getLatitude()}, and
     * {@link android.location.Location#getLongitude()} methods).
     * </p>
     *
     * @param location an {@link android.location.Location} object to set.
     * @return this {@link #DngCreator} object.
     *
     * @throws java.lang.IllegalArgumentException if the given location object doesn't
     *          contain enough information to set location metadata.
     * @hide
     */
    public DngCreator setLocation(Location location) {
        /*TODO*/
        return this;
    }

    /**
     * Set the user description string to write.
     *
     * <p>
     * This is equivalent to setting the TIFF "ImageDescription" tag {@code (0x010E)}.
     * </p>
     *
     * @param description the user description string.
     * @return this {@link #DngCreator} object.
     * @hide
     */
    public DngCreator setDescription(String description) {
        /*TODO*/
        return this;
    }

    /**
     * Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with
     * the currently configured metadata.
     *
     * <p>
     * Raw pixel data must have 16 bits per pixel, and the input must contain at least
     * {@code offset + 2 * width * height)} bytes.  The width and height of
     * the input are taken from the width and height set in the {@link DngCreator} metadata tags,
     * and will typically be equal to the width and height of
     * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}.
     * The pixel layout in the input is determined from the reported color filter arrangement (CFA)
     * set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}.  If insufficient
     * metadata is available to write a well-formatted DNG file, an
     * {@link java.lang.IllegalStateException} will be thrown.
     * </p>
     *
     * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to.
     * @param size the {@link Size} of the image to write, in pixels.
     * @param pixels an {@link java.io.InputStream} of pixel data to write.
     * @param offset the offset of the raw image in bytes.  This indicates how many bytes will
     *               be skipped in the input before any pixel data is read.
     *
     * @throws IOException if an error was encountered in the input or output stream.
     * @throws java.lang.IllegalStateException if not enough metadata information has been
     *          set to write a well-formatted DNG file.
     * @throws java.lang.IllegalArgumentException if the size passed in does not match the
     * @hide
     */
    public void writeInputStream(OutputStream dngOutput, Size size, InputStream pixels, long offset)
            throws IOException {
        if (dngOutput == null || pixels == null) {
            throw new NullPointerException("Null argument to writeImage");
        }
        nativeWriteInputStream(dngOutput, pixels, offset);
    }

    /**
     * Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with
     * the currently configured metadata.
     *
     * <p>
     * Raw pixel data must have 16 bits per pixel, and the input must contain at least
     * {@code offset + 2 * width * height)} bytes.  The width and height of
     * the input are taken from the width and height set in the {@link DngCreator} metadata tags,
     * and will typically be equal to the width and height of
     * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}.
     * The pixel layout in the input is determined from the reported color filter arrangement (CFA)
     * set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}.  If insufficient
     * metadata is available to write a well-formatted DNG file, an
     * {@link java.lang.IllegalStateException} will be thrown.
     * </p>
     *
     * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to.
     * @param size the {@link Size} of the image to write, in pixels.
     * @param pixels an {@link java.nio.ByteBuffer} of pixel data to write.
     * @param offset the offset of the raw image in bytes.  This indicates how many bytes will
     *               be skipped in the input before any pixel data is read.
     *
     * @throws IOException if an error was encountered in the input or output stream.
     * @throws java.lang.IllegalStateException if not enough metadata information has been
     *          set to write a well-formatted DNG file.
     * @hide
     */
    public void writeByteBuffer(OutputStream dngOutput, Size size, ByteBuffer pixels, long offset)
            throws IOException {
        if (dngOutput == null || pixels == null) {
            throw new NullPointerException("Null argument to writeImage");
        }
        nativeWriteByteBuffer(dngOutput, pixels, offset);
    }

    /**
     * Write the pixel data to a DNG file with the currently configured metadata.
     *
     * <p>
     * For this method to succeed, the {@link android.media.Image} input must contain
     * {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data, otherwise an
     * {@link java.lang.IllegalArgumentException} will be thrown.
     * </p>
     *
     * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to.
     * @param pixels an {@link android.media.Image} to write.
     *
     * @throws java.io.IOException if an error was encountered in the output stream.
     * @throws java.lang.IllegalArgumentException if an image with an unsupported format was used.
     * @throws java.lang.IllegalStateException if not enough metadata information has been
     *          set to write a well-formatted DNG file.
     */
    public void writeImage(OutputStream dngOutput, Image pixels) throws IOException {
        if (dngOutput == null || pixels == null) {
            throw new NullPointerException("Null argument to writeImage");
        }

        int format = pixels.getFormat();
        if (format != ImageFormat.RAW_SENSOR) {
            throw new IllegalArgumentException("Unsupported image format " + format);
        }

        Image.Plane[] planes = pixels.getPlanes();
        nativeWriteImage(dngOutput, pixels.getWidth(), pixels.getHeight(), planes[0].getBuffer(),
                planes[0].getRowStride(), planes[0].getPixelStride());
    }

    @Override
    public void close() {
        nativeDestroy();
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            close();
        } finally {
            super.finalize();
        }
    }

    private static final String TIFF_DATETIME_FORMAT = "yyyy:MM:dd kk:mm:ss";
    private static final DateFormat sDateTimeStampFormat =
            new SimpleDateFormat(TIFF_DATETIME_FORMAT);

    static {
        sDateTimeStampFormat.setTimeZone(TimeZone.getDefault());
    }
    /**
     * This field is used by native code, do not access or modify.
     */
    private long mNativeContext;

    private static native void nativeClassInit();

    private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics,
                                                CameraMetadataNative nativeResult,
                                                String captureTime);

    private synchronized native void nativeDestroy();

    private synchronized native void nativeSetOrientation(int orientation);

    private synchronized native void nativeSetThumbnailBitmap(Bitmap bitmap);

    private synchronized native void nativeSetThumbnailImage(int width, int height,
                                                             ByteBuffer yBuffer, int yRowStride,
                                                             int yPixStride, ByteBuffer uBuffer,
                                                             int uRowStride, int uPixStride,
                                                             ByteBuffer vBuffer, int vRowStride,
                                                             int vPixStride);

    private synchronized native void nativeWriteImage(OutputStream out, int width, int height,
                                                      ByteBuffer rawBuffer, int rowStride,
                                                      int pixStride) throws IOException;

    private synchronized native void nativeWriteByteBuffer(OutputStream out, ByteBuffer rawBuffer,
                                                           long offset) throws IOException;

    private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream,
                                                            long offset) throws IOException;

    static {
        nativeClassInit();
    }
}
