/*
 * Copyright (C) 2012 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.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;

import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;

import java.util.Arrays;

/**
 * Maintains invariant that inner rectangle is constrained to be within the
 * outer, rotated rectangle.
 */
public class BoundedRect {
    private float rot;
    private RectF outer;
    private RectF inner;
    private float[] innerRotated;

    public BoundedRect(float rotation, Rect outerRect, Rect innerRect) {
        rot = rotation;
        outer = new RectF(outerRect);
        inner = new RectF(innerRect);
        innerRotated = CropMath.getCornersFromRect(inner);
        rotateInner();
        if (!isConstrained())
            reconstrain();
    }

    public BoundedRect(float rotation, RectF outerRect, RectF innerRect) {
        rot = rotation;
        outer = new RectF(outerRect);
        inner = new RectF(innerRect);
        innerRotated = CropMath.getCornersFromRect(inner);
        rotateInner();
        if (!isConstrained())
            reconstrain();
    }

    public void resetTo(float rotation, RectF outerRect, RectF innerRect) {
        rot = rotation;
        outer.set(outerRect);
        inner.set(innerRect);
        innerRotated = CropMath.getCornersFromRect(inner);
        rotateInner();
        if (!isConstrained())
            reconstrain();
    }

    /**
     * Sets inner, and re-constrains it to fit within the rotated bounding rect.
     */
    public void setInner(RectF newInner) {
        if (inner.equals(newInner))
            return;
        inner = newInner;
        innerRotated = CropMath.getCornersFromRect(inner);
        rotateInner();
        if (!isConstrained())
            reconstrain();
    }

    /**
     * Sets rotation, and re-constrains inner to fit within the rotated bounding rect.
     */
    public void setRotation(float rotation) {
        if (rotation == rot)
            return;
        rot = rotation;
        innerRotated = CropMath.getCornersFromRect(inner);
        rotateInner();
        if (!isConstrained())
            reconstrain();
    }

    public void setToInner(RectF r) {
        r.set(inner);
    }

    public void setToOuter(RectF r) {
        r.set(outer);
    }

    public RectF getInner() {
        return new RectF(inner);
    }

    public RectF getOuter() {
        return new RectF(outer);
    }

    /**
     * Tries to move the inner rectangle by (dx, dy).  If this would cause it to leave
     * the bounding rectangle, snaps the inner rectangle to the edge of the bounding
     * rectangle.
     */
    public void moveInner(float dx, float dy) {
        Matrix m0 = getInverseRotMatrix();

        RectF translatedInner = new RectF(inner);
        translatedInner.offset(dx, dy);

        float[] translatedInnerCorners = CropMath.getCornersFromRect(translatedInner);
        float[] outerCorners = CropMath.getCornersFromRect(outer);

        m0.mapPoints(translatedInnerCorners);
        float[] correction = {
                0, 0
        };

        // find correction vectors for corners that have moved out of bounds
        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
            float correctedInnerX = translatedInnerCorners[i] + correction[0];
            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
            if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
                float[] badCorner = {
                        correctedInnerX, correctedInnerY
                };
                float[] nearestSide = CropMath.closestSide(badCorner, outerCorners);
                float[] correctionVec =
                        GeometryMathUtils.shortestVectorFromPointToLine(badCorner, nearestSide);
                correction[0] += correctionVec[0];
                correction[1] += correctionVec[1];
            }
        }

        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
            float correctedInnerX = translatedInnerCorners[i] + correction[0];
            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
            if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
                float[] correctionVec = {
                        correctedInnerX, correctedInnerY
                };
                CropMath.getEdgePoints(outer, correctionVec);
                correctionVec[0] -= correctedInnerX;
                correctionVec[1] -= correctedInnerY;
                correction[0] += correctionVec[0];
                correction[1] += correctionVec[1];
            }
        }

        // Set correction
        for (int i = 0; i < translatedInnerCorners.length; i += 2) {
            float correctedInnerX = translatedInnerCorners[i] + correction[0];
            float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
            // update translated corners with correction vectors
            translatedInnerCorners[i] = correctedInnerX;
            translatedInnerCorners[i + 1] = correctedInnerY;
        }

        innerRotated = translatedInnerCorners;
        // reconstrain to update inner
        reconstrain();
    }

    /**
     * Attempts to resize the inner rectangle.  If this would cause it to leave
     * the bounding rect, clips the inner rectangle to fit.
     */
    public void resizeInner(RectF newInner) {
        Matrix m = getRotMatrix();
        Matrix m0 = getInverseRotMatrix();

        float[] outerCorners = CropMath.getCornersFromRect(outer);
        m.mapPoints(outerCorners);
        float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
        float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
        RectF ret = new RectF(newInner);

        for (int i = 0; i < newInnerCorners.length; i += 2) {
            float[] c = {
                    newInnerCorners[i], newInnerCorners[i + 1]
            };
            float[] c0 = Arrays.copyOf(c, 2);
            m0.mapPoints(c0);
            if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
                float[] outerSide = CropMath.closestSide(c, outerCorners);
                float[] pathOfCorner = {
                        newInnerCorners[i], newInnerCorners[i + 1],
                        oldInnerCorners[i], oldInnerCorners[i + 1]
                };
                float[] p = GeometryMathUtils.lineIntersect(pathOfCorner, outerSide);
                if (p == null) {
                    // lines are parallel or not well defined, so don't resize
                    p = new float[2];
                    p[0] = oldInnerCorners[i];
                    p[1] = oldInnerCorners[i + 1];
                }
                // relies on corners being in same order as method
                // getCornersFromRect
                switch (i) {
                    case 0:
                    case 1:
                        ret.left = (p[0] > ret.left) ? p[0] : ret.left;
                        ret.top = (p[1] > ret.top) ? p[1] : ret.top;
                        break;
                    case 2:
                    case 3:
                        ret.right = (p[0] < ret.right) ? p[0] : ret.right;
                        ret.top = (p[1] > ret.top) ? p[1] : ret.top;
                        break;
                    case 4:
                    case 5:
                        ret.right = (p[0] < ret.right) ? p[0] : ret.right;
                        ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
                        break;
                    case 6:
                    case 7:
                        ret.left = (p[0] > ret.left) ? p[0] : ret.left;
                        ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
                        break;
                    default:
                        break;
                }
            }
        }
        float[] retCorners = CropMath.getCornersFromRect(ret);
        m0.mapPoints(retCorners);
        innerRotated = retCorners;
        // reconstrain to update inner
        reconstrain();
    }

    /**
     * Attempts to resize the inner rectangle.  If this would cause it to leave
     * the bounding rect, clips the inner rectangle to fit while maintaining
     * aspect ratio.
     */
    public void fixedAspectResizeInner(RectF newInner) {
        Matrix m = getRotMatrix();
        Matrix m0 = getInverseRotMatrix();

        float aspectW = inner.width();
        float aspectH = inner.height();
        float aspRatio = aspectW / aspectH;
        float[] corners = CropMath.getCornersFromRect(outer);

        m.mapPoints(corners);
        float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
        float[] newInnerCorners = CropMath.getCornersFromRect(newInner);

        // find fixed corner
        int fixed = -1;
        if (inner.top == newInner.top) {
            if (inner.left == newInner.left)
                fixed = 0; // top left
            else if (inner.right == newInner.right)
                fixed = 2; // top right
        } else if (inner.bottom == newInner.bottom) {
            if (inner.right == newInner.right)
                fixed = 4; // bottom right
            else if (inner.left == newInner.left)
                fixed = 6; // bottom left
        }
        // no fixed corner, return without update
        if (fixed == -1)
            return;
        float widthSoFar = newInner.width();
        int moved = -1;
        for (int i = 0; i < newInnerCorners.length; i += 2) {
            float[] c = {
                    newInnerCorners[i], newInnerCorners[i + 1]
            };
            float[] c0 = Arrays.copyOf(c, 2);
            m0.mapPoints(c0);
            if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
                moved = i;
                if (moved == fixed)
                    continue;
                float[] l2 = CropMath.closestSide(c, corners);
                float[] l1 = {
                        newInnerCorners[i], newInnerCorners[i + 1],
                        oldInnerCorners[i], oldInnerCorners[i + 1]
                };
                float[] p = GeometryMathUtils.lineIntersect(l1, l2);
                if (p == null) {
                    // lines are parallel or not well defined, so set to old
                    // corner
                    p = new float[2];
                    p[0] = oldInnerCorners[i];
                    p[1] = oldInnerCorners[i + 1];
                }
                // relies on corners being in same order as method
                // getCornersFromRect
                float fixed_x = oldInnerCorners[fixed];
                float fixed_y = oldInnerCorners[fixed + 1];
                float newWidth = Math.abs(fixed_x - p[0]);
                float newHeight = Math.abs(fixed_y - p[1]);
                newWidth = Math.max(newWidth, aspRatio * newHeight);
                if (newWidth < widthSoFar)
                    widthSoFar = newWidth;
            }
        }

        float heightSoFar = widthSoFar / aspRatio;
        RectF ret = new RectF(inner);
        if (fixed == 0) {
            ret.right = ret.left + widthSoFar;
            ret.bottom = ret.top + heightSoFar;
        } else if (fixed == 2) {
            ret.left = ret.right - widthSoFar;
            ret.bottom = ret.top + heightSoFar;
        } else if (fixed == 4) {
            ret.left = ret.right - widthSoFar;
            ret.top = ret.bottom - heightSoFar;
        } else if (fixed == 6) {
            ret.right = ret.left + widthSoFar;
            ret.top = ret.bottom - heightSoFar;
        }
        float[] retCorners = CropMath.getCornersFromRect(ret);
        m0.mapPoints(retCorners);
        innerRotated = retCorners;
        // reconstrain to update inner
        reconstrain();
    }

    // internal methods

    private boolean isConstrained() {
        for (int i = 0; i < 8; i += 2) {
            if (!CropMath.inclusiveContains(outer, innerRotated[i], innerRotated[i + 1]))
                return false;
        }
        return true;
    }

    private void reconstrain() {
        // innerRotated has been changed to have incorrect values
        CropMath.getEdgePoints(outer, innerRotated);
        Matrix m = getRotMatrix();
        float[] unrotated = Arrays.copyOf(innerRotated, 8);
        m.mapPoints(unrotated);
        inner = CropMath.trapToRect(unrotated);
    }

    private void rotateInner() {
        Matrix m = getInverseRotMatrix();
        m.mapPoints(innerRotated);
    }

    private Matrix getRotMatrix() {
        Matrix m = new Matrix();
        m.setRotate(rot, outer.centerX(), outer.centerY());
        return m;
    }

    private Matrix getInverseRotMatrix() {
        Matrix m = new Matrix();
        m.setRotate(-rot, outer.centerX(), outer.centerY());
        return m;
    }
}
