/*
 * Copyright 2016-2019 devemux86
 * Copyright 2017 Luca Osten
 * Copyright 2018 Izumi Kawashima
 * Copyright 2018 Mathieu De Brito
 *
 * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.oscim.map;

import org.oscim.core.MapPosition;
import org.oscim.core.Point;
import org.oscim.core.Tile;
import org.oscim.renderer.GLMatrix;
import org.oscim.utils.FastMath;
import org.oscim.utils.ThreadUtils;

import static org.oscim.utils.FastMath.clamp;

public class ViewController extends Viewport {

    private float mPivotX = 0.0f;
    private float mPivotY = 0.0f;

    private final float[] mat = new float[16];

    public void setViewSize(int width, int height) {
        ThreadUtils.assertMainThread();

        mHeight = height;
        mWidth = width;

        /* setup projection matrix:
         * 0. scale to window coordinates
         * 1. translate to VIEW_DISTANCE
         * 2. apply projection
         * setup inverse projection:
         * 0. invert projection
         * 1. invert translate to VIEW_DISTANCE */

        float ratio = (mHeight / mWidth) * VIEW_SCALE;

        GLMatrix.frustumM(mat, 0, -VIEW_SCALE, VIEW_SCALE,
                ratio, -ratio, VIEW_NEAR, VIEW_FAR);

        mProjMatrix.set(mat);

        mTmpMatrix.setTranslation(0, 0, -VIEW_DISTANCE);
        mProjMatrix.multiplyRhs(mTmpMatrix);

        /* set inverse projection matrix (without scaling) */
        mProjMatrix.get(mat);
        GLMatrix.invertM(mat, 0, mat, 0);
        mProjMatrixInverse.set(mat);

        mProjMatrixUnscaled.copy(mProjMatrix);

        /* scale to window coordinates */
        mTmpMatrix.setScale(1 / mWidth, 1 / mWidth, 1 / mWidth);
        mProjMatrix.multiplyRhs(mTmpMatrix);

        updateMatrices();
    }

    /**
     * Get pivot horizontal / vertical relative to view center.
     * e.g. pivotY 0.5 is usually preferred for navigation, moving center to 25% of view height.
     * Range is [-1, 1].
     */
    public float[] getMapViewCenter() {
        return new float[]{mPivotX, mPivotY};
    }

    /**
     * Set pivot horizontal / vertical relative to view center.
     * e.g. pivotY 0.5 is usually preferred for navigation, moving center to 25% of view height.
     * Range is [-1, 1].
     */
    public void setMapViewCenter(float pivotX, float pivotY) {
        mPivotX = FastMath.clamp(pivotX, -1, 1) * 0.5f;
        mPivotY = FastMath.clamp(pivotY, -1, 1) * 0.5f;
    }

    /**
     * Moves this Viewport by the given amount of pixels.
     *
     * @param mx the amount of pixels to move the map horizontally.
     * @param my the amount of pixels to move the map vertically.
     */
    public synchronized void moveMap(float mx, float my) {
        ThreadUtils.assertMainThread();

        applyRotation(mx, my, mPos.bearing, mMovePoint);
        double tileScale = mPos.scale * Tile.SIZE;
        moveTo(mPos.x - mMovePoint.x / tileScale, mPos.y - mMovePoint.y / tileScale);
    }

    /* used by MapAnimator */
    void moveTo(double x, double y) {
        mPos.x = x;
        mPos.y = y;

        /* clamp latitude */
        mPos.y = FastMath.clamp(mPos.y, 0, 1);

        /* wrap longitude */
        while (mPos.x > 1)
            mPos.x -= 1;
        while (mPos.x < 0)
            mPos.x += 1;

        /* limit longitude */
        if (mPos.x > mMaxX)
            mPos.x = mMaxX;
        else if (mPos.x < mMinX)
            mPos.x = mMinX;
        /* limit latitude */
        if (mPos.y > mMaxY)
            mPos.y = mMaxY;
        else if (mPos.y < mMinY)
            mPos.y = mMinY;
    }

    /**
     * @param mx      the amount of pixels to move the map horizontally.
     * @param my      the amount of pixels to move the map vertically.
     * @param bearing the bearing to rotate the map.
     * @param out     the position where to move.
     */
    public static void applyRotation(double mx, double my, float bearing, Point out) {
        if (out == null)
            out = new Point();

        if (bearing == 0) {
            out.x = mx;
            out.y = my;
        } else {
            double rad = Math.toRadians(bearing);
            double rcos = Math.cos(rad);
            double rsin = Math.sin(rad);
            out.x = mx * rcos + my * rsin;
            out.y = mx * -rsin + my * rcos;
        }
    }

    /**
     * Scale map by scale width center at pivot in pixel relative to
     * view center. Map scale is clamp to MIN_SCALE and MAX_SCALE.
     *
     * @return true if scale was changed
     */
    public boolean scaleMap(float scale, float pivotX, float pivotY) {
        ThreadUtils.assertMainThread();

        // just sanitize input
        //scale = FastMath.clamp(scale, 0.5f, 2);
        if (scale < 0.000001)
            return false;

        double newScale = mPos.scale * scale;

        newScale = clamp(newScale, mMinScale, mMaxScale);

        if (newScale == mPos.scale)
            return false;

        scale = (float) (newScale / mPos.scale);

        mPos.scale = newScale;

        if (pivotX != 0 || pivotY != 0) {
            pivotX -= mWidth * mPivotX;
            pivotY -= mHeight * mPivotY;

            moveMap(pivotX * (1.0f - scale),
                    pivotY * (1.0f - scale));
        }
        return true;
    }

    /**
     * Rotate map by radians around pivot. Pivot is in pixel relative
     * to view center.
     */
    public void rotateMap(double radians, float pivotX, float pivotY) {
        ThreadUtils.assertMainThread();

        double rsin = Math.sin(radians);
        double rcos = Math.cos(radians);

        pivotX -= mWidth * mPivotX;
        pivotY -= mHeight * mPivotY;

        float x = (float) (pivotX - pivotX * rcos + pivotY * rsin);
        float y = (float) (pivotY - pivotX * rsin - pivotY * rcos);

        moveMap(x, y);

        setRotation(mPos.bearing + Math.toDegrees(radians));
    }

    public void setRotation(double degree) {
        ThreadUtils.assertMainThread();

        degree = FastMath.clampDegree(degree);

        mPos.bearing = (float) degree;

        updateMatrices();
    }

    public void rollMap(float move) {
        setRoll(mPos.roll + move);
    }

    public void setRoll(double degree) {
        ThreadUtils.assertMainThread();

        degree = FastMath.clampDegree(degree);

        mPos.roll = (float) degree;

        updateMatrices();
    }

    public boolean tiltMap(float move) {
        return setTilt(mPos.tilt + move);
    }

    public boolean setTilt(float tilt) {
        ThreadUtils.assertMainThread();

        tilt = limitTilt(tilt);
        if (tilt == mPos.tilt)
            return false;

        mPos.tilt = tilt;
        updateMatrices();
        return true;
    }

    public void setMapPosition(MapPosition mapPosition) {
        ThreadUtils.assertMainThread();

        mPos.copy(mapPosition);
        limitPosition(mPos);

        //    mPos.scale = clamp(mapPosition.scale, mMinScale, mMaxScale);
        //    mPos.x = mapPosition.x;
        //    mPos.y = mapPosition.y;
        //    mPos.tilt = limitTilt(mapPosition.tilt);
        //    mPos.bearing = mapPosition.bearing;

        updateMatrices();
    }

    private void updateMatrices() {
        /* - view matrix:
         * 0. apply yaw
         * 1. apply roll
         * 2. apply pitch */

        mRotationMatrix.setRotation(mPos.bearing, 0, 0, 1);

        mTmpMatrix.setRotation(mPos.roll, 0, 1, 0);
        mRotationMatrix.multiplyLhs(mTmpMatrix);

        mTmpMatrix.setRotation(mPos.tilt, 1, 0, 0);
        mRotationMatrix.multiplyLhs(mTmpMatrix);

        mViewMatrix.copy(mRotationMatrix);

        mTmpMatrix.setTranslation(mPivotX * mWidth, mPivotY * mHeight, 0);
        mViewMatrix.multiplyLhs(mTmpMatrix);

        mViewProjMatrix.multiplyMM(mProjMatrix, mViewMatrix);

        mViewProjMatrix.get(mat);
        GLMatrix.invertM(mat, 0, mat, 0);
        mUnprojMatrix.set(mat);
    }

    public final Viewport mNextFrame = new Viewport();

    /**
     * synchronize on this object when doing multiple calls on it
     */
    public final Viewport getSyncViewport() {
        return mNextFrame;
    }

    boolean sizeChanged() {
        synchronized (mNextFrame) {
            return mNextFrame.sizeChanged(this);
        }
    }

    public void syncViewport() {
        synchronized (mNextFrame) {
            mNextFrame.copy(this);
        }
    }

    public boolean getSyncViewport(Viewport v) {
        synchronized (mNextFrame) {
            return v.copy(mNextFrame);
        }
    }

    public boolean getSyncMapPosition(MapPosition mapPosition) {
        synchronized (mNextFrame) {
            return mNextFrame.getMapPosition(mapPosition);
        }
    }

}
