/*
 * 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.gallery3d.filtershow.crop;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.android.gallery3d.R;


public class CropView extends View {
    private static final String LOGTAG = "CropView";

    private RectF mImageBounds = new RectF();
    private RectF mScreenBounds = new RectF();
    private RectF mScreenImageBounds = new RectF();
    private RectF mScreenCropBounds = new RectF();
    private Rect mShadowBounds = new Rect();

    private Bitmap mBitmap;
    private Paint mPaint = new Paint();

    private NinePatchDrawable mShadow;
    private CropObject mCropObj = null;
    private Drawable mCropIndicator;
    private int mIndicatorSize;
    private int mRotation = 0;
    private boolean mMovingBlock = false;
    private Matrix mDisplayMatrix = null;
    private Matrix mDisplayMatrixInverse = null;
    private boolean mDirty = false;

    private float mPrevX = 0;
    private float mPrevY = 0;
    private float mSpotX = 0;
    private float mSpotY = 0;
    private boolean mDoSpot = false;

    private int mShadowMargin = 15;
    private int mMargin = 32;
    private int mOverlayShadowColor = 0xCF000000;
    private int mOverlayWPShadowColor = 0x5F000000;
    private int mWPMarkerColor = 0x7FFFFFFF;
    private int mMinSideSize = 90;
    private int mTouchTolerance = 40;
    private float mDashOnLength = 20;
    private float mDashOffLength = 10;

    private enum Mode {
        NONE, MOVE
    }

    private Mode mState = Mode.NONE;

    public CropView(Context context) {
        super(context);
        setup(context);
    }

    public CropView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setup(context);
    }

    public CropView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setup(context);
    }

    private void setup(Context context) {
        Resources rsc = context.getResources();
        mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow);
        mCropIndicator = rsc.getDrawable(R.drawable.camera_crop);
        mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size);
        mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin);
        mMargin = (int) rsc.getDimension(R.dimen.preview_margin);
        mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side);
        mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance);
        mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color);
        mOverlayWPShadowColor = (int) rsc.getColor(R.color.crop_shadow_wp_color);
        mWPMarkerColor = (int) rsc.getColor(R.color.crop_wp_markers);
        mDashOnLength = rsc.getDimension(R.dimen.wp_selector_dash_length);
        mDashOffLength = rsc.getDimension(R.dimen.wp_selector_off_length);
    }

    public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) {
        mBitmap = image;
        if (mCropObj != null) {
            RectF crop = mCropObj.getInnerBounds();
            RectF containing = mCropObj.getOuterBounds();
            if (crop != newCropBounds || containing != newPhotoBounds
                    || mRotation != rotation) {
                mRotation = rotation;
                mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds);
                clearDisplay();
            }
        } else {
            mRotation = rotation;
            mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0);
            clearDisplay();
        }
    }

    public RectF getCrop() {
        return mCropObj.getInnerBounds();
    }

    public RectF getPhoto() {
        return mCropObj.getOuterBounds();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
            return true;
        }
        float[] touchPoint = {
                x, y
        };
        mDisplayMatrixInverse.mapPoints(touchPoint);
        x = touchPoint[0];
        y = touchPoint[1];
        switch (event.getActionMasked()) {
            case (MotionEvent.ACTION_DOWN):
                if (mState == Mode.NONE) {
                    if (!mCropObj.selectEdge(x, y)) {
                        mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK);
                    }
                    mPrevX = x;
                    mPrevY = y;
                    mState = Mode.MOVE;
                }
                break;
            case (MotionEvent.ACTION_UP):
                if (mState == Mode.MOVE) {
                    mCropObj.selectEdge(CropObject.MOVE_NONE);
                    mMovingBlock = false;
                    mPrevX = x;
                    mPrevY = y;
                    mState = Mode.NONE;
                }
                break;
            case (MotionEvent.ACTION_MOVE):
                if (mState == Mode.MOVE) {
                    float dx = x - mPrevX;
                    float dy = y - mPrevY;
                    mCropObj.moveCurrentSelection(dx, dy);
                    mPrevX = x;
                    mPrevY = y;
                }
                break;
            default:
                break;
        }
        invalidate();
        return true;
    }

    private void reset() {
        Log.w(LOGTAG, "crop reset called");
        mState = Mode.NONE;
        mCropObj = null;
        mRotation = 0;
        mMovingBlock = false;
        clearDisplay();
    }

    private void clearDisplay() {
        mDisplayMatrix = null;
        mDisplayMatrixInverse = null;
        invalidate();
    }

    protected void configChanged() {
        mDirty = true;
    }

    public void applyFreeAspect() {
        mCropObj.unsetAspectRatio();
        invalidate();
    }

    public void applyOriginalAspect() {
        RectF outer = mCropObj.getOuterBounds();
        float w = outer.width();
        float h = outer.height();
        if (w > 0 && h > 0) {
            applyAspect(w, h);
            mCropObj.resetBoundsTo(outer, outer);
        } else {
            Log.w(LOGTAG, "failed to set aspect ratio original");
        }
    }

    public void applySquareAspect() {
        applyAspect(1, 1);
    }

    public void applyAspect(float x, float y) {
        if (x <= 0 || y <= 0) {
            throw new IllegalArgumentException("Bad arguments to applyAspect");
        }
        // If we are rotated by 90 degrees from horizontal, swap x and y
        if (((mRotation < 0) ? -mRotation : mRotation) % 180 == 90) {
            float tmp = x;
            x = y;
            y = tmp;
        }
        if (!mCropObj.setInnerAspectRatio(x, y)) {
            Log.w(LOGTAG, "failed to set aspect ratio");
        }
        invalidate();
    }

    public void setWallpaperSpotlight(float spotlightX, float spotlightY) {
        mSpotX = spotlightX;
        mSpotY = spotlightY;
        if (mSpotX > 0 && mSpotY > 0) {
            mDoSpot = true;
        }
    }

    public void unsetWallpaperSpotlight() {
        mDoSpot = false;
    }

    /**
     * Rotates first d bits in integer x to the left some number of times.
     */
    private int bitCycleLeft(int x, int times, int d) {
        int mask = (1 << d) - 1;
        int mout = x & mask;
        times %= d;
        int hi = mout >> (d - times);
        int low = (mout << times) & mask;
        int ret = x & ~mask;
        ret |= low;
        ret |= hi;
        return ret;
    }

    /**
     * Find the selected edge or corner in screen coordinates.
     */
    private int decode(int movingEdges, float rotation) {
        int rot = CropMath.constrainedRotation(rotation);
        switch (rot) {
            case 90:
                return bitCycleLeft(movingEdges, 1, 4);
            case 180:
                return bitCycleLeft(movingEdges, 2, 4);
            case 270:
                return bitCycleLeft(movingEdges, 3, 4);
            default:
                return movingEdges;
        }
    }

    @Override
    public void onDraw(Canvas canvas) {
        if (mBitmap == null) {
            return;
        }
        if (mDirty) {
            mDirty = false;
            clearDisplay();
        }

        mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
        mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
        mScreenBounds.inset(mMargin, mMargin);

        // If crop object doesn't exist, create it and update it from master
        // state
        if (mCropObj == null) {
            reset();
            mCropObj = new CropObject(mImageBounds, mImageBounds, 0);
        }

        // If display matrix doesn't exist, create it and its dependencies
        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
            mDisplayMatrix = new Matrix();
            mDisplayMatrix.reset();
            if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds,
                    mRotation)) {
                Log.w(LOGTAG, "failed to get screen matrix");
                mDisplayMatrix = null;
                return;
            }
            mDisplayMatrixInverse = new Matrix();
            mDisplayMatrixInverse.reset();
            if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) {
                Log.w(LOGTAG, "could not invert display matrix");
                mDisplayMatrixInverse = null;
                return;
            }
            // Scale min side and tolerance by display matrix scale factor
            mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize));
            mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance));
        }

        mScreenImageBounds.set(mImageBounds);

        // Draw background shadow
        if (mDisplayMatrix.mapRect(mScreenImageBounds)) {
            int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin);
            mScreenImageBounds.roundOut(mShadowBounds);
            mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top -
                    margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin);
            mShadow.setBounds(mShadowBounds);
            mShadow.draw(canvas);
        }

        mPaint.setAntiAlias(true);
        mPaint.setFilterBitmap(true);
        // Draw actual bitmap
        canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint);

        mCropObj.getInnerBounds(mScreenCropBounds);

        if (mDisplayMatrix.mapRect(mScreenCropBounds)) {

            // Draw overlay shadows
            Paint p = new Paint();
            p.setColor(mOverlayShadowColor);
            p.setStyle(Paint.Style.FILL);
            CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds);

            // Draw crop rect and markers
            CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds);
            if (!mDoSpot) {
                CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds);
            } else {
                Paint wpPaint = new Paint();
                wpPaint.setColor(mWPMarkerColor);
                wpPaint.setStrokeWidth(3);
                wpPaint.setStyle(Paint.Style.STROKE);
                wpPaint.setPathEffect(new DashPathEffect(new float[]
                        {mDashOnLength, mDashOnLength + mDashOffLength}, 0));
                p.setColor(mOverlayWPShadowColor);
                CropDrawingUtils.drawWallpaperSelectionFrame(canvas, mScreenCropBounds,
                        mSpotX, mSpotY, wpPaint, p);
            }
            CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize,
                    mScreenCropBounds, mCropObj.isFixedAspect(), decode(mCropObj.getSelectState(), mRotation));
        }

    }
}
