/*
 * Copyright (C) 2013 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 com.android.camera.tinyplanet;

import android.app.DialogFragment;
import android.app.ProgressDialog;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;

import com.adobe.xmp.XMPException;
import com.adobe.xmp.XMPMeta;
import com.android.camera.CameraActivity;
import com.android.camera.app.CameraServicesImpl;
import com.android.camera.app.MediaSaver;
import com.android.camera.app.MediaSaver.OnMediaSavedListener;
import com.android.camera.debug.Log;
import com.android.camera.exif.ExifInterface;
import com.android.camera.tinyplanet.TinyPlanetPreview.PreviewSizeListener;
import com.android.camera.util.XmpUtil;
import com.android.camera2.R;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.TimeZone;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * An activity that provides an editor UI to create a TinyPlanet image from a
 * 360 degree stereographically mapped panoramic image.
 */
public class TinyPlanetFragment extends DialogFragment implements PreviewSizeListener {
    /** Argument to tell the fragment the URI of the original panoramic image. */
    public static final String ARGUMENT_URI = "uri";
    /** Argument to tell the fragment the title of the original panoramic image. */
    public static final String ARGUMENT_TITLE = "title";

    public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS =
            "CroppedAreaImageWidthPixels";
    public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS =
            "CroppedAreaImageHeightPixels";
    public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS =
            "FullPanoWidthPixels";
    public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS =
            "FullPanoHeightPixels";
    public static final String CROPPED_AREA_LEFT =
            "CroppedAreaLeftPixels";
    public static final String CROPPED_AREA_TOP =
            "CroppedAreaTopPixels";
    public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/";

    private static final Log.Tag TAG = new Log.Tag("TinyPlanetActivity");
    /** Delay between a value update and the renderer running. */
    private static final int RENDER_DELAY_MILLIS = 50;
    /** Filename prefix to prepend to the original name for the new file. */
    private static final String FILENAME_PREFIX = "TINYPLANET_";

    private Uri mSourceImageUri;
    private TinyPlanetPreview mPreview;
    private int mPreviewSizePx = 0;
    private float mCurrentZoom = 0.5f;
    private float mCurrentAngle = 0;
    private ProgressDialog mDialog;

    /**
     * Lock for the result preview bitmap. We can't change it while we're trying
     * to draw it.
     */
    private final Lock mResultLock = new ReentrantLock();

    /** The title of the original panoramic image. */
    private String mOriginalTitle = "";

    /** The padded source bitmap. */
    private Bitmap mSourceBitmap;
    /** The resulting preview bitmap. */
    private Bitmap mResultBitmap;

    /** Used to delay-post a tiny planet rendering task. */
    private final Handler mHandler = new Handler();
    /** Whether rendering is in progress right now. */
    private Boolean mRendering = false;
    /**
     * Whether we should render one more time after the current rendering run is
     * done. This is needed when there was an update to the values during the
     * current rendering.
     */
    private Boolean mRenderOneMore = false;

    /** Tiny planet data plus size. */
    private static final class TinyPlanetImage {
        public final byte[] mJpegData;
        public final int mSize;

        public TinyPlanetImage(byte[] jpegData, int size) {
            mJpegData = jpegData;
            mSize = size;
        }
    }

    /**
     * Creates and executes a task to create a tiny planet with the current
     * values.
     */
    private final Runnable mCreateTinyPlanetRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mRendering) {
                if (mRendering) {
                    mRenderOneMore = true;
                    return;
                }
                mRendering = true;
            }

            (new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                    mResultLock.lock();
                    try {
                        if (mSourceBitmap == null || mResultBitmap == null) {
                            return null;
                        }
                        int width = mSourceBitmap.getWidth();
                        int height = mSourceBitmap.getHeight();
                        TinyPlanetNative.process(mSourceBitmap, width, height, mResultBitmap,
                                mPreviewSizePx, mCurrentZoom, mCurrentAngle);
                    } finally {
                        mResultLock.unlock();
                    }
                    return null;
                }

                @Override
                protected void onPostExecute(Void result) {
                    mPreview.setBitmap(mResultBitmap, mResultLock);
                    synchronized (mRendering) {
                        mRendering = false;
                        if (mRenderOneMore) {
                            mRenderOneMore = false;
                            scheduleUpdate();
                        }
                    }
                }
            }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Camera);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        getDialog().setCanceledOnTouchOutside(true);

        View view = inflater.inflate(R.layout.tinyplanet_editor,
                container, false);
        mPreview = (TinyPlanetPreview) view.findViewById(R.id.preview);
        mPreview.setPreviewSizeChangeListener(this);

        // Zoom slider setup.
        SeekBar zoomSlider = (SeekBar) view.findViewById(R.id.zoomSlider);
        zoomSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // Do nothing.
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                // Do nothing.
            }

            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                onZoomChange(progress);
            }
        });

        // Rotation slider setup.
        SeekBar angleSlider = (SeekBar) view.findViewById(R.id.angleSlider);
        angleSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // Do nothing.
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                // Do nothing.
            }

            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                onAngleChange(progress);
            }
        });

        Button createButton = (Button) view.findViewById(R.id.creatTinyPlanetButton);
        createButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                onCreateTinyPlanet();
            }
        });

        mOriginalTitle = getArguments().getString(ARGUMENT_TITLE);
        mSourceImageUri = Uri.parse(getArguments().getString(ARGUMENT_URI));
        mSourceBitmap = createPaddedSourceImage(mSourceImageUri, true);

        if (mSourceBitmap == null) {
            Log.e(TAG, "Could not decode source image.");
            dismiss();
        }
        return view;
    }

    /**
     * From the given URI this method creates a 360/180 padded image that is
     * ready to be made a tiny planet.
     */
    private Bitmap createPaddedSourceImage(Uri sourceImageUri, boolean previewSize) {
        InputStream is = getInputStream(sourceImageUri);
        if (is == null) {
            Log.e(TAG, "Could not create input stream for image.");
            dismiss();
        }
        Bitmap sourceBitmap = BitmapFactory.decodeStream(is);

        is = getInputStream(sourceImageUri);
        XMPMeta xmp = XmpUtil.extractXMPMeta(is);

        if (xmp != null) {
            int size = previewSize ? getDisplaySize() : sourceBitmap.getWidth();
            sourceBitmap = createPaddedBitmap(sourceBitmap, xmp, size);
        }
        return sourceBitmap;
    }

    /**
     * Starts an asynchronous task to create a tiny planet. Once done, will add
     * the new image to the filmstrip and dismisses the fragment.
     */
    private void onCreateTinyPlanet() {
        // Make sure we stop rendering before we create the high-res tiny
        // planet.
        synchronized (mRendering) {
            mRenderOneMore = false;
        }

        final String savingTinyPlanet = getActivity().getResources().getString(
                R.string.saving_tiny_planet);
        (new AsyncTask<Void, Void, TinyPlanetImage>() {
            @Override
            protected void onPreExecute() {
                mDialog = ProgressDialog.show(getActivity(), null, savingTinyPlanet, true, false);
            }

            @Override
            protected TinyPlanetImage doInBackground(Void... params) {
                return createFinalTinyPlanet();
            }

            @Override
            protected void onPostExecute(TinyPlanetImage image) {
                // Once created, store the new file and add it to the filmstrip.
                final CameraActivity activity = (CameraActivity) getActivity();
                MediaSaver mediaSaver = CameraServicesImpl.instance().getMediaSaver();
                OnMediaSavedListener doneListener =
                        new OnMediaSavedListener() {
                            @Override
                            public void onMediaSaved(Uri uri) {
                                // Add the new photo to the filmstrip and exit
                                // the fragment.
                                activity.notifyNewMedia(uri);
                                mDialog.dismiss();
                                TinyPlanetFragment.this.dismiss();
                            }
                        };
                String tinyPlanetTitle = FILENAME_PREFIX + mOriginalTitle;
                mediaSaver.addImage(image.mJpegData, tinyPlanetTitle, (new Date()).getTime(),
                        null,
                        image.mSize, image.mSize, 0, null, doneListener);
            }
        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /**
     * Creates the high quality tiny planet file and adds it to the media
     * service. Don't call this on the UI thread.
     */
    private TinyPlanetImage createFinalTinyPlanet() {
        // Free some memory we don't need anymore as we're going to dimiss the
        // fragment after the tiny planet creation.
        mResultLock.lock();
        try {
            mResultBitmap.recycle();
            mResultBitmap = null;
            mSourceBitmap.recycle();
            mSourceBitmap = null;
        } finally {
            mResultLock.unlock();
        }

        // Create a high-resolution padded image.
        Bitmap sourceBitmap = createPaddedSourceImage(mSourceImageUri, false);
        int width = sourceBitmap.getWidth();
        int height = sourceBitmap.getHeight();

        int outputSize = width / 2;
        Bitmap resultBitmap = Bitmap.createBitmap(outputSize, outputSize,
                Bitmap.Config.ARGB_8888);

        TinyPlanetNative.process(sourceBitmap, width, height, resultBitmap,
                outputSize, mCurrentZoom, mCurrentAngle);

        // Free the sourceImage memory as we don't need it and we need memory
        // for the JPEG bytes.
        sourceBitmap.recycle();
        sourceBitmap = null;

        ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
        resultBitmap.compress(CompressFormat.JPEG, 100, jpeg);
        return new TinyPlanetImage(addExif(jpeg.toByteArray()), outputSize);
    }

    /**
     * Adds basic EXIF data to the tiny planet image so it an be rewritten
     * later.
     *
     * @param jpeg the JPEG data of the tiny planet.
     * @return The JPEG data containing basic EXIF.
     */
    private byte[] addExif(byte[] jpeg) {
        ExifInterface exif = new ExifInterface();
        exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, System.currentTimeMillis(),
                TimeZone.getDefault());
        ByteArrayOutputStream jpegOut = new ByteArrayOutputStream();
        try {
            exif.writeExif(jpeg, jpegOut);
        } catch (IOException e) {
            Log.e(TAG, "Could not write EXIF", e);
        }
        return jpegOut.toByteArray();
    }

    private int getDisplaySize() {
        Display display = getActivity().getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        return Math.min(size.x, size.y);
    }

    @Override
    public void onSizeChanged(int sizePx) {
        mPreviewSizePx = sizePx;
        mResultLock.lock();
        try {
            if (mResultBitmap == null || mResultBitmap.getWidth() != sizePx
                    || mResultBitmap.getHeight() != sizePx) {
                if (mResultBitmap != null) {
                    mResultBitmap.recycle();
                }
                mResultBitmap = Bitmap.createBitmap(mPreviewSizePx, mPreviewSizePx,
                        Bitmap.Config.ARGB_8888);
            }
        } finally {
            mResultLock.unlock();
        }
        scheduleUpdate();
    }

    private void onZoomChange(int zoom) {
        // 1000 needs to be in sync with the max values declared in the layout
        // xml file.
        mCurrentZoom = zoom / 1000f;
        scheduleUpdate();
    }

    private void onAngleChange(int angle) {
        mCurrentAngle = (float) Math.toRadians(angle);
        scheduleUpdate();
    }

    /**
     * Delay-post a new preview rendering run.
     */
    private void scheduleUpdate() {
        mHandler.removeCallbacks(mCreateTinyPlanetRunnable);
        mHandler.postDelayed(mCreateTinyPlanetRunnable, RENDER_DELAY_MILLIS);
    }

    private InputStream getInputStream(Uri uri) {
        try {
            return getActivity().getContentResolver().openInputStream(uri);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Could not load source image.", e);
        }
        return null;
    }

    /**
     * To create a proper TinyPlanet, the input image must be 2:1 (360:180
     * degrees). So if needed, we pad the source image with black.
     */
    private static Bitmap createPaddedBitmap(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) {
        try {
            int croppedAreaWidth =
                    getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS);
            int croppedAreaHeight =
                    getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS);
            int fullPanoWidth =
                    getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS);
            int fullPanoHeight =
                    getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS);
            int left = getInt(xmp, CROPPED_AREA_LEFT);
            int top = getInt(xmp, CROPPED_AREA_TOP);

            if (fullPanoWidth == 0 || fullPanoHeight == 0) {
                return bitmapIn;
            }
            // Make sure the intermediate image has the similar size to the
            // input.
            Bitmap paddedBitmap = null;
            float scale = intermediateWidth / (float) fullPanoWidth;
            while (paddedBitmap == null) {
                try {
                    paddedBitmap = Bitmap.createBitmap(
                            (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale),
                            Bitmap.Config.ARGB_8888);
                } catch (OutOfMemoryError e) {
                    System.gc();
                    scale /= 2;
                }
            }
            Canvas paddedCanvas = new Canvas(paddedBitmap);

            int right = left + croppedAreaWidth;
            int bottom = top + croppedAreaHeight;
            RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale);
            paddedCanvas.drawBitmap(bitmapIn, null, destRect, null);
            return paddedBitmap;
        } catch (XMPException ex) {
            // Do nothing, just use mSourceBitmap as is.
        }
        return bitmapIn;
    }

    private static int getInt(XMPMeta xmp, String key) throws XMPException {
        if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) {
            return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key);
        } else {
            return 0;
        }
    }
}
