/*
 * Copyright (C) 2020 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.
 */

#include <math/scalar.h>

#include <camutils/Manipulator.h>

#include "FreeFlightManipulator.h"
#include "MapManipulator.h"
#include "OrbitManipulator.h"

using namespace filament::math;

namespace filament
{
    namespace camutils
    {

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::viewport(int width, int height)
        {
            details.viewport[0] = width;
            details.viewport[1] = height;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::targetPosition(FLOAT x, FLOAT y, FLOAT z)
        {
            details.targetPosition = {x, y, z};
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::upVector(FLOAT x, FLOAT y, FLOAT z)
        {
            details.upVector = {x, y, z};
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::zoomSpeed(FLOAT val)
        {
            details.zoomSpeed = val;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::orbitHomePosition(FLOAT x, FLOAT y, FLOAT z)
        {
            details.orbitHomePosition = {x, y, z};
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::orbitSpeed(FLOAT x, FLOAT y)
        {
            details.orbitSpeed = {x, y};
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::fovDirection(Fov fov)
        {
            details.fovDirection = fov;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::fovDegrees(FLOAT degrees)
        {
            details.fovDegrees = degrees;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::farPlane(FLOAT distance)
        {
            details.farPlane = distance;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::mapExtent(FLOAT worldWidth, FLOAT worldHeight)
        {
            details.mapExtent = {worldWidth, worldHeight};
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::mapMinDistance(FLOAT mindist)
        {
            details.mapMinDistance = mindist;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::flightStartPosition(FLOAT x, FLOAT y, FLOAT z)
        {
            details.flightStartPosition = {x, y, z};
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::flightStartOrientation(FLOAT pitch, FLOAT yaw)
        {
            details.flightStartPitch = pitch;
            details.flightStartYaw = yaw;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::flightMaxMoveSpeed(FLOAT maxSpeed)
        {
            details.flightMaxSpeed = maxSpeed;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::flightSpeedSteps(int steps)
        {
            details.flightSpeedSteps = steps;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::flightPanSpeed(FLOAT x, FLOAT y)
        {
            details.flightPanSpeed = {x, y};
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::flightMoveDamping(FLOAT damping)
        {
            details.flightMoveDamping = damping;
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::groundPlane(FLOAT a, FLOAT b, FLOAT c, FLOAT d)
        {
            details.groundPlane = {a, b, c, d};
            return *this;
        }

        template <typename FLOAT>
        typename Manipulator<FLOAT>::Builder &Manipulator<FLOAT>::Builder::raycastCallback(RayCallback cb, void *userdata)
        {
            details.raycastCallback = cb;
            details.raycastUserdata = userdata;
            return *this;
        }

        template <typename FLOAT>
        Manipulator<FLOAT> *Manipulator<FLOAT>::Builder::build(Mode mode)
        {
            switch (mode)
            {
            case Mode::FREE_FLIGHT:
                return new FreeFlightManipulator<FLOAT>(mode, details);
            case Mode::MAP:
                return new MapManipulator<FLOAT>(mode, details);
            case Mode::ORBIT:
                return new OrbitManipulator<FLOAT>(mode, details);
            }
        }

        template <typename FLOAT>
        Manipulator<FLOAT>::Manipulator(Mode mode, const Config &props) : mMode(mode)
        {
            setProperties(props);
        }

        template <typename FLOAT>
        void Manipulator<FLOAT>::setProperties(const Config &props)
        {
            mProps = props;

            if (mProps.zoomSpeed == FLOAT(0))
            {
                mProps.zoomSpeed = 0.01;
            }

            if (mProps.upVector == vec3(0))
            {
                mProps.upVector = vec3(0, 1, 0);
            }

            if (mProps.fovDegrees == FLOAT(0))
            {
                mProps.fovDegrees = 33;
            }

            if (mProps.farPlane == FLOAT(0))
            {
                mProps.farPlane = 5000;
            }

            if (mProps.mapExtent == vec2(0))
            {
                mProps.mapExtent = vec2(512);
            }
        }

        template <typename FLOAT>
        void Manipulator<FLOAT>::setViewport(int width, int height)
        {
            Config props = mProps;
            props.viewport[0] = width;
            props.viewport[1] = height;
            setProperties(props);
        }

        template <typename FLOAT>
        void Manipulator<FLOAT>::getLookAt(vec3 *eyePosition, vec3 *targetPosition, vec3 *upward) const
        {
            *targetPosition = mTarget;
            *eyePosition = mEye;
            const vec3 gaze = normalize(mTarget - mEye);
            const vec3 right = cross(gaze, mProps.upVector);
            *upward = cross(right, gaze);
        }

        template <typename FLOAT>
        static bool raycastPlane(const filament::math::vec3<FLOAT> &origin,
                                 const filament::math::vec3<FLOAT> &dir, FLOAT *t, void *userdata)
        {
            using vec3 = filament::math::vec3<FLOAT>;
            using vec4 = filament::math::vec4<FLOAT>;
            auto props = (const typename Manipulator<FLOAT>::Config *)userdata;
            const vec4 plane = props->groundPlane;
            const vec3 n = vec3(plane[0], plane[1], plane[2]);
            const vec3 p0 = n * plane[3];
            const FLOAT denom = -dot(n, dir);
            if (denom > 1e-6)
            {
                const vec3 p0l0 = p0 - origin;
                *t = dot(p0l0, n) / -denom;
                return *t >= 0;
            }
            return false;
        }

        template <typename FLOAT>
        void Manipulator<FLOAT>::getRay(int x, int y, vec3 *porigin, vec3 *pdir) const
        {
            const vec3 gaze = normalize(mTarget - mEye);
            const vec3 right = normalize(cross(gaze, mProps.upVector));
            const vec3 upward = cross(right, gaze);
            const FLOAT width = mProps.viewport[0];
            const FLOAT height = mProps.viewport[1];
            const FLOAT fov = mProps.fovDegrees * F_PI / 180.0;

            // Remap the grid coordinate into [-1, +1] and shift it to the pixel center.
            const FLOAT u = 2.0 * (0.5 + x) / width - 1.0;
            const FLOAT v = 2.0 * (0.5 + y) / height - 1.0;

            // Compute the tangent of the field-of-view angle as well as the aspect ratio.
            const FLOAT tangent = tan(fov / 2.0);
            const FLOAT aspect = width / height;

            // Adjust the gaze so it goes through the pixel of interest rather than the grid center.
            vec3 dir = gaze;
            if (mProps.fovDirection == Fov::VERTICAL)
            {
                dir += right * tangent * u * aspect;
                dir += upward * tangent * v;
            }
            else
            {
                dir += right * tangent * u;
                dir += upward * tangent * v / aspect;
            }
            dir = normalize(dir);

            *porigin = mEye;
            *pdir = dir;
        }

        template <typename FLOAT>
        bool Manipulator<FLOAT>::raycast(int x, int y, vec3 *result) const
        {
            vec3 origin, dir;
            getRay(x, y, &origin, &dir);

            // Choose either the user's callback function or the plane intersector.
            auto callback = mProps.raycastCallback;
            auto fallback = raycastPlane<FLOAT>;
            void *userdata = mProps.raycastUserdata;
            if (!callback)
            {
                callback = fallback;
                userdata = (void *)&mProps;
            }

            // If the ray misses, then try the fallback function.
            FLOAT t;
            if (!callback(mEye, dir, &t, userdata))
            {
                if (callback == fallback || !fallback(mEye, dir, &t, (void *)&mProps))
                {
                    return false;
                }
            }

            *result = mEye + dir * t;
            return true;
        }

        template <typename FLOAT>
        filament::math::vec3<FLOAT> Manipulator<FLOAT>::raycastFarPlane(int x, int y) const
        {
            const filament::math::vec3<FLOAT> gaze = normalize(mTarget - mEye);
            const vec3 right = cross(gaze, mProps.upVector);
            const vec3 upward = cross(right, gaze);
            const FLOAT width = mProps.viewport[0];
            const FLOAT height = mProps.viewport[1];
            const FLOAT fov = mProps.fovDegrees * math::F_PI / 180.0;

            // Remap the grid coordinate into [-1, +1] and shift it to the pixel center.
            const FLOAT u = 2.0 * (0.5 + x) / width - 1.0;
            const FLOAT v = 2.0 * (0.5 + y) / height - 1.0;

            // Compute the tangent of the field-of-view angle as well as the aspect ratio.
            const FLOAT tangent = tan(fov / 2.0);
            const FLOAT aspect = width / height;

            // Adjust the gaze so it goes through the pixel of interest rather than the grid center.
            vec3 dir = gaze;
            if (mProps.fovDirection == Fov::VERTICAL)
            {
                dir += right * tangent * u * aspect;
                dir += upward * tangent * v;
            }
            else
            {
                dir += right * tangent * u;
                dir += upward * tangent * v / aspect;
            }
            return mEye + dir * mProps.farPlane;
        }

        template <typename FLOAT>
        void Manipulator<FLOAT>::keyDown(Manipulator<FLOAT>::Key key) {}

        template <typename FLOAT>
        void Manipulator<FLOAT>::keyUp(Manipulator<FLOAT>::Key key) {}

        template <typename FLOAT>
        void Manipulator<FLOAT>::update(FLOAT deltaTime) {}

        template class Manipulator<float>;
        template class Manipulator<double>;

    } // namespace camutils
} // namespace filament
