#include "Camera3D.h"

#include <d3d11.h>
#include <DirectXMath.h>

#include "Globals.h"
#include "MouseEvent.h"
#include "Shader.h"
#include "KeyboardEvent.h"
#include "World3D.h"

using namespace Framework;

// Contents of the Cam3D class
// Constructor
Cam3D::Cam3D()
    : ReferenceCounter(),
      rend(0)
{
    openingAngle = (float)PI / 4;
    minZ = 0.1f;
    maxZ = 5000;
    pos = Vec3<float>(0, 0, -1000);
    rotX = 0;
    rotY = 0;
    rotZ = 0;

    viewport.x = 0;
    viewport.y = 0;
    viewport.front = 0.f;
    viewport.back = 1.f;
    viewport.width = 200;
    viewport.height = 200;

    welt = 0;
    style = 0;
    speed = 1;

    updateMatrix();
}

// Destructor
Cam3D::~Cam3D()
{
    if (welt) welt->release();
}

// private

// Updates the view and projection matrices
void Cam3D::updateMatrix()
{
    view = view.rotationX(-rotX) * view.rotationY(-rotY) * view.rotationZ(-rotZ)
         * view.translation(Vec3<float>(-pos.x, -pos.y, -pos.z));
    proj = proj.projektion(
        openingAngle, viewport.width / viewport.height, minZ, maxZ);
}

// Sets the position of the camera in the 3D world
void Cam3D::setPosition(Vec3<float> pos)
{
    this->pos = pos;
    rend = 1;
    updateMatrix();
}

// Zooms in by moving the camera closer to the view target
//  val: The length of the distance the camera should move
void Cam3D::scrollIn(float val)
{
    Vec3<float> n(0, 0, 1);
    Mat4<float> tmp
        = tmp.rotationY(rotY) * tmp.rotationX(rotX) * tmp.rotationZ(rotZ);
    n = tmp * n * val;
    pos += n;
    rend = 1;
    updateMatrix();
}

// Zooms out by moving the camera away from the view target
//  val: The length of the distance the camera should move
void Cam3D::scrollOut(float val)
{
    Vec3<float> n(0, 0, 1);
    Mat4<float> tmp
        = tmp.rotationY(rotY) * tmp.rotationX(rotX) * tmp.rotationZ(rotZ);
    n = tmp * n * val;
    pos -= n;
    rend = 1;
    updateMatrix();
}

// Aligns the camera so that it points exactly at a specific point
//  ziel: The point the camera should point at
void Cam3D::setAusrichtung(Vec3<float> ziel)
{
    Vec3<float> target = (ziel - pos).normalize();
    if (Vec3<float>(0, target.y, target.z).getLength() == 0)
        rotX = 0;
    else
        rotX = -lowPrecisionACos(
            target.z / Vec3<float>(0, target.y, target.z).getLength());
    if (target.y < 0) rotX = -rotX;
    if (Vec3<float>(target.x, 0, target.z).getLength() == 0)
        rotY = 0;
    else
        rotY = lowPrecisionACos(
            abs(target.z) / Vec3<float>(target.x, 0, target.z).getLength());
    if (target.x < 0) rotY = -rotY;
    rotZ = 0;
    rend = 1;
    updateMatrix();
}

//! Sets the rotation of the camera around each axis
//! \param rotation The rotation around each axis
void Cam3D::setRotation(Vec3<float> rotation)
{
    rotX = rotation.x;
    rotY = rotation.y;
    rotZ = rotation.z;
    rend = 1;
    updateMatrix();
}

// Sets the position of the image on the screen
//  p: A point with x and y coordinates in pixels
void Cam3D::setScreenPosition(Point p)
{
    viewport.x = (float)p.x;
    viewport.y = (float)p.y;
}

// Sets the position of the image on the screen
//  x: The x coordinate in pixels
//  y: The y coordinate in pixels
void Cam3D::setScreenPosition(int x, int y)
{
    viewport.x = (float)x;
    viewport.y = (float)y;
}

// Sets the size of the image on the screen
//  p: A point, with x as width and y as height in pixels
void Cam3D::setScreenSize(Point p)
{
    viewport.width = (float)p.x;
    viewport.height = (float)p.y;
    updateMatrix();
}

// Sets the size of the image on the screen
//  br: The width in pixels
//  hoe: The height in pixels
void Cam3D::setScreenSize(int br, int hoe)
{
    viewport.width = (float)br;
    viewport.height = (float)hoe;
    updateMatrix();
}

// Sets the world to be drawn
//  w: The world
void Cam3D::setWorld(World3D* w)
{
    if (welt) welt->release();
    welt = w;
}

// Sets the style of the camera
//  style: The new style consisting of the flags from the associated Style
//  class
void Cam3D::setStyle(__int64 style)
{
    this->style = style;
}

// Sets the style of the camera
//  style: All Style Flags to be changed
//  add_remove: 1 if the style should be added. 0 if the style
//  should be removed
void Cam3D::setStyle(__int64 style, bool add_remove)
{
    if (add_remove)
        this->style |= style;
    else if (!add_remove)
        this->style &= ~style;
}

// Adds style flags
//  style: The style to be added
void Cam3D::addStyle(__int64 style)
{
    this->style |= style;
}

// Removes style flags
//  style: The style to be removed
void Cam3D::removeStyle(__int64 style)
{
    this->style &= ~style;
}

//! Set the movement speed per second if the camera has style Movable
void Cam3D::setMovementSpeed(float speed)
{
    this->speed = speed;
}

// Processes the elapsed time
//  tickval: The time in seconds since the last call of the function
//  has passed return: true, if the image needs to be redrawn,
//  false otherwise.
bool Cam3D::tick(double tv)
{
    bool ret = rend;
    rend = 0;
    if (hasStyle(Style::Rotatable))
    {
        if (getKeyState(T_Oben))
        {
            rotX -= (float)tv;
            ret = 1;
        }
        if (getKeyState(T_Unten))
        {
            rotX += (float)tv;
            ret = 1;
        }
        if (getKeyState(T_Links))
        {
            rotY -= (float)tv;
            ret = 1;
        }
        if (getKeyState(T_Rechts))
        {
            rotY += (float)tv;
            ret = 1;
        }
    }
    if (hasStyle(Style::Movable))
    {
        Vec3<float> n(0, 0, 1);
        Vec3<float> n2(1, 0, 0);
        Vec3<float> n3(0, 1, 0);
        Mat4<float> tmp = tmp.rotationY(rotY) * tmp.rotationX(rotX);
        n = tmp * n;
        n = n * (float)tv * speed;
        n2 = tmp * n2;
        n2 = n2 * (float)tv * speed;
        n3 = tmp * n3;
        n3 = n3 * (float)tv * speed;
        if (getKeyState('W'))
        {
            pos += n;
            ret = 1;
        }
        if (getKeyState('S'))
        {
            pos -= n;
            ret = 1;
        }
        if (getKeyState('D'))
        {
            pos += n2;
            ret = 1;
        }
        if (getKeyState('A'))
        {
            pos -= n2;
            ret = 1;
        }
        if (getKeyState(' '))
        {
            pos += n3;
            ret = 1;
        }
        if (getKeyState(T_Shift))
        {
            pos -= n3;
            ret = 1;
        }
    }
    updateMatrix();
    if (welt && hasStyle(Style::Tick)) return welt->tick(tv) || ret;
    return ret;
}

// Processes a mouse event
//  me: The mouse event to be processed
void Cam3D::doMouseEvent(MouseEvent& me)
{
    if (me.processed) return;
    if (me.mx > viewport.x && me.my > viewport.y
        && me.mx < viewport.x + viewport.width
        && me.my < viewport.y + viewport.height)
    {
        MouseEvent3D me3d;
        me3d.id = me.id;
        me3d.processed = me.processed;
        Vec3<float> mouseP
            = Vec3<float>((me.mx - viewport.x) / (0.5f * viewport.width) - 1,
                (me.my - viewport.y) / (0.5f * viewport.height) - 1,
                0);
        Vec3<float> mouseT = Vec3<float>(mouseP.x, mouseP.y, 1);
        Mat4<float> mat = proj * view;
        mat = mat.getInverse();
        mouseP = mat * mouseP;
        mouseT = mat * mouseT;
        me3d.pos = mouseP;
        me3d.dir = mouseT - mouseP;
        me.processed = 1;
    }
}

// Processes a keyboard event
//  te: The keyboard event to be processed
void Cam3D::doKeyboardEvent(KeyboardEvent& te) {}

// Returns whether certain styles are set
//  style: The styles to be checked
//  return: 1, if all styles in style are set
bool Cam3D::hasStyle(__int64 style) const
{
    return (this->style | style) == this->style;
}

// Returns whether certain styles are not set
//  style: The styles to be checked
//  return: 1, if all styles in style are not set
bool Cam3D::hasStyleNot(__int64 style) const
{
    return (this->style | style) != this->style;
}

// Returns a pointer to the viewport
const ViewPort* Cam3D::zViewPort() const
{
    return &viewport;
}

// Returns the position of the camera in the world
const Vec3<float>& Cam3D::getWorldPosition() const
{
    return pos;
}

// Returns the position in the world
//  screen: the screen position to be translated
const Vec3<float> Cam3D::getWorldPosition(Point screen) const
{
    Vec3<float> point = Vec3<float>(
        (screen.x - viewport.x) / (0.5f * viewport.width) - 1,
        (viewport.height - (screen.y - viewport.y)) / (0.5f * viewport.height)
            - 1,
        0.1f);
    Mat4<float> mat = getProjectionMatrix();
    Mat4<float> inv = getViewMatrix().getInverse();
    point.x = (point.x * 0.1f) / mat.elements[0][0];
    point.y = (point.y * 0.1f) / mat.elements[1][1];
    return inv * point;
}

// Returns the direction of the camera in the world
//  screen: the screen position to be translated
const Vec3<float> Cam3D::getWorldDirection(Point screen) const
{
    Vec3<float> point = Vec3<float>(
        (screen.x - viewport.x) / (0.5f * viewport.width) - 1,
        (viewport.height - (screen.y - viewport.y)) / (0.5f * viewport.height)
            - 1,
        0.1f);
    Vec3<float> pointT = Vec3<float>(point.x, point.y, 1);
    Mat4<float> mat = getProjectionMatrix();
    Mat4<float> inv = getViewMatrix().getInverse();
    point.x = (point.x * 0.1f) / mat.elements[0][0];
    point.y = (point.y * 0.1f) / mat.elements[1][1];
    pointT.x = (pointT.x) / mat.elements[0][0];
    pointT.y = (pointT.y) / mat.elements[1][1];
    point = inv * point;
    pointT = inv * pointT;
    return pointT - point;
}

// Returns the projection matrix of the camera
const Mat4<float>& Cam3D::getProjectionMatrix() const
{
    return proj;
}

// Returns the view matrix of the camera
const Mat4<float>& Cam3D::getViewMatrix() const
{
    return view;
}

//! Returns the rotation around each axis
const Vec3<float> Cam3D::getRotation() const
{
    return {rotX, rotY, rotZ};
}

//! Returns the position of the camera on the screen
const Point Cam3D::getScreenPos() const
{
    return Point((int)viewport.x, (int)viewport.y);
}

//! Returns the size of the camera on the screen
const Point Cam3D::getScreenSize() const
{
    return Point((int)viewport.width, (int)viewport.height);
}

// Returns the world
World3D* Cam3D::getWorld() const
{
    return welt ? dynamic_cast<World3D*>(welt->getThis()) : 0;
}

// Returns the world
World3D* Cam3D::zWorld() const
{
    return welt;
}