/////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2018-     Equinor ASA
//
//  ResInsight 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.
//
//  ResInsight 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 at <http://www.gnu.org/licenses/gpl.html>
//  for more details.
//
/////////////////////////////////////////////////////////////////////////////////

#include "RicHoloLensSession.h"
#include "RicHoloLensSessionObserver.h"

#include "RiaLogging.h"
#include "RiaPreferences.h"
#include "RiaPreferencesSystem.h"

#include "VdeArrayDataPacket.h"
#include "VdeFileExporter.h"
#include "VdePacketDirectory.h"
#include "VdeVizDataExtractor.h"

#include "cvfAssert.h"
#include "cvfTimer.h"

#include <QDir>

#include <algorithm>

//==================================================================================================
//
//
//
//==================================================================================================

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RicHoloLensSession::RicHoloLensSession()
    : m_isSessionValid( false )
    , m_lastExtractionMetaDataSequenceNumber( -1 )
    , m_sessionObserver( nullptr )
{
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RicHoloLensSession::~RicHoloLensSession()
{
    destroySession();
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RicHoloLensSession* RicHoloLensSession::createSession( const QString&              serverUrl,
                                                       const QString&              sessionName,
                                                       const QByteArray&           sessionPinCode,
                                                       RicHoloLensSessionObserver* sessionObserver )
{
    RicHoloLensSession* newSession = new RicHoloLensSession;

    newSession->m_restClient = new RicHoloLensRestClient( serverUrl, sessionName, newSession );

    if ( RiaPreferences::current()->holoLensDisableCertificateVerification() )
    {
        RiaLogging::warning( "HoloLens: Disabling certificate verification for HTTPS connections" );
        newSession->m_restClient->dbgDisableCertificateVerification();
    }

    newSession->m_restClient->createSession( sessionPinCode );

    newSession->m_sessionObserver = sessionObserver;

    const QString dbgExportFolder = RiaPreferencesSystem::current()->holoLensExportFolder();
    if ( !dbgExportFolder.isEmpty() )
    {
        newSession->m_dbgFileExportDestinationFolder = dbgExportFolder;
        RiaLogging::info( QString( "HoloLens: Debug file export will be written to folder: %1" ).arg( dbgExportFolder ) );
    }

    return newSession;
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RicHoloLensSession* RicHoloLensSession::createDummyFileBackedSession()
{
    RicHoloLensSession* newSession = new RicHoloLensSession;

    newSession->m_isSessionValid = true;

    newSession->m_dbgFileExportDestinationFolder = RiaPreferencesSystem::current()->holoLensExportFolder();

    return newSession;
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicHoloLensSession::destroySession()
{
    if ( m_restClient )
    {
        if ( m_isSessionValid )
        {
            m_restClient->deleteSession();
        }

        m_restClient->clearResponseHandler();
        m_restClient->deleteLater();
        m_restClient = nullptr;
    }

    m_isSessionValid = false;

    m_lastExtractionMetaDataSequenceNumber = -1;
    m_lastExtractionAllReferencedPacketIdsArr.clear();
    m_packetDirectory.clear();
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RicHoloLensSession::isSessionValid() const
{
    return m_isSessionValid;
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicHoloLensSession::updateSessionDataFromView( const RimGridView& activeView )
{
    RiaLogging::info( "HoloLens: Updating visualization data" );

    // Grab the current max ID as an easy way to detect if new IDs have been added for debugging purposes
    const int dbgMaxAssignedIdBeforeExtraction = m_cachingIdFactory.lastAssignedId();

    // Note that we pass the caching ID factory on the constructor which will try and detect data payloads that
    // are equal, and will then "recycle" the array IDs for these
    VdeVizDataExtractor extractor( activeView, &m_cachingIdFactory );

    QString          modelMetaJsonStr;
    std::vector<int> allReferencedPacketIds;
    extractor.extractViewContents( &modelMetaJsonStr, &allReferencedPacketIds, &m_packetDirectory );

    // Note!
    // The packet directory should now contain all the packets that are being actively referenced.
    // We now prune out any packets that are no longer being referenced. This means we do no caching of actual packet
    // data over time and that we assume that the server will ask for data packets/arrays right after having received
    // updated meta data
    m_packetDirectory.pruneUnreferencedPackets( allReferencedPacketIds );

    m_lastExtractionMetaDataSequenceNumber++;
    m_lastExtractionAllReferencedPacketIdsArr = allReferencedPacketIds;

    if ( m_restClient )
    {
        RiaLogging::info(
            QString( "HoloLens: Sending updated meta data to sharing server (sequenceNumber=%1)" ).arg( m_lastExtractionMetaDataSequenceNumber ) );
        m_restClient->sendMetaData( m_lastExtractionMetaDataSequenceNumber, modelMetaJsonStr );
    }

    // Debug export to file
    if ( !m_dbgFileExportDestinationFolder.isEmpty() )
    {
        const QDir    outputDir( m_dbgFileExportDestinationFolder );
        const QString absOutputFolder = outputDir.absolutePath();

        if ( !outputDir.mkpath( "." ) )
        {
            RiaLogging::error( QString( "HoloLens: Could not create debug file export folder: %1" ).arg( absOutputFolder ) );
            return;
        }

        // For debugging, write only the new packets to file
        // Determine which packets are new by comparing the IDs to the max known ID before extraction
        std::vector<int> packetIdsToWrite;
        for ( int packetId : allReferencedPacketIds )
        {
            if ( packetId > dbgMaxAssignedIdBeforeExtraction )
            {
                packetIdsToWrite.push_back( packetId );
            }
        }

        // This will write all packets seen in this extraction to file
        // packetIdsToWrite = allReferencedPacketIds;

        RiaLogging::info(
            QString( "HoloLens: Doing debug export of data (%1 packets) to folder: %2" ).arg( packetIdsToWrite.size() ).arg( absOutputFolder ) );
        VdeFileExporter fileExporter( absOutputFolder );
        if ( !fileExporter.exportToFile( modelMetaJsonStr, m_packetDirectory, packetIdsToWrite ) )
        {
            RiaLogging::error( "HoloLens: Error exporting debug data to folder" );
        }

        RiaLogging::info( "HoloLens: Done exporting debug data" );
    }
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicHoloLensSession::handleSuccessfulCreateSession()
{
    RiaLogging::info( "HoloLens: Session successfully created" );
    m_isSessionValid = true;

    notifyObserver( RicHoloLensSessionObserver::CreateSessionSucceeded );
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicHoloLensSession::handleFailedCreateSession()
{
    RiaLogging::error( "HoloLens: Failed to create session" );
    m_isSessionValid = false;

    notifyObserver( RicHoloLensSessionObserver::CreateSessionFailed );
}

//--------------------------------------------------------------------------------------------------
/// Handle the server response we receive after sending new meta data
//--------------------------------------------------------------------------------------------------
void RicHoloLensSession::handleSuccessfulSendMetaData( int metaDataSequenceNumber, const QByteArray& jsonServerResponseString )
{
    cvf::Timer tim;

    RiaLogging::info( QString( "HoloLens: Processing server response (meta data sequenceNumber=%1)" ).arg( metaDataSequenceNumber ) );

    if ( m_lastExtractionMetaDataSequenceNumber != metaDataSequenceNumber )
    {
        RiaLogging::warning(
            QString( "HoloLens: Ignoring server response, the meta data sequenceNumber(%1) has been superseded" ).arg( metaDataSequenceNumber ) );
        return;
    }

    std::vector<int> arrayIdsToSend;

    // cvf::Trace::show("Raw JSON response from server: '%s'", jsonServerResponseString.data());
    QByteArray trimmedServerResponseString = jsonServerResponseString.trimmed();
    if ( trimmedServerResponseString.size() > 0 )
    {
        if ( !parseJsonIntegerArray( trimmedServerResponseString, &arrayIdsToSend ) )
        {
            RiaLogging::error( "HoloLens: Error parsing array server response with array Ids, no data will be sent to server" );
            return;
        }
    }
    else
    {
        // An empty server response means we should send all array referenced by the last sent meta data
        if ( m_lastExtractionAllReferencedPacketIdsArr.size() > 0 )
        {
            arrayIdsToSend = m_lastExtractionAllReferencedPacketIdsArr;
            RiaLogging::info( "HoloLens: Empty server response, sending all arrays referenced by last meta data" );
        }
    }

    if ( arrayIdsToSend.size() == 0 )
    {
        RiaLogging::info( "HoloLens: Nothing to do, no data requested by server" );
        return;
    }

    RiaLogging::info( QString( "HoloLens: Start sending data to server, %1 data arrays have been requested" ).arg( arrayIdsToSend.size() ) );

    size_t totalBytesSent     = 0;
    size_t totalNumArraysSent = 0;

    const bool sendAsIndividualPackets = false;

    // Sending data packets one by one
    if ( sendAsIndividualPackets )
    {
        for ( size_t i = 0; i < arrayIdsToSend.size(); i++ )
        {
            const int                 arrayId = arrayIdsToSend[i];
            const VdeArrayDataPacket* packet  = m_packetDirectory.lookupPacket( arrayId );
            if ( !packet )
            {
                RiaLogging::warning( QString( "HoloLens: Could not get the requested data from cache, array id: %1 " ).arg( arrayId ) );
                continue;
            }

            QByteArray packetByteArr( packet->fullPacketRawPtr(), static_cast<int>( packet->fullPacketSize() ) );

            RiaLogging::info( QString( "HoloLens:     sending array id: %1, %2KB (%3 bytes)" )
                                  .arg( arrayId )
                                  .arg( packetByteArr.size() / 1024.0, 0, 'f', 2 )
                                  .arg( packetByteArr.size() ) );

            m_restClient->sendBinaryData( packetByteArr, "arrId" + QByteArray::number( arrayId ) );

            totalNumArraysSent++;
            totalBytesSent += packetByteArr.size();
        }
    }
    // Sending all requested arrays/packets in one combined packet
    else
    {
        QByteArray combinedPacketArr;
        if ( !m_packetDirectory.getPacketsAsCombinedBuffer( arrayIdsToSend, &combinedPacketArr ) )
        {
            RiaLogging::warning( "HoloLens: Error gathering the requested arrays, no data will be sent" );
            return;
        }

        totalNumArraysSent = arrayIdsToSend.size();
        totalBytesSent     = combinedPacketArr.size();

        RiaLogging::info( QString( "HoloLens: Sending data to server (%1 arrays combined), %2KB (%3 bytes)" )
                              .arg( totalNumArraysSent )
                              .arg( totalBytesSent / 1024.0, 0, 'f', 2 )
                              .arg( totalBytesSent ) );

        m_restClient->sendBinaryData( combinedPacketArr, "metaSeqNum" + QByteArray::number( metaDataSequenceNumber ) );
    }

    const double totalMb = totalBytesSent / ( 1024.0 * 1024.0 );
    RiaLogging::info( QString( "HoloLens: Finished sending data to server, %1 arrays, total %2MB (%3 bytes) in %4ms" )
                          .arg( totalNumArraysSent )
                          .arg( totalMb, 0, 'f', 2 )
                          .arg( totalBytesSent )
                          .arg( static_cast<int>( tim.time() * 1000 ) ) );
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RicHoloLensSession::parseJsonIntegerArray( const QByteArray& jsonString, std::vector<int>* integerArr )
{
    // cvf::Trace::show("jsonString: '%s'", jsonString.data());

    const int openBraceIdx  = jsonString.indexOf( '[' );
    const int closeBraceIdx = jsonString.lastIndexOf( ']' );
    if ( openBraceIdx < 0 || closeBraceIdx < 0 )
    {
        RiaLogging::debug( "Error parsing JSON array, could not find opening or closing braces" );
        return false;
    }

    if ( closeBraceIdx <= openBraceIdx )
    {
        RiaLogging::debug( "Error parsing JSON array, wrong placement of braces" );
        return false;
    }

    QByteArray arrayContents = jsonString.mid( openBraceIdx + 1, closeBraceIdx - openBraceIdx - 1 ).trimmed();
    // cvf::Trace::show("arrayContents: '%s'", arrayContents.data());

    QList<QByteArray> stringList = arrayContents.split( ',' );
    for ( const QByteArray& entry : stringList )
    {
        bool      convertOk = false;
        const int intVal    = entry.toInt( &convertOk );
        if ( convertOk )
        {
            integerArr->push_back( intVal );
        }
    }

    return true;
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicHoloLensSession::handleError( const QString& errMsg, const QString& url, const QString& serverData )
{
    QString fullMsg = "HoloLens communication error: " + errMsg;

    if ( !serverData.isEmpty() )
    {
        fullMsg += "\n    serverMsg: " + serverData;
    }

    fullMsg += "\n    url: " + url;

    RiaLogging::error( fullMsg );

    // It is probably not correct to always consider an error a state change, but for now
    notifyObserver( RicHoloLensSessionObserver::GeneralError );
}

//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicHoloLensSession::notifyObserver( RicHoloLensSessionObserver::Notification notification )
{
    if ( m_sessionObserver )
    {
        m_sessionObserver->handleSessionNotification( this, notification );
    }
}
