/*  PCSX2 - PS2 Emulator for PCs
 *  Copyright (C) 2002-2010  PCSX2 Dev Team
 *
 *  PCSX2 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 Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  PCSX2 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 PCSX2.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include "PrecompiledHeader.h"
#include "PageFaultSource.h"

#ifndef __WXMSW__
#include <wx/thread.h>
#endif

#include "EventSource.inl"
#include "MemsetFast.inl"

template class EventSource<IEventListener_PageFault>;

SrcType_PageFault *Source_PageFault = NULL;
Threading::Mutex PageFault_Mutex;

void pxInstallSignalHandler()
{
    if (!Source_PageFault) {
        Source_PageFault = new SrcType_PageFault();
    }

    _platform_InstallSignalHandler();

    // NOP on Win32 systems -- we use __try{} __except{} instead.
}

// --------------------------------------------------------------------------------------
//  EventListener_PageFault  (implementations)
// --------------------------------------------------------------------------------------
EventListener_PageFault::EventListener_PageFault()
{
    pxAssert(Source_PageFault);
    Source_PageFault->Add(*this);
}

EventListener_PageFault::~EventListener_PageFault()
{
    if (Source_PageFault)
        Source_PageFault->Remove(*this);
}

void SrcType_PageFault::Dispatch(const PageFaultInfo &params)
{
    m_handled = false;
    _parent::Dispatch(params);
}

void SrcType_PageFault::_DispatchRaw(ListenerIterator iter, const ListenerIterator &iend, const PageFaultInfo &evt)
{
    do {
        (*iter)->DispatchEvent(evt, m_handled);
    } while ((++iter != iend) && !m_handled);
}

// --------------------------------------------------------------------------------------
//  VirtualMemoryReserve  (implementations)
// --------------------------------------------------------------------------------------
VirtualMemoryReserve::VirtualMemoryReserve(const wxString &name, size_t size)
    : m_name(name)
{
    m_defsize = size;

    m_pages_commited = 0;
    m_pages_reserved = 0;
    m_baseptr = NULL;
    m_prot_mode = PageAccess_None();
    m_allow_writes = true;
}

VirtualMemoryReserve &VirtualMemoryReserve::SetName(const wxString &newname)
{
    m_name = newname;
    return *this;
}

VirtualMemoryReserve &VirtualMemoryReserve::SetBaseAddr(uptr newaddr)
{
    if (!pxAssertDev(!m_pages_reserved, "Invalid object state: you must release the virtual memory reserve prior to changing its base address!"))
        return *this;

    m_baseptr = (void *)newaddr;
    return *this;
}

VirtualMemoryReserve &VirtualMemoryReserve::SetPageAccessOnCommit(const PageProtectionMode &mode)
{
    m_prot_mode = mode;
    return *this;
}

// Notes:
//  * This method should be called if the object is already in an released (unreserved) state.
//    Subsequent calls will be ignored, and the existing reserve will be returned.
//
// Parameters:
//   size - size of the reserve, in bytes. (optional)
//     If not specified (or zero), then the default size specified in the constructor for the
//     object instance is used.
//
//   upper_bounds - criteria that must be met for the allocation to be valid.
//     If the OS refuses to allocate the memory below the specified address, the
//     object will fail to initialize and an exception will be thrown.
void *VirtualMemoryReserve::Reserve(size_t size, uptr base, uptr upper_bounds)
{
    if (!pxAssertDev(m_baseptr == NULL, "(VirtualMemoryReserve) Invalid object state; object has already been reserved."))
        return m_baseptr;

    if (!size)
        size = m_defsize;
    if (!size)
        return NULL;

    m_pages_reserved = (size + __pagesize - 4) / __pagesize;
    uptr reserved_bytes = m_pages_reserved * __pagesize;

    m_baseptr = (void *)HostSys::MmapReserve(base, reserved_bytes);

    if (!m_baseptr || (upper_bounds != 0 && (((uptr)m_baseptr + reserved_bytes) > upper_bounds))) {
        DevCon.Warning(L"%s: host memory @ %ls -> %ls is unavailable; attempting to map elsewhere...",
                       WX_STR(m_name), pxsPtr(base), pxsPtr(base + size));

        SafeSysMunmap(m_baseptr, reserved_bytes);

        if (base) {
            // Let's try again at an OS-picked memory area, and then hope it meets needed
            // boundschecking criteria below.
            m_baseptr = HostSys::MmapReserve(0, reserved_bytes);
        }
    }

    if ((upper_bounds != 0) && (((uptr)m_baseptr + reserved_bytes) > upper_bounds)) {
        SafeSysMunmap(m_baseptr, reserved_bytes);
        // returns null, caller should throw an exception or handle appropriately.
    }

    if (!m_baseptr)
        return NULL;

    FastFormatUnicode mbkb;
    uint mbytes = reserved_bytes / _1mb;
    if (mbytes)
        mbkb.Write("[%umb]", mbytes);
    else
        mbkb.Write("[%ukb]", reserved_bytes / 1024);

    DevCon.WriteLn(Color_Gray, L"%-32s @ %ls -> %ls %ls", WX_STR(m_name),
                   pxsPtr(m_baseptr), pxsPtr((uptr)m_baseptr + reserved_bytes), mbkb.c_str());

    return m_baseptr;
}

void VirtualMemoryReserve::ReprotectCommittedBlocks(const PageProtectionMode &newmode)
{
    if (!m_pages_commited)
        return;
    HostSys::MemProtect(m_baseptr, m_pages_commited * __pagesize, newmode);
}

// Clears all committed blocks, restoring the allocation to a reserve only.
void VirtualMemoryReserve::Reset()
{
    if (!m_pages_commited)
        return;

    ReprotectCommittedBlocks(PageAccess_None());
    HostSys::MmapResetPtr(m_baseptr, m_pages_commited * __pagesize);
    m_pages_commited = 0;
}

void VirtualMemoryReserve::Release()
{
    SafeSysMunmap(m_baseptr, m_pages_reserved * __pagesize);
}

bool VirtualMemoryReserve::Commit()
{
    if (!m_pages_reserved)
        return false;
    if (!pxAssert(!m_pages_commited))
        return true;

    m_pages_commited = m_pages_reserved;
    return HostSys::MmapCommitPtr(m_baseptr, m_pages_reserved * __pagesize, m_prot_mode);
}

void VirtualMemoryReserve::AllowModification()
{
    m_allow_writes = true;
    HostSys::MemProtect(m_baseptr, m_pages_commited * __pagesize, m_prot_mode);
}

void VirtualMemoryReserve::ForbidModification()
{
    m_allow_writes = false;
    HostSys::MemProtect(m_baseptr, m_pages_commited * __pagesize, PageProtectionMode(m_prot_mode).Write(false));
}


// If growing the array, or if shrinking the array to some point that's still *greater* than the
// committed memory range, then attempt a passive "on-the-fly" resize that maps/unmaps some portion
// of the reserve.
//
// If the above conditions are not met, or if the map/unmap fails, this method returns false.
// The caller will be responsible for manually resetting the reserve.
//
// Parameters:
//  newsize - new size of the reserved buffer, in bytes.
bool VirtualMemoryReserve::TryResize(uint newsize)
{
    uint newPages = (newsize + __pagesize - 1) / __pagesize;

    if (newPages > m_pages_reserved) {
        uint toReservePages = newPages - m_pages_reserved;
        uint toReserveBytes = toReservePages * __pagesize;

        DevCon.WriteLn(L"%-32s is being expanded by %u pages.", WX_STR(m_name), toReservePages);

        m_baseptr = (void *)HostSys::MmapReserve((uptr)GetPtrEnd(), toReserveBytes);

        if (!m_baseptr) {
            Console.Warning("%-32s could not be passively resized due to virtual memory conflict!");
            Console.Indent().Warning("(attempted to map memory @ %08p -> %08p)", m_baseptr, (uptr)m_baseptr + toReserveBytes);
        }

        DevCon.WriteLn(Color_Gray, L"%-32s @ %08p -> %08p [%umb]", WX_STR(m_name),
                       m_baseptr, (uptr)m_baseptr + toReserveBytes, toReserveBytes / _1mb);
    } else if (newPages < m_pages_reserved) {
        if (m_pages_commited > newsize)
            return false;

        uint toRemovePages = m_pages_reserved - newPages;
        uint toRemoveBytes = toRemovePages * __pagesize;

        DevCon.WriteLn(L"%-32s is being shrunk by %u pages.", WX_STR(m_name), toRemovePages);

        HostSys::MmapResetPtr(GetPtrEnd(), toRemoveBytes);

        DevCon.WriteLn(Color_Gray, L"%-32s @ %08p -> %08p [%umb]", WX_STR(m_name),
                       m_baseptr, (uptr)m_baseptr + toRemoveBytes, toRemoveBytes / _1mb);
    }

    return true;
}

// --------------------------------------------------------------------------------------
//  PageProtectionMode  (implementations)
// --------------------------------------------------------------------------------------
wxString PageProtectionMode::ToString() const
{
    wxString modeStr;

    if (m_read)
        modeStr += L"Read";
    if (m_write)
        modeStr += L"Write";
    if (m_exec)
        modeStr += L"Exec";

    if (modeStr.IsEmpty())
        return L"NoAccess";
    if (modeStr.Length() <= 5)
        modeStr += L"Only";

    return modeStr;
}

// --------------------------------------------------------------------------------------
//  Common HostSys implementation
// --------------------------------------------------------------------------------------
void HostSys::Munmap(void *base, size_t size)
{
    Munmap((uptr)base, size);
}
