/*
    Copyright 2016-2019 Arisotura

    This file is part of melonDS.

    melonDS is free software: you can redistribute it and/or modify it under
    the terms of the GNU General Public License as published by the Free
    Software Foundation, either version 3 of the License, or (at your option)
    any later version.

    melonDS 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 General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with melonDS. If not, see http://www.gnu.org/licenses/.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL2/SDL.h>
#include "../Platform.h"
#include "PlatformConfig.h"
#include "LAN_Socket.h"
#include "LAN_PCap.h"
#include "libui/ui.h"
#include <string>

#ifdef __WIN32__
    #define NTDDI_VERSION		0x06000000 // GROSS FUCKING HACK
    #include <windows.h>
    //#include <knownfolders.h> // FUCK THAT SHIT
    extern "C" const GUID DECLSPEC_SELECTANY FOLDERID_RoamingAppData = {0x3eb685db, 0x65f9, 0x4cf6, {0xa0, 0x3a, 0xe3, 0xef, 0x65, 0x72, 0x9f, 0x3d}};
    #include <shlobj.h>
	#include <winsock2.h>
	#include <ws2tcpip.h>
	#define socket_t    SOCKET
	#define sockaddr_t  SOCKADDR
#else
    #include <glib.h>
	#include <unistd.h>
	#include <arpa/inet.h>
	#include <netinet/in.h>
	#include <sys/select.h>
	#include <sys/socket.h>
	#define socket_t    int
	#define sockaddr_t  struct sockaddr
	#define closesocket close
#endif

#ifndef INVALID_SOCKET
#define INVALID_SOCKET  (socket_t)-1
#endif


extern char* EmuDirectory;

void Stop(bool internal);


namespace Platform
{


typedef struct
{
    SDL_Thread* ID;
    void (*Func)();

} ThreadData;

int ThreadEntry(void* data)
{
    ThreadData* thread = (ThreadData*)data;
    thread->Func();
    return 0;
}


socket_t MPSocket;
sockaddr_t MPSendAddr;
u8 PacketBuffer[2048];

#define NIFI_VER 1


void StopEmu()
{
    Stop(true);
}


FILE* OpenFile(const char* path, const char* mode, bool mustexist)
{
    FILE* ret;

#ifdef __WIN32__

    int len = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0);
    if (len < 1) return NULL;
    WCHAR* fatpath = new WCHAR[len];
    int res = MultiByteToWideChar(CP_UTF8, 0, path, -1, fatpath, len);
    if (res != len) { delete[] fatpath; return NULL; } // checkme?

    // this will be more than enough
    WCHAR fatmode[4];
    fatmode[0] = mode[0];
    fatmode[1] = mode[1];
    fatmode[2] = mode[2];
    fatmode[3] = 0;

    if (mustexist)
    {
        ret = _wfopen(fatpath, L"rb");
        if (ret) ret = _wfreopen(fatpath, fatmode, ret);
    }
    else
        ret = _wfopen(fatpath, fatmode);

    delete[] fatpath;

#else

    if (mustexist)
    {
        ret = fopen(path, "rb");
        if (ret) ret = freopen(path, mode, ret);
    }
    else
        ret = fopen(path, mode);

#endif

    return ret;
}

FILE* OpenLocalFile(const char* path, const char* mode)
{
    bool relpath = false;
    int pathlen = strlen(path);

#ifdef __WIN32__
    if (pathlen > 3)
    {
        if (path[1] == ':' && path[2] == '\\')
            return OpenFile(path, mode);
    }
#else
    if (pathlen > 1)
    {
        if (path[0] == '/')
            return OpenFile(path, mode);
    }
#endif

    if (pathlen >= 3)
    {
        if (path[0] == '.' && path[1] == '.' && (path[2] == '/' || path[2] == '\\'))
            relpath = true;
    }

    int emudirlen = strlen(EmuDirectory);
    char* emudirpath;
    if (emudirlen)
    {
        int len = emudirlen + 1 + pathlen + 1;
        emudirpath = new char[len];
        strncpy(&emudirpath[0], EmuDirectory, emudirlen);
        emudirpath[emudirlen] = '/';
        strncpy(&emudirpath[emudirlen+1], path, pathlen);
        emudirpath[emudirlen+1+pathlen] = '\0';
    }
    else
    {
        emudirpath = new char[pathlen+1];
        strncpy(&emudirpath[0], path, pathlen);
        emudirpath[pathlen] = '\0';
    }

    // Locations are application directory, and AppData/melonDS on Windows or XDG_CONFIG_HOME/melonds on Linux

    FILE* f;

    // First check current working directory
    f = OpenFile(path, mode, true);
    if (f) { delete[] emudirpath; return f; }

    // then emu directory
    f = OpenFile(emudirpath, mode, true);
    if (f) { delete[] emudirpath; return f; }

#ifdef __WIN32__

    // a path relative to AppData wouldn't make much sense
    if (!relpath)
    {
        // Now check AppData
        PWSTR appDataPath = NULL;
        SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &appDataPath);
        if (!appDataPath)
        {
            delete[] emudirpath;
            return NULL;
        }

        // this will be more than enough
        WCHAR fatperm[4];
        fatperm[0] = mode[0];
        fatperm[1] = mode[1];
        fatperm[2] = mode[2];
        fatperm[3] = 0;

        int fnlen = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0);
        if (fnlen < 1) { delete[] emudirpath; return NULL; }
        WCHAR* wfileName = new WCHAR[fnlen];
        int res = MultiByteToWideChar(CP_UTF8, 0, path, -1, wfileName, fnlen);
        if (res != fnlen) { delete[] wfileName; delete[] emudirpath; return NULL; } // checkme?

        const WCHAR* appdir = L"\\melonDS\\";

        int pos = wcslen(appDataPath);
        void* ptr = CoTaskMemRealloc(appDataPath, (pos+wcslen(appdir)+fnlen+1)*sizeof(WCHAR));
        if (!ptr) { delete[] wfileName; delete[] emudirpath; return NULL; } // oh well
        appDataPath = (PWSTR)ptr;

        wcscpy(&appDataPath[pos], appdir); pos += wcslen(appdir);
        wcscpy(&appDataPath[pos], wfileName);

        f = _wfopen(appDataPath, L"rb");
        if (f) f = _wfreopen(appDataPath, fatperm, f);
        CoTaskMemFree(appDataPath);
        delete[] wfileName;
        if (f) { delete[] emudirpath; return f; }
    }

#else

    if (!relpath)
    {
        // Now check XDG_CONFIG_HOME
        // TODO: check for memory leak there
        std::string fullpath = std::string(g_get_user_config_dir()) + "/melonds/" + path;
        f = OpenFile(fullpath.c_str(), mode, true);
        if (f) { delete[] emudirpath; return f; }
    }

#endif

    if (mode[0] != 'r')
    {
        f = OpenFile(emudirpath, mode);
        if (f) { delete[] emudirpath; return f; }
    }

    delete[] emudirpath;
    return NULL;
}


void* Thread_Create(void (*func)())
{
    ThreadData* data = new ThreadData;
    data->Func = func;
    data->ID = SDL_CreateThread(ThreadEntry, "melonDS core thread", data);
    return data;
}

void Thread_Free(void* thread)
{
    delete (ThreadData*)thread;
}

void Thread_Wait(void* thread)
{
    SDL_WaitThread((SDL_Thread*)((ThreadData*)thread)->ID, NULL);
}


void* Semaphore_Create()
{
    return SDL_CreateSemaphore(0);
}

void Semaphore_Free(void* sema)
{
    SDL_DestroySemaphore((SDL_sem*)sema);
}

void Semaphore_Reset(void* sema)
{
    while (SDL_SemTryWait((SDL_sem*)sema) == 0);
}

void Semaphore_Wait(void* sema)
{
    SDL_SemWait((SDL_sem*)sema);
}

void Semaphore_Post(void* sema)
{
    SDL_SemPost((SDL_sem*)sema);
}


void* GL_GetProcAddress(const char* proc)
{
    return uiGLGetProcAddress(proc);
}


bool MP_Init()
{
    int opt_true = 1;
    int res;

#ifdef __WIN32__
    WSADATA wsadata;
    if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
    {
        return false;
    }
#endif // __WIN32__

    MPSocket = socket(AF_INET, SOCK_DGRAM, 0);
	if (MPSocket < 0)
	{
		return false;
	}

	res = setsockopt(MPSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt_true, sizeof(int));
	if (res < 0)
	{
		closesocket(MPSocket);
		MPSocket = INVALID_SOCKET;
		return false;
	}

	sockaddr_t saddr;
	saddr.sa_family = AF_INET;
	*(u32*)&saddr.sa_data[2] = htonl(Config::SocketBindAnyAddr ? INADDR_ANY : INADDR_LOOPBACK);
	*(u16*)&saddr.sa_data[0] = htons(7064);
	res = bind(MPSocket, &saddr, sizeof(sockaddr_t));
	if (res < 0)
	{
		closesocket(MPSocket);
		MPSocket = INVALID_SOCKET;
		return false;
	}

	res = setsockopt(MPSocket, SOL_SOCKET, SO_BROADCAST, (const char*)&opt_true, sizeof(int));
	if (res < 0)
	{
		closesocket(MPSocket);
		MPSocket = INVALID_SOCKET;
		return false;
	}

	MPSendAddr.sa_family = AF_INET;
	*(u32*)&MPSendAddr.sa_data[2] = htonl(INADDR_BROADCAST);
	*(u16*)&MPSendAddr.sa_data[0] = htons(7064);

	return true;
}

void MP_DeInit()
{
    if (MPSocket >= 0)
        closesocket(MPSocket);

#ifdef __WIN32__
    WSACleanup();
#endif // __WIN32__
}

int MP_SendPacket(u8* data, int len)
{
    if (MPSocket < 0)
        return 0;

    if (len > 2048-8)
    {
        printf("MP_SendPacket: error: packet too long (%d)\n", len);
        return 0;
    }

    *(u32*)&PacketBuffer[0] = htonl(0x4946494E); // NIFI
    PacketBuffer[4] = NIFI_VER;
    PacketBuffer[5] = 0;
    *(u16*)&PacketBuffer[6] = htons(len);
    memcpy(&PacketBuffer[8], data, len);

    int slen = sendto(MPSocket, (const char*)PacketBuffer, len+8, 0, &MPSendAddr, sizeof(sockaddr_t));
    if (slen < 8) return 0;
    return slen - 8;
}

int MP_RecvPacket(u8* data, bool block)
{
    if (MPSocket < 0)
        return 0;

    fd_set fd;
	struct timeval tv;

	FD_ZERO(&fd);
	FD_SET(MPSocket, &fd);
	tv.tv_sec = 0;
	tv.tv_usec = block ? 5000 : 0;

	if (!select(MPSocket+1, &fd, 0, 0, &tv))
    {
        return 0;
    }

    sockaddr_t fromAddr;
    socklen_t fromLen = sizeof(sockaddr_t);
    int rlen = recvfrom(MPSocket, (char*)PacketBuffer, 2048, 0, &fromAddr, &fromLen);
    if (rlen < 8+24)
    {
        return 0;
    }
    rlen -= 8;

    if (ntohl(*(u32*)&PacketBuffer[0]) != 0x4946494E)
    {
        return 0;
    }

    if (PacketBuffer[4] != NIFI_VER)
    {
        return 0;
    }

    if (ntohs(*(u16*)&PacketBuffer[6]) != rlen)
    {
        return 0;
    }

    memcpy(data, &PacketBuffer[8], rlen);
    return rlen;
}



bool LAN_Init()
{
    if (Config::DirectLAN)
    {
        if (!LAN_PCap::Init(true))
            return false;
    }
    else
    {
        if (!LAN_Socket::Init())
            return false;
    }

    return true;
}

void LAN_DeInit()
{
    // checkme. blarg
    //if (Config::DirectLAN)
    //    LAN_PCap::DeInit();
    //else
    //    LAN_Socket::DeInit();
    LAN_PCap::DeInit();
    LAN_Socket::DeInit();
}

int LAN_SendPacket(u8* data, int len)
{
    if (Config::DirectLAN)
        return LAN_PCap::SendPacket(data, len);
    else
        return LAN_Socket::SendPacket(data, len);
}

int LAN_RecvPacket(u8* data)
{
    if (Config::DirectLAN)
        return LAN_PCap::RecvPacket(data);
    else
        return LAN_Socket::RecvPacket(data);
}


}