/*
 * 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.ui;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ImageView;

import com.android.camera.data.LocalDataUtil;
import com.android.camera.debug.Log;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class ZoomView extends ImageView {

    private static final Log.Tag TAG = new Log.Tag("ZoomView");

    private int mViewportWidth = 0;
    private int mViewportHeight = 0;

    private BitmapRegionDecoder mRegionDecoder;
    // This is null when there's no decoding going on.
    private DecodePartialBitmap mPartialDecodingTask;

    private Uri mUri;
    private int mOrientation;

    private class DecodePartialBitmap extends AsyncTask<RectF, Void, Bitmap> {
        BitmapRegionDecoder mDecoder;

        @Override
        protected void onPreExecute() {
            mDecoder = mRegionDecoder;
        }

        @Override
        protected Bitmap doInBackground(RectF... params) {
            RectF endRect = params[0];

            // Calculate the rotation matrix to apply orientation on the original image
            // rect.
            InputStream isForDimensions = getInputStream();
            if (isForDimensions == null) {
                return null;
            }

            Point imageSize = LocalDataUtil.decodeBitmapDimension(isForDimensions);
            try {
                isForDimensions.close();
            } catch (IOException e) {
                Log.e(TAG, "exception closing dimensions inputstream", e);
            }
            if (imageSize == null) {
                return null;
            }

            RectF fullResRect = new RectF(0, 0, imageSize.x - 1, imageSize.y - 1);
            Matrix rotationMatrix = new Matrix();
            rotationMatrix.setRotate(mOrientation, 0, 0);
            rotationMatrix.mapRect(fullResRect);
            // Set the translation of the matrix so that after rotation, the top left
            // of the image rect is at (0, 0)
            rotationMatrix.postTranslate(-fullResRect.left, -fullResRect.top);
            rotationMatrix.mapRect(fullResRect, new RectF(0, 0, imageSize.x - 1,
                    imageSize.y - 1));

            // Find intersection with the screen
            RectF visibleRect = new RectF(endRect);
            visibleRect.intersect(0, 0, mViewportWidth - 1, mViewportHeight - 1);
            // Calculate the mapping (i.e. transform) between current low res rect
            // and full res image rect, and apply the mapping on current visible rect
            // to find out the partial region in the full res image that we need
            // to decode.
            Matrix mapping = new Matrix();
            mapping.setRectToRect(endRect, fullResRect, Matrix.ScaleToFit.CENTER);
            RectF visibleAfterRotation = new RectF();
            mapping.mapRect(visibleAfterRotation, visibleRect);

            // Now the visible region we have is rotated, we need to reverse the
            // rotation to find out the region in the original image
            RectF visibleInImage = new RectF();
            Matrix invertRotation = new Matrix();
            rotationMatrix.invert(invertRotation);
            invertRotation.mapRect(visibleInImage, visibleAfterRotation);

            // Decode region
            Rect region = new Rect();
            visibleInImage.round(region);

            // Make sure region to decode is inside the image.
            region.intersect(0, 0, imageSize.x - 1, imageSize.y - 1);

            if (region.width() == 0 || region.height() == 0) {
                Log.e(TAG, "Invalid size for partial region. Region: " + region.toString());
                return null;
            }

            if (isCancelled()) {
                return null;
            }

            BitmapFactory.Options options = new BitmapFactory.Options();
            if ((mOrientation + 360) % 180 == 0) {
                options.inSampleSize = getSampleFactor(region.width(), region.height());
            } else {
                // The decoded region will be rotated 90/270 degrees before showing
                // on screen. In other words, the width and height will be swapped.
                // Therefore, sample factor should be calculated using swapped width
                // and height.
                options.inSampleSize = getSampleFactor(region.height(), region.width());
            }

            if (mDecoder == null) {
                InputStream is = getInputStream();
                if (is == null) {
                    return null;
                }

                try {
                    mDecoder = BitmapRegionDecoder.newInstance(is, false);
                    is.close();
                } catch (IOException e) {
                    Log.e(TAG, "Failed to instantiate region decoder");
                }
            }
            if (mDecoder == null) {
                return null;
            }
            Bitmap b = mDecoder.decodeRegion(region, options);
            if (isCancelled()) {
                return null;
            }
            Matrix rotation = new Matrix();
            rotation.setRotate(mOrientation);
            return Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), rotation, false);
        }

        @Override
        protected void onPostExecute(Bitmap b) {
            mPartialDecodingTask = null;
            if (mDecoder != mRegionDecoder) {
                // This decoder will no longer be used, recycle it.
                mDecoder.recycle();
            }
            if (b != null) {
                setImageBitmap(b);
                showPartiallyDecodedImage(true);
            }
        }
    }

    public ZoomView(Context context) {
        super(context);
        setScaleType(ScaleType.FIT_CENTER);
        addOnLayoutChangeListener(new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                                       int oldLeft, int oldTop, int oldRight, int oldBottom) {
                int w = right - left;
                int h = bottom - top;
                if (mViewportHeight != h || mViewportWidth != w) {
                    mViewportWidth = w;
                    mViewportHeight = h;
                }
            }
        });
    }

    public void resetDecoder() {
        if (mRegionDecoder != null) {
            cancelPartialDecodingTask();
            if (mPartialDecodingTask == null) {
                // No ongoing decoding task, safe to recycle the decoder.
                mRegionDecoder.recycle();
            }
            mRegionDecoder = null;
        }
    }

    public void loadBitmap(Uri uri, int orientation, RectF imageRect) {
        if (!uri.equals(mUri)) {
            resetDecoder();
            mUri = uri;
            mOrientation = orientation;
        }
        startPartialDecodingTask(imageRect);
    }

    private void showPartiallyDecodedImage(boolean show) {
        if (show) {
            setVisibility(View.VISIBLE);
        } else {
            setVisibility(View.GONE);
        }
    }

    public void cancelPartialDecodingTask() {
        if (mPartialDecodingTask != null && !mPartialDecodingTask.isCancelled()) {
            mPartialDecodingTask.cancel(true);
            setVisibility(GONE);
        }
    }

    /**
     * If the given rect is smaller than viewport on x or y axis, center rect within
     * viewport on the corresponding axis. Otherwise, make sure viewport is within
     * the bounds of the rect.
     */
    public static RectF adjustToFitInBounds(RectF rect, int viewportWidth, int viewportHeight) {
        float dx = 0, dy = 0;
        RectF newRect = new RectF(rect);
        if (newRect.width() < viewportWidth) {
            dx = viewportWidth / 2 - (newRect.left + newRect.right) / 2;
        } else {
            if (newRect.left > 0) {
                dx = -newRect.left;
            } else if (newRect.right < viewportWidth) {
                dx = viewportWidth - newRect.right;
            }
        }

        if (newRect.height() < viewportHeight) {
            dy = viewportHeight / 2 - (newRect.top + newRect.bottom) / 2;
        } else {
            if (newRect.top > 0) {
                dy = -newRect.top;
            } else if (newRect.bottom < viewportHeight) {
                dy = viewportHeight - newRect.bottom;
            }
        }

        if (dx != 0 || dy != 0) {
            newRect.offset(dx, dy);
        }
        return newRect;
    }

    private void startPartialDecodingTask(RectF endRect) {
        // Cancel on-going partial decoding tasks
        cancelPartialDecodingTask();
        mPartialDecodingTask = new DecodePartialBitmap();
        mPartialDecodingTask.execute(endRect);
    }

    // TODO: Cache the inputstream
    private InputStream getInputStream() {
        InputStream is = null;
        try {
            is = getContext().getContentResolver().openInputStream(mUri);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "File not found at: " + mUri);
        }
        return is;
    }

    /**
     * Find closest sample factor that is power of 2, based on the given width and height
     *
     * @param width width of the partial region to decode
     * @param height height of the partial region to decode
     * @return sample factor
     */
    private int getSampleFactor(int width, int height) {

        float fitWidthScale = ((float) mViewportWidth) / ((float) width);
        float fitHeightScale = ((float) mViewportHeight) / ((float) height);

        float scale = Math.min(fitHeightScale, fitWidthScale);

        // Find the closest sample factor that is power of 2
        int sampleFactor = (int) (1f / scale);
        if (sampleFactor <=1) {
            return 1;
        }
        for (int i = 0; i < 32; i++) {
            if ((1 << (i + 1)) > sampleFactor) {
                sampleFactor = (1 << i);
                break;
            }
        }
        return sampleFactor;
    }
}
