#include "Screen.h"

#include <iostream>

#include "Drawing.h"
#include "File.h"
#include "Globals.h"
#include "GraphicsApi.h"
#include "Image.h"
#include "Logging.h"
#include "Mat3.h"
#include "Model3D.h"
#include "MouseEvent.h"
#include "Text.h"
#include "Timer.h"
#include "ToolTip.h"
#include "Window.h"
#ifdef WIN32
#    include <d3d11.h>
#    include <d3d9.h>
#    include <D3Dcompiler.h>
#    include <DirectXMath.h>

#    include "Camera3D.h"
#    include "comdef.h"
#endif

using namespace Framework;

// Content of the Screen class from Screen.h
// Constructor
Screen::Screen(NativeWindow* f)
    : ReferenceCounter(),
      api(0),
      fenster(f),
      members(new RCArray<Drawable>()),
      fillColor(0xFF000000),
      deckColor(0),
      onTop(0),
      renderOnTop(0),
      renderDrawables(1),
      rendering(0),
      renderTime(new Timer()),
      tips(new RCArray<ToolTip>()),
      testRend(1),
      fill(1),
      rend(0),
      handleUserInputsOnTick(0)
{}

// Destructor
Screen::~Screen()
{
    rwLock.lockWrite();
#ifdef WIN32
    if (fenster) fenster->release();
#endif
    members->release();
    tips->release();
    renderTime->release();
    if (onTop) onTop->release();
    if (api) api->release();
    rwLock.unlockWrite();
}

void Screen::postAction(std::function<void()> action)
{
    rwLock.lockWrite();
    actions.push(action);
    rwLock.unlockWrite();
}

void Screen::setHandleUserInputsOnTick(bool handleOnTick)
{
    handleUserInputsOnTick = handleOnTick;
}

// non-constant
Lock& Screen::readLock()
{
    return rwLock.getReadLock();
}

Lock& Screen::writeLock()
{
    return rwLock.getWriteLock();
}

void Screen::setFill(bool f)
{
    fill = f;
}

// Updates the objects that manage the graphics card
void Screen::update()
{
    rwLock.lockRead();
    api->update();
    rwLock.unlockRead();
}

void Screen::setTestRend(
    bool tr) // specifies whether to check for updates before rendering
{
    testRend = tr;
}

void Screen::setRenderDrawables(
    bool rO) // specifies whether drawings are rendered
{
    rwLock.lockWrite();
    renderDrawables = rO;
    rend = 1;
    rwLock.unlockWrite();
}

void Screen::setOnTop(
    bool onTop) // specifies whether the onTop drawing is rendered
{
    renderOnTop = onTop;
    rend = 1;
}

void Screen::setOnTopDrawable(Drawable* obj) // sets the onTop drawing
{
    rwLock.lockWrite();
    if (onTop) onTop->release();
    onTop = obj;
    rend = 1;
    rwLock.unlockWrite();
}

void Screen::setdeckColor(int f) // sets the overlay color
{
    deckColor = f;
    rend = 1;
}

void Screen::addMember(Drawable* obj) // Adds a drawing
{
    rwLock.lockWrite();
    members->add(obj);
    rend = 1;
    rwLock.unlockWrite();
}

void Screen::removeMember(Drawable* zObj) // Removes a drawing
{
    rwLock.lockWrite();
    for (int i = 0; i < members->getEntryCount(); i++)
    {
        if (members->z(i) == zObj) members->remove(i);
    }
    rend = 1;
    rwLock.unlockWrite();
}

void Screen::setFillColor(int f) // sets the fill color
{
    fillColor = f;
    rend = 1;
}

void Screen::setFullscreen(bool vollbild) // sets fullscreen
{
    rwLock.lockRead();
    api->setFullScreen(vollbild);
    rwLock.unlockRead();
}

void Screen::tick(double tickval)
{
    rwLock.lockRead();
    while (!actions.empty())
    {
        actions.front()();
        rwLock.lockWrite();
        actions.pop();
        rwLock.unlockWrite();
    }
    if (!renderOnTop)
    {
        for (ArrayIterator<ToolTip*> i = tips->begin(); i; i++)
        {
            i->tick(tickval);
            if (i->getReferenceCount() == 1)
            {
                rwLock.lockWrite();
                i.remove();
                rwLock.unlockWrite();
            }
        }
        for (Drawable* i : *members)
            rend |= i->tick(tickval);
    }
    else if (onTop)
    {
        rend |= onTop->tick(tickval);
        for (ArrayIterator<ToolTip*> i = tips->begin(); i; i++)
        {
            i->tick(tickval);
            if (i->getReferenceCount() == 1)
            {
                rwLock.lockWrite();
                i.remove();
                rwLock.unlockWrite();
            }
        }
    }
    rwLock.unlockRead();
}

void Screen::setBackBufferSize(
    int Width, int height) // sets the size of the back buffer
{
    rwLock.lockRead();
    api->setBackBufferSize(Vec2<int>(Width, height));
    rwLock.unlockRead();
}

void Screen::setBackBufferSize(Point& size)
{
    rwLock.lockRead();
    api->setBackBufferSize(size);
    rwLock.unlockRead();
}

void Screen::doMouseEvent(MouseEvent& me) // sends mouse event
{
    int fBr = api->getBackBufferSize().x;
    int fHi = api->getBackBufferSize().y;
#ifdef WIN32
    if (fenster)
    {
        fBr = fenster->getBodyWidth();
        fHi = fenster->getBodyHeight();
    }
#endif
    me.mx = (int)(me.mx * api->getBackBufferSize().x / (double)fBr + 0.5);
    me.my = (int)(me.my * api->getBackBufferSize().y / (double)fHi + 0.5);
    rwLock.lockRead();
    if (!renderOnTop)
    {
        for (ArrayIterator<ToolTip*> i = tips->begin(); i; i++)
        {
            i->doPublicMouseEvent(me);
        }
        for (int i = members->getEntryCount() - 1; i >= 0; i--)
            members->z(i)->doPublicMouseEvent(me);
    }
    else if (onTop)
    {
        onTop->doPublicMouseEvent(me);
        for (ArrayIterator<ToolTip*> i = tips->begin(); i; i++)
        {
            i->doPublicMouseEvent(me);
        }
    }
    rwLock.unlockRead();
}

void Screen::doKeyboardEvent(KeyboardEvent& te) // sends keyboard event
{
    rwLock.lockRead();
    if (!renderOnTop)
    {
        for (int i = members->getEntryCount() - 1; i >= 0; i--)
            members->z(i)->doKeyboardEvent(te);
    }
    for (ArrayIterator<ToolTip*> i = tips->begin(); i; i++)
    {
        i->doKeyboardEvent(te);
    }
    rwLock.unlockRead();
}

void Screen::addToolTip(ToolTip* tip) // adds a ToolTip
{
    rwLock.lockWrite();
    tips->add(tip);
    rend = 1;
    rwLock.unlockWrite();
}

// constant
Image* Screen::getRenderImage() const
{
    return dynamic_cast<Image*>(api->zUIRenderImage()->getThis());
}

Image* Screen::zRenderImage() const
{
    return api->zUIRenderImage();
}

ArrayIterator<Drawable*> Screen::getMembers() const // returns the drawings
{
    return members->begin();
}

int Screen::getFillColor() const // returns the fill color
{
    return fillColor;
}

bool Screen::isFullscreen() const // returns whether fullscreen is on
{
    return api->isFullScreen();
}

const Point
Screen::getBackBufferSize() const // returns the size of the back buffer
{
    return api->getBackBufferSize();
}

void Screen::waitForRendering() const // waits for the render function
{
    while (rendering)
    {
        if (!rendering) return;
    }
}

double Screen::getRenderZeit() const // returns how many seconds rendering takes
{
    return renderTime->getSekunden();
}

// Returns the graphics API (without increased reference counter)
GraphicsApi* Screen::zGraphicsApi() const
{
    return api;
}

// Returns the graphics API
GraphicsApi* Screen::getGraphicsApi() const
{
    return api ? dynamic_cast<GraphicsApi*>(api->getThis()) : 0;
}

#ifdef WIN32
int MonitorEnum(HMONITOR m, HDC dc, LPRECT r, LPARAM p)
{
    MONITORINFOEXA info;
    ZeroMemory(&info, sizeof(info));
    info.cbSize = sizeof(info);
    GetMonitorInfo(m, &info);
    Monitor* mon = new Monitor();
    mon->exists = 1;
    mon->x = r->left;
    mon->y = r->top;
    mon->width = r->right - r->left;
    mon->height = r->bottom - r->top;
    mon->name = info.szDevice;
    if (mon->x == 0 && mon->y == 0)
    {
        ((Array<Monitor*>*)p)->add(mon, 0);
    }
    else
    {
        ((Array<Monitor*>*)p)->add(mon);
    }
    return 1;
}

Monitor Framework::getMonitor(int id)
{
    if (id < 0)
    {
        Monitor m;
        m.exists = 0;
        return m;
    }
    Array<Monitor*>* monitore = new Array<Monitor*>();
    DISPLAY_DEVICE dispDev;
    ZeroMemory(&dispDev, sizeof(dispDev));
    dispDev.cb = sizeof(dispDev);
    EnumDisplayDevices(NULL, id, &dispDev, 0);
    EnumDisplayMonitors(0, 0, (MONITORENUMPROC)MonitorEnum, (LPARAM)monitore);
    int anz = monitore->getEntryCount();
    Monitor result;
    result.exists = 0;
    result = *monitore->get(id);
    for (Monitor* m : *monitore)
    {
        delete m;
    }
    monitore->release();
    return result;
}

// Screen2D
// Constructor
Screen2D::Screen2D(NativeWindow* fenster)
    : Screen(fenster)
{
    api = new DirectX9();
    api->initialize(dynamic_cast<NativeWindow*>(fenster->getThis()),
        fenster->getBodySize(),
        0);
}

// Destructor
Screen2D::~Screen2D() {}

void Screen2D::render() // Draws the image
{
    if (!rend && testRend) return;
    rendering = 1;
    Image* ui = api->zUIRenderImage();
    if (ui)
    {
        rwLock.lockRead();
        renderTime->measureStart();
        api->beginFrame(fill && (rend || !testRend), 0, fillColor);
        if (renderDrawables)
        {
            if (renderOnTop && deckColor
                && (deckColor & (fillColor | 0xFF000000)) == deckColor)
            {
                ui->setAlpha(255 - (unsigned char)(deckColor >> 24));
                for (Drawable* z : *members)
                    z->render(*ui); // draw to intermediate buffer
                ui->releaseAlpha();
            }
            else
            {
                for (Drawable* z : *members)
                    z->render(*ui); // draw to intermediate buffer
                if (renderOnTop && deckColor)
                    ui->alphaRegion(
                        0, 0, ui->getWidth(), ui->getHeight(), deckColor);
            }
            for (ArrayIterator<ToolTip*> i = tips->begin(); i; i++)
            {
                i->render(*ui);
                if (i->getReferenceCount() == 1) i.remove();
            }
        }
        if (renderOnTop && onTop) onTop->render(*ui);
        api->presentFrame();
        renderTime->measureEnd();
        rwLock.unlockRead();
    }
    rendering = 0;
    rend = 0;
}

// Screen3D
// Constructor
Screen3D::Screen3D(NativeWindow* fenster)
    : Screen(fenster),
      kameras(new RCArray<Cam3D>()),
      rend3D(0)
{
    if (DirectX12::isAvailable())
        api = new DirectX12();
    else if (DirectX11::isAvailable())
        api = new DirectX11();
    else
        api = new DirectX9();
    api->initialize(dynamic_cast<NativeWindow*>(fenster->getThis()),
        fenster->getBodySize(),
        0);
}

Screen3D::Screen3D(NativeWindow* fenster, GraphicApiType apiTyp)
    : Screen(fenster),
      kameras(new RCArray<Cam3D>()),
      rend3D(0)
{
    if (apiTyp == DIRECTX9) api = new DirectX9();
    if (apiTyp == DIRECTX11) api = new DirectX11();
    if (apiTyp == DIRECTX12) api = new DirectX12();
    api->initialize(dynamic_cast<NativeWindow*>(fenster->getThis()),
        fenster->getBodySize(),
        0);
}

Screen3D::Screen3D(NativeWindow* fenster, GraphicsApi* api)
    : Screen(fenster),
      kameras(new RCArray<Cam3D>()),
      rend3D(0)
{
    this->api = api;
    api->initialize(dynamic_cast<NativeWindow*>(fenster->getThis()),
        fenster->getBodySize(),
        0);
}

// Destructor
Screen3D::~Screen3D()
{
    kameras->release();
}

// non-constant
void Screen3D::addKamera(Cam3D* obj) // Adds a camera
{
    rwLock.lockWrite();
    kameras->add(obj);
    rend3D = 1;
    rwLock.unlockWrite();
}

void Screen3D::removeKamera(Cam3D* zObj) // Removes a camera
{
    rwLock.lockWrite();
    for (int i = 0; kameras->z(i); i++)
    {
        if (kameras->z(i) == zObj)
        {
            kameras->remove(i);
            break;
        }
    }
    rend3D = 1;
    rwLock.unlockWrite();
}

void Screen3D::tick(double tickval)
{
    rwLock.lockRead();
    __super::tick(tickval);
    for (Cam3D* k : *kameras)
        rend3D |= k->tick(tickval);
    rwLock.unlockRead();
    if (handleUserInputsOnTick)
    {
        queueCs.lock();
        while (meQueue.getEntryCount() > 0)
        {
            MouseEvent me = meQueue.get(0);
            __super::doMouseEvent(me);
            for (int i = kameras->getEntryCount() - 1; i >= 0; i--)
                kameras->z(i)->doMouseEvent(me);
            meQueue.remove(0);
        }
        while (teQueue.getEntryCount() > 0)
        {
            KeyboardEvent te = teQueue.get(0);
            __super::doKeyboardEvent(te);
            for (int i = kameras->getEntryCount() - 1; i >= 0; i--)
                kameras->z(i)->doKeyboardEvent(te);
            teQueue.remove(0);
        }
        queueCs.unlock();
    }
}

void Screen3D::doMouseEvent(MouseEvent& me) // sends mouse event
{
    if (handleUserInputsOnTick)
    {
        queueCs.lock();
        meQueue.add(me);
        queueCs.unlock();
    }
    else
    {
        rwLock.lockRead();
        __super::doMouseEvent(me);
        for (int i = kameras->getEntryCount() - 1; i >= 0; i--)
            kameras->z(i)->doMouseEvent(me);
        rwLock.unlockRead();
    }
}

void Screen3D::doKeyboardEvent(KeyboardEvent& te) // sends keyboard event
{
    if (handleUserInputsOnTick)
    {
        queueCs.lock();
        teQueue.add(te);
        queueCs.unlock();
    }
    else
    {
        rwLock.lockRead();
        __super::doKeyboardEvent(te);
        for (int i = kameras->getEntryCount() - 1; i >= 0; i--)
            kameras->z(i)->doKeyboardEvent(te);
        rwLock.unlockRead();
    }
}

void Screen3D::render() // Draws the image
{
    rendering = 1;
    rwLock.lockRead();
    renderTime->measureStart();
    // Clear the back buffer.
    api->beginFrame(fill && (rend || !testRend),
        fill && (rend3D || !testRend || rend),
        fillColor);
    if (rend3D || !testRend || rend)
    {
        // Render 3d Objects
        for (Cam3D* k : *kameras)
            api->renderKamera(k);
        rend3D = 0;
    }
    // render User Interface
    Image* ui = api->zUIRenderImage();
    if ((rend || !testRend) && ui)
    {
        if (renderDrawables)
        {
            if (renderOnTop && deckColor
                && (deckColor & (fillColor | 0xFF000000)) == deckColor)
            {
                ui->setAlpha(255 - (unsigned char)(deckColor >> 24));
                for (Drawable* z : *members)
                    z->render(*ui); // draw to intermediate buffer
                ui->releaseAlpha();
            }
            else
            {
                for (Drawable* z : *members)
                    z->render(*ui); // draw to intermediate buffer
                if (renderOnTop && deckColor)
                    ui->alphaRegion(
                        0, 0, ui->getWidth(), ui->getHeight(), deckColor);
            }
            for (ArrayIterator<ToolTip*> i = tips->begin(); i; i++)
            {
                i->render(*ui);
            }
        }
        if (renderOnTop && onTop) onTop->render(*ui);
    }
    api->presentFrame();
    rwLock.unlockRead();
    renderTime->measureEnd();
#    ifdef _DEBUG
    // Logging::trace() << renderTime->getSekunden() << "\n";
#    endif
    rendering = 0;
    rend = 0;
}
#endif