//////////////////////////////////////////////////////////////////////////////
// Program Name: upnpcds.cpp
// Created     : Oct. 24, 2005
//
// Purpose     : uPnp Content Directory Service
//                                                                            
// Copyright (c) 2005 David Blain <mythtv@theblains.net>
//                                          
// This library 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 Foundation; either
// version 2.1 of the License, or at your option any later version of the LGPL.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library.  If not, see <http://www.gnu.org/licenses/>.
//
//////////////////////////////////////////////////////////////////////////////

#include <cmath>

#include "upnp.h"
#include "upnpcds.h"
#include "upnputil.h"

#define DIDL_LITE_BEGIN "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">"
#define DIDL_LITE_END   "</DIDL-Lite>";

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDSExtensionResults::Add( CDSObject *pObject )
{
    if (pObject)
        m_List.append( pObject );
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

QString UPnpCDSExtensionResults::GetResultXML(FilterMap &filter)
{
    QString sXML;

    CDSObjects::const_iterator it = m_List.begin();
    for (; it != m_List.end(); ++it)
        sXML += (*it)->toXml(filter);    

    return sXML;
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDS::UPnpCDS( UPnpDevice *pDevice, const QString &sSharePath )
  : Eventing( "UPnpCDS", "CDS_Event", sSharePath )
{
    m_root.m_eType      = OT_Container;
    m_root.m_sId        = "0";
    m_root.m_sParentId  = "-1";
    m_root.m_sTitle     = "MythTv";
    m_root.m_sClass     = "object.container";
    m_root.m_bRestricted= true;
    m_root.m_bSearchable= true;

    AddVariable( new StateVariable< QString        >( "TransferIDs"       , true ) );
    AddVariable( new StateVariable< QString        >( "ContainerUpdateIDs", true ) );
    AddVariable( new StateVariable< unsigned short >( "SystemUpdateID"    , true ) );

    SetValue< unsigned short >( "SystemUpdateID", 1 );

    QString sUPnpDescPath = UPnp::g_pConfig->GetValue( "UPnP/DescXmlPath", sSharePath );

    m_sServiceDescFileName = sUPnpDescPath + "CDS_scpd.xml";
    m_sControlUrl          = "/CDS_Control";


    // Add our Service Definition to the device.

    RegisterService( pDevice );
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDS::~UPnpCDS()
{
    while (!m_extensions.empty())
    {
        delete m_extensions.back();
        m_extensions.pop_back();
    }
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSMethod UPnpCDS::GetMethod( const QString &sURI )
{
    if (sURI == "GetServDesc"           ) return CDSM_GetServiceDescription;
    if (sURI == "Browse"                ) return CDSM_Browse               ;
    if (sURI == "Search"                ) return CDSM_Search               ;
    if (sURI == "GetSearchCapabilities" ) return CDSM_GetSearchCapabilities;
    if (sURI == "GetSortCapabilities"   ) return CDSM_GetSortCapabilities  ;
    if (sURI == "GetSystemUpdateID"     ) return CDSM_GetSystemUpdateID    ;

    return(  CDSM_Unknown );
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSBrowseFlag UPnpCDS::GetBrowseFlag( const QString &sFlag )
{
    if (sFlag == "BrowseMetadata"       ) return( CDS_BrowseMetadata        );
    if (sFlag == "BrowseDirectChildren" ) return( CDS_BrowseDirectChildren  );

    return( CDS_BrowseUnknown );
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDS::RegisterExtension  ( UPnpCDSExtension *pExtension )
{
    if (pExtension != NULL )
        m_extensions.append( pExtension );
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDS::UnregisterExtension( UPnpCDSExtension *pExtension )
{
    if (pExtension)
    {
        delete pExtension;
        m_extensions.removeAll(pExtension);
    }
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

bool UPnpCDS::ProcessRequest( HttpWorkerThread *pThread, HTTPRequest *pRequest )
{
    if (pRequest)
    {
        if (Eventing::ProcessRequest( pThread, pRequest ))
            return true;

        if ( pRequest->m_sBaseUrl != m_sControlUrl )
        {
//            VERBOSE( VB_UPNP, QString("UPnpCDS::ProcessRequest - BaseUrl (%1) not ours...").arg(pRequest->m_sBaseUrl ));
            return false;
        }

        switch( GetMethod( pRequest->m_sMethod ) )
        {
            case CDSM_GetServiceDescription : pRequest->FormatFileResponse( m_sServiceDescFileName ); break;
            case CDSM_Browse                : HandleBrowse                ( pRequest ); break;
            case CDSM_Search                : HandleSearch                ( pRequest ); break;
            case CDSM_GetSearchCapabilities : HandleGetSearchCapabilities ( pRequest ); break;
            case CDSM_GetSortCapabilities   : HandleGetSortCapabilities   ( pRequest ); break;
            case CDSM_GetSystemUpdateID     : HandleGetSystemUpdateID     ( pRequest ); break;
            default:
                UPnp::FormatErrorResponse( pRequest, UPnPResult_InvalidAction );
                break;
        }       

        return true;
    }

    return false;

}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDS::HandleBrowse( HTTPRequest *pRequest )
{
    UPnpCDSExtensionResults *pResult  = NULL;
    UPnpCDSRequest           request;

    request.m_sObjectId         = pRequest->m_mapParams[ "ObjectID"      ];
    request.m_sContainerID      = pRequest->m_mapParams[ "ContainerID"   ];
    request.m_sParentId         = "0";
    request.m_eBrowseFlag       = GetBrowseFlag( pRequest->m_mapParams[ "BrowseFlag"    ] );
    request.m_sFilter           = pRequest->m_mapParams[ "Filter"        ];
    request.m_nStartingIndex    = pRequest->m_mapParams[ "StartingIndex" ].toLong();
    request.m_nRequestedCount   = pRequest->m_mapParams[ "RequestedCount"].toLong();
    request.m_sSortCriteria     = pRequest->m_mapParams[ "SortCriteria"  ];

/*
    VERBOSE(VB_UPNP,QString("UPnpCDS::ProcessRequest \n"
                            ": url            = %1 \n"
                            ": Method         = %2 \n"
                            ": ObjectId       = %3 \n"
                            ": BrowseFlag     = %4 \n"
                            ": Filter         = %5 \n"
                            ": StartingIndex  = %6 \n"
                            ": RequestedCount = %7 \n"
                            ": SortCriteria   = %8 " )
                       .arg( pRequest->m_sBaseUrl     )
                       .arg( pRequest->m_sMethod      )
                       .arg( request.m_sObjectId      )
                       .arg( request.m_eBrowseFlag    )
                       .arg( request.m_sFilter        )
                       .arg( request.m_nStartingIndex )
                       .arg( request.m_nRequestedCount)
                       .arg( request.m_sSortCriteria  ));
*/

    UPnPResultCode eErrorCode      = UPnPResult_CDS_NoSuchObject;
    QString        sErrorDesc      = "";
    short          nNumberReturned = 0;
    short          nTotalMatches   = 0;
    short          nUpdateID       = 0;
    QString        sResultXML;
    FilterMap filter =  (FilterMap) request.m_sFilter.split(',');

    VERBOSE(VB_UPNP, QString("UPnpCDS::HandleBrowse ObjectID=%1, ContainerId=%2")
		             .arg(request.m_sObjectId)
		             .arg(request.m_sContainerID));

    if (request.m_sObjectId == "0")
    {
        // ------------------------------------------------------------------
        // This is for the root object... lets handle it.
        // ------------------------------------------------------------------

        switch( request.m_eBrowseFlag )
        { 
            case CDS_BrowseMetadata:
            {
                // ----------------------------------------------------------------------
                // Return Root Object Only
                // ----------------------------------------------------------------------

                eErrorCode      = UPnPResult_Success;
                nNumberReturned = 1;
                nTotalMatches   = 1;
                nUpdateID       = m_root.m_nUpdateId;

                m_root.SetChildCount( m_extensions.count() );

                sResultXML      = m_root.toXml(filter);

                break;
            }

            case CDS_BrowseDirectChildren:
            {
                // Loop Through each extension and Build the Root Folders

                // -=>TODO: Need to handle StartingIndex & RequestedCount

                eErrorCode      = UPnPResult_Success;
                nTotalMatches   = m_extensions.count();
                nUpdateID       = m_root.m_nUpdateId;

                if (request.m_nRequestedCount == 0)
                    request.m_nRequestedCount = nTotalMatches;

                short nStart = Max( request.m_nStartingIndex, short( 0 ));
                short nCount = Min( nTotalMatches, request.m_nRequestedCount );

                UPnpCDSRequest       childRequest;

                childRequest.m_sParentId         = "0";
                childRequest.m_eBrowseFlag       = CDS_BrowseMetadata;
                childRequest.m_sFilter           = "";
                childRequest.m_nStartingIndex    = 0;
                childRequest.m_nRequestedCount   = 1;
                childRequest.m_sSortCriteria     = "";

                for (uint i = nStart;
                     (i < (uint)m_extensions.size()) &&
                         (nNumberReturned < nCount);
                     i++)
                {
                    UPnpCDSExtension *pExtension = m_extensions[i];
                    childRequest.m_sObjectId = pExtension->m_sExtensionId;

                    pResult = pExtension->Browse( &childRequest );

                    if (pResult != NULL)
                    {
                        if (pResult->m_eErrorCode == UPnPResult_Success)
                        {
                            sResultXML  += pResult->GetResultXML(filter);
                            nNumberReturned ++;
                        }

                        delete pResult;
                    }
                }

                break;
            }
            default: break;
        }
    }
    else
    {
        // ------------------------------------------------------------------
        // Look for a CDS Extension that knows how to handle this ObjectID
        // ------------------------------------------------------------------

        UPnpCDSExtensionList::iterator it = m_extensions.begin();
        for (; (it != m_extensions.end()) && !pResult; ++it)
        {
            VERBOSE(VB_UPNP, QString(
                        "UPNP Browse : Searching for : %1  / ObjectID : %2")
                    .arg((*it)->m_sExtensionId).arg(request.m_sObjectId));

            pResult = (*it)->Browse(&request);
        }

        if (pResult != NULL)
        {
            eErrorCode  = pResult->m_eErrorCode;
            sErrorDesc  = pResult->m_sErrorDesc;

            if (eErrorCode == UPnPResult_Success)
            {
                nNumberReturned = pResult->m_List.count();
                nTotalMatches   = pResult->m_nTotalMatches;
                nUpdateID       = pResult->m_nUpdateID;
                sResultXML      = pResult->GetResultXML(filter);
            }

            delete pResult;
        }
    }
    
    // ----------------------------------------------------------------------
    // Output Results of Browse Method
    // ----------------------------------------------------------------------

    if (eErrorCode == UPnPResult_Success)
    {
        NameValues list;

        QString sResults = DIDL_LITE_BEGIN;
        sResults += sResultXML;
        sResults += DIDL_LITE_END;

        list.push_back(NameValue("Result",         sResults));
        list.push_back(NameValue("NumberReturned", nNumberReturned));
        list.push_back(NameValue("TotalMatches",   nTotalMatches));
        list.push_back(NameValue("UpdateID",       nUpdateID));

        pRequest->FormatActionResponse(list);
    }
    else
        UPnp::FormatErrorResponse ( pRequest, eErrorCode, sErrorDesc );

}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDS::HandleSearch( HTTPRequest *pRequest )
{
    UPnpCDSExtensionResults *pResult  = NULL;
    UPnpCDSRequest           request;

    UPnPResultCode eErrorCode      = UPnPResult_InvalidAction;
    QString       sErrorDesc      = "";
    short         nNumberReturned = 0;
    short         nTotalMatches   = 0;
    short         nUpdateID       = 0;
    QString       sResultXML;

    request.m_sObjectId         = pRequest->m_mapParams[ "ObjectID"      ];
    request.m_sContainerID      = pRequest->m_mapParams[ "ContainerID"   ];
    request.m_sFilter           = pRequest->m_mapParams[ "Filter"        ];
    request.m_nStartingIndex    = pRequest->m_mapParams[ "StartingIndex" ].toLong();
    request.m_nRequestedCount   = pRequest->m_mapParams[ "RequestedCount"].toLong();
    request.m_sSortCriteria     = pRequest->m_mapParams[ "SortCriteria"  ];
    request.m_sSearchCriteria   = pRequest->m_mapParams[ "SearchCriteria"];

    VERBOSE(VB_UPNP, QString("UPnpCDS::HandleSearch ObjectID=%1, ContainerId=%2")
                        .arg(request.m_sObjectId)
                        .arg(request.m_sContainerID));

    // ----------------------------------------------------------------------
    // Break the SearchCriteria into it's parts 
    // -=>TODO: This DOES NOT handle ('s or other complex expressions
    // ----------------------------------------------------------------------

    QRegExp  rMatch( "\\b(or|and)\\b" );
    rMatch.setCaseSensitivity(Qt::CaseInsensitive);

    request.m_sSearchList  = request.m_sSearchCriteria.split(
        rMatch, QString::SkipEmptyParts);
    request.m_sSearchClass = "object";  // Default to all objects.

    // ----------------------------------------------------------------------
    // -=>TODO: Need to process all expressions in searchCriteria... for now,
    //          Just focus on the "upnp:class derivedfrom" expression
    // ----------------------------------------------------------------------

    for ( QStringList::Iterator it  = request.m_sSearchList.begin(); 
                                it != request.m_sSearchList.end(); 
                              ++it ) 
    {
        if ((*it).contains("upnp:class derivedfrom", Qt::CaseInsensitive))
        {
            QStringList sParts = (*it).split(' ', QString::SkipEmptyParts);

            if (sParts.count() > 2)
            {
                request.m_sSearchClass = sParts[2].trimmed();
                request.m_sSearchClass.remove( '"' );

                break;
            }
        }
    }

    // ----------------------------------------------------------------------


    VERBOSE(VB_UPNP,QString("UPnpCDS::ProcessRequest \n"
                            ": url            = %1 \n"
                            ": Method         = %2 \n"
                            ": ObjectId       = %3 \n"
                            ": SearchCriteria = %4 \n"
                            ": Filter         = %5 \n"
                            ": StartingIndex  = %6 \n"
                            ": RequestedCount = %7 \n"
                            ": SortCriteria   = %8 \n"
                            ": SearchClass    = %9" )
                       .arg( pRequest->m_sBaseUrl     )
                       .arg( pRequest->m_sMethod      )
                       .arg( request.m_sObjectId      )
                       .arg( request.m_sSearchCriteria)
                       .arg( request.m_sFilter        )
                       .arg( request.m_nStartingIndex )
                       .arg( request.m_nRequestedCount)
                       .arg( request.m_sSortCriteria  )
                       .arg( request.m_sSearchClass   ));


    //bool bSearchDone = false;

    UPnpCDSExtensionList::iterator it = m_extensions.begin();
    for (; (it != m_extensions.end()) && !pResult; ++it)
        pResult = (*it)->Search(&request);

    if (pResult != NULL)
    {
        eErrorCode  = pResult->m_eErrorCode;
        sErrorDesc  = pResult->m_sErrorDesc;

        if (eErrorCode == UPnPResult_Success)
        {
            FilterMap filter =  (FilterMap) request.m_sFilter.split(',');
            nNumberReturned = pResult->m_List.count();
            nTotalMatches   = pResult->m_nTotalMatches;
            nUpdateID       = pResult->m_nUpdateID;
            sResultXML      = pResult->GetResultXML(filter);
            //bSearchDone = true;
        }

        delete pResult;
    }

    // nUpdateID       = 0;
    //VERBOSE(VB_UPNP,sResultXML);

    if (eErrorCode == UPnPResult_Success)
    {
        NameValues list;
        QString sResults = DIDL_LITE_BEGIN;
        sResults += sResultXML;
        sResults += DIDL_LITE_END;

        list.push_back(NameValue("Result",         sResults));
        list.push_back(NameValue("NumberReturned", nNumberReturned));
        list.push_back(NameValue("TotalMatches",   nTotalMatches));
        list.push_back(NameValue("UpdateID",       nUpdateID));

        pRequest->FormatActionResponse(list);
    }
    else
        UPnp::FormatErrorResponse( pRequest, eErrorCode, sErrorDesc );

}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDS::HandleGetSearchCapabilities( HTTPRequest *pRequest )
{
    NameValues list;

    VERBOSE(VB_UPNP,QString("UPnpCDS::ProcessRequest : %1 : %2")
                       .arg(pRequest->m_sBaseUrl)
                       .arg(pRequest->m_sMethod));

    // -=>TODO: Need to implement based on CDS Extension Capabilities

    list.push_back(
        NameValue("SearchCaps",
                  "dc:title,dc:creator,dc:date,upnp:class,res@size"));

    pRequest->FormatActionResponse(list);
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDS::HandleGetSortCapabilities( HTTPRequest *pRequest )
{
    NameValues list;

    VERBOSE(VB_UPNP,QString("UPnpCDS::ProcessRequest : %1 : %2")
                       .arg(pRequest->m_sBaseUrl)
                       .arg(pRequest->m_sMethod));

    // -=>TODO: Need to implement based on CDS Extension Capabilities

    list.push_back(
        NameValue("SortCaps",
                  "dc:title,dc:creator,dc:date,upnp:class,res@size"));

    pRequest->FormatActionResponse(list);
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDS::HandleGetSystemUpdateID( HTTPRequest *pRequest )
{
    NameValues list;

    VERBOSE(VB_UPNP,QString("UPnpCDS::ProcessRequest : %1 : %2")
                       .arg(pRequest->m_sBaseUrl)
                       .arg(pRequest->m_sMethod));

    unsigned short nId = GetValue< unsigned short >( "SystemUpdateID" );

    list.push_back(NameValue("Id", nId));

    pRequest->FormatActionResponse(list);
}



/////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////
//
// UPnpCDSExtension Implementation
//
/////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

bool UPnpCDSExtension::IsBrowseRequestForUs( UPnpCDSRequest *pRequest )
{
    if (!pRequest->m_sObjectId.startsWith(m_sExtensionId, Qt::CaseSensitive))
        return false;

    return true;
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSExtensionResults *UPnpCDSExtension::Browse( UPnpCDSRequest *pRequest )
{
    // -=>TODO: Need to add Filter & Sorting Support.
    // -=>TODO: Need to add Sub-Folder/Category Support!!!!!

    if (!IsBrowseRequestForUs( pRequest ))
        return( NULL );

    // ----------------------------------------------------------------------
    // Parse out request object's path
    // ----------------------------------------------------------------------

    QStringList idPath = pRequest->m_sObjectId.section('=',0,0).split(
        "/", QString::SkipEmptyParts);

    QString key = pRequest->m_sObjectId.section('=',1);

    if (idPath.count() == 0)
        return( NULL );

    // ----------------------------------------------------------------------
    // Process based on location in hierarchy
    // ----------------------------------------------------------------------

    UPnpCDSExtensionResults *pResults = new UPnpCDSExtensionResults();

    if (pResults != NULL)
    {
        if (!key.isEmpty())
            idPath.last().append(QString("=%1").arg(key)); 
        else 
        {
            if (pRequest->m_sObjectId.contains("item"))
            {
                idPath = idPath[idPath.count() - 2].split(
                    " ", QString::SkipEmptyParts);
                idPath = idPath[0].split('?', QString::SkipEmptyParts);

                if (idPath[0].startsWith(QString("Id")))
                    idPath[0] = QString("item=%1").arg(idPath[0].right(idPath[0].length() - 2));
            }
        }

        QString sLast = idPath.last();

        pRequest->m_sParentId = pRequest->m_sObjectId;

        if (sLast == m_sExtensionId         ) { return( ProcessRoot   ( pRequest, pResults, idPath )); }
        if (sLast == "0"                    ) { return( ProcessAll    ( pRequest, pResults, idPath )); }

        if (sLast.startsWith(QString("key") , Qt::CaseSensitive))
            return ProcessKey(pRequest, pResults, idPath);

        if (sLast.startsWith(QString("item"), Qt::CaseSensitive))
            return ProcessItem(pRequest, pResults, idPath);

        int nNodeIdx = sLast.toInt();

        if ((nNodeIdx > 0) && (nNodeIdx < GetRootCount()))
            return( ProcessContainer( pRequest, pResults, nNodeIdx, idPath ));

        pResults->m_eErrorCode = UPnPResult_CDS_NoSuchObject;
        pResults->m_sErrorDesc = "";
    }

    return( pResults );        
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

bool UPnpCDSExtension::IsSearchRequestForUs( UPnpCDSRequest *pRequest )
{
    if ( !m_sClass.startsWith( pRequest->m_sSearchClass ))
        return false;

    return true;
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSExtensionResults *UPnpCDSExtension::Search( UPnpCDSRequest *pRequest )
{
    // -=>TODO: Need to add Filter & Sorting Support.
    // -=>TODO: Need to add Sub-Folder/Category Support!!!!!

    QStringList sEmptyList;
    VERBOSE(VB_UPNP, QString("UPnpCDSExtension::Search : m_sClass = %1 : m_sSearchClass = %2").arg(m_sClass).arg(pRequest->m_sSearchClass));

    if ( !IsSearchRequestForUs( pRequest ))
    {
        VERBOSE( VB_UPNP, QString("UPnpCDSExtension::Search - Not For Us : m_sClass = %1 : m_sSearchClass = %2").arg(m_sClass).arg(pRequest->m_sSearchClass));
        return NULL;
    }

    UPnpCDSExtensionResults *pResults = new UPnpCDSExtensionResults();

    CreateItems( pRequest, pResults, 0, "", false );

    return pResults;
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

QString UPnpCDSExtension::RemoveToken( const QString &sToken, const QString &sStr, int num )
{
    QString sResult( "" );
    int     nPos = -1;

    for (int nIdx=0; nIdx < num; nIdx++)
    {
        if ((nPos = sStr.lastIndexOf( sToken, nPos )) == -1)
            break;
    }

    if (nPos > 0)
        sResult = sStr.left( nPos );

    return sResult;
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSExtensionResults *UPnpCDSExtension::ProcessRoot( UPnpCDSRequest          *pRequest, 
                                                        UPnpCDSExtensionResults *pResults, 
                                                        QStringList             &/*idPath*/ )
{
    pResults->m_nTotalMatches   = 0;
    pResults->m_nUpdateID       = 1;

    short nRootCount = GetRootCount();

    switch( pRequest->m_eBrowseFlag )
    { 
        case CDS_BrowseMetadata:
        {
            // --------------------------------------------------------------
            // Return Root Object Only
            // --------------------------------------------------------------

            pResults->m_nTotalMatches   = 1;
            pResults->m_nUpdateID       = 1;

            CDSObject *pRoot = CreateContainer( m_sExtensionId, m_sName, "0");

            pRoot->SetChildCount( nRootCount );

            pResults->Add( pRoot ); 

            break;
        }

        case CDS_BrowseDirectChildren:
        {
		VERBOSE(VB_UPNP, "CDS_BrowseDirectChildren");
            pResults->m_nUpdateID     = 1;
            pResults->m_nTotalMatches = nRootCount ;
            
            if ( pRequest->m_nRequestedCount == 0)
                pRequest->m_nRequestedCount = nRootCount ;

            short nStart = Max( pRequest->m_nStartingIndex, short( 0 ));
            short nEnd   = Min( nRootCount, short( nStart + pRequest->m_nRequestedCount));

            if (nStart < nRootCount)
            {
                for (short nIdx = nStart; nIdx < nEnd; nIdx++)
                {
                    UPnpCDSRootInfo *pInfo = GetRootInfo( nIdx );
                    if (pInfo != NULL)
                    {

                        QString sId = QString( "%1/%2" ).arg( pRequest->m_sObjectId   )
                                                        .arg( nIdx );

                        CDSObject *pItem = CreateContainer( sId,  
                                                            QObject::tr( pInfo->title ), 
                                                            m_sExtensionId );

                        pItem->SetChildCount( GetDistinctCount( pInfo ) );

                        pResults->Add( pItem );
                    }
                }
            }
        }

        case CDS_BrowseUnknown:
        default:
            break;
    }

    return pResults;
}


/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSExtensionResults *UPnpCDSExtension::ProcessAll ( UPnpCDSRequest          *pRequest,
                                                        UPnpCDSExtensionResults *pResults,
                                                        QStringList             &/*idPath*/ )
{
    pResults->m_nTotalMatches   = 0;
    pResults->m_nUpdateID       = 1;

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------
    
    switch( pRequest->m_eBrowseFlag )
    { 
        case CDS_BrowseMetadata:
        {
            // --------------------------------------------------------------
            // Return Container Object Only
            // --------------------------------------------------------------

            UPnpCDSRootInfo *pInfo = GetRootInfo( 0 );

            if (pInfo != NULL)
            {
                pResults->m_nTotalMatches   = 1;
                pResults->m_nUpdateID       = 1;

                CDSObject *pItem = CreateContainer( pRequest->m_sObjectId,  
                                                    QObject::tr( pInfo->title ), 
                                                    m_sExtensionId );

                pItem->SetChildCount( GetDistinctCount( pInfo ) );

                pResults->Add( pItem ); 
            }

            break;
        }

        case CDS_BrowseDirectChildren:
        {

            CreateItems( pRequest, pResults, 0, "", false );

            break;
        }

        case CDS_BrowseUnknown:
        default:
            break;

    }
    

    return pResults;
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSExtensionResults *UPnpCDSExtension::ProcessItem(
    UPnpCDSRequest          *pRequest,
    UPnpCDSExtensionResults *pResults, 
    QStringList             &idPath)
{
    pResults->m_nTotalMatches   = 0;
    pResults->m_nUpdateID       = 1;

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------
   // VERBOSE(VB_UPNP, QString("UPnpCDSExtension::ProcessItem : %1").arg(idPath
    switch( pRequest->m_eBrowseFlag )
    {
	case CDS_BrowseMetadata:
        {
            // --------------------------------------------------------------
            // Return 1 Item
            // --------------------------------------------------------------

            QStringMap  mapParams;
            QString     sParams = idPath.last().section( '?', 1, 1 );
            sParams.replace("&amp;", "&"); 

            HTTPRequest::GetParameters( sParams, mapParams );

            MSqlQuery query(MSqlQuery::InitCon());

            if (query.isConnected())                                                           
            {
                BuildItemQuery( query, mapParams );

                if (query.exec() && query.next())
                {
                    pRequest->m_sObjectId = RemoveToken( "/", pRequest->m_sObjectId, 1 );

                    AddItem( pRequest->m_sObjectId, pResults, false, query );
                    pResults->m_nTotalMatches = 1;
                }
            }
            break;
        }
        case CDS_BrowseDirectChildren:
        {
            // Items don't have any children.
            break;
        }

    }

    return pResults;
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSExtensionResults *UPnpCDSExtension::ProcessKey( UPnpCDSRequest          *pRequest,
                                                       UPnpCDSExtensionResults *pResults, 
                                                       QStringList             &idPath )
{
    pResults->m_nTotalMatches   = 0;
    pResults->m_nUpdateID       = 1;

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------
    
    QString sKey = idPath.last().section( '=', 1, 1 );
    sKey = QUrl::fromPercentEncoding(sKey.toLatin1());

    if (sKey.length() > 0)
    {
        int nNodeIdx = idPath[ idPath.count() - 2 ].toInt();

        switch( pRequest->m_eBrowseFlag )
        { 

            case CDS_BrowseMetadata:
            {                              
                UPnpCDSRootInfo *pInfo = GetRootInfo( nNodeIdx );

                if (pInfo == NULL)
                    return pResults;

                pRequest->m_sParentId = RemoveToken( "/", pRequest->m_sObjectId, 1 );

                // --------------------------------------------------------------
                // Since Key is not always the title, we need to lookup title.
                // --------------------------------------------------------------

                MSqlQuery query(MSqlQuery::InitCon());

                if (query.isConnected())
                {
                    QString sSQL   = QString( pInfo->sql )
                                        .arg( pInfo->where );

                    // -=>TODO: There is a problem when called for an Item, instead of a container
                    //          sKey = '<KeyName>/item?ChanId' which is incorrect.


                    query.prepare  ( sSQL );
                    query.bindValue( ":KEY", sKey );

                    if (query.exec() && query.next())
                    {
                        // ----------------------------------------------
                        // Return Container Object Only
                        // ----------------------------------------------

                        pResults->m_nTotalMatches   = 1;
                        pResults->m_nUpdateID       = 1;

                        CDSObject *pItem = CreateContainer( pRequest->m_sObjectId,
                                                            query.value(1).toString(),
                                                            pRequest->m_sParentId );

                        pItem->SetChildCount( GetDistinctCount( pInfo ));

                        pResults->Add( pItem );
                    }
                }
                break;
            }

            case CDS_BrowseDirectChildren:
            {
		
                CreateItems( pRequest, pResults, nNodeIdx, sKey, true );

                break;
            }

            case CDS_BrowseUnknown:
                default:
                break;
        }
    }

    return pResults;                                                                           
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

UPnpCDSExtensionResults *UPnpCDSExtension::ProcessContainer( UPnpCDSRequest          *pRequest,
                                                             UPnpCDSExtensionResults *pResults,
                                                             int                      nNodeIdx,
                                                             QStringList             &/*idPath*/ )

{
    pResults->m_nUpdateID     = 1;
    pResults->m_nTotalMatches = 0;

    UPnpCDSRootInfo *pInfo = GetRootInfo( nNodeIdx );

    if (pInfo == NULL)
        return pResults;

    switch( pRequest->m_eBrowseFlag )
    { 
        case CDS_BrowseMetadata:
        {
            // --------------------------------------------------------------
            // Return Container Object Only
            // --------------------------------------------------------------

            pResults->m_nTotalMatches   = 1;
            pResults->m_nUpdateID       = 1;

            CDSObject *pItem = CreateContainer( pRequest->m_sObjectId,  
                                                QObject::tr( pInfo->title ), 
                                                m_sExtensionId );

            pItem->SetChildCount( GetDistinctCount( pInfo ));

            pResults->Add( pItem ); 

            break;
        }

        case CDS_BrowseDirectChildren:
        {
            pResults->m_nTotalMatches = GetDistinctCount( pInfo );
            pResults->m_nUpdateID     = 1;

            if (pRequest->m_nRequestedCount == 0) 
                pRequest->m_nRequestedCount = SHRT_MAX;

            MSqlQuery query(MSqlQuery::InitCon());

            if (query.isConnected())
            {
                // Remove where clause placeholder.

                QString sSQL = pInfo->sql;

                sSQL.remove( "%1" );

                sSQL += QString( " LIMIT %2, %3" )
                           .arg( pRequest->m_nStartingIndex  )
                           .arg( pRequest->m_nRequestedCount );

                query.prepare( sSQL );

                if (query.exec())
                {

                    while(query.next())
                    {
                        QString sKey   = query.value(0).toString();
                        QString sTitle = query.value(1).toString();
                        long    nCount = query.value(2).toInt();

                        if (sTitle.length() == 0)
                            sTitle = "(undefined)";

                        QString sId = QString( "%1/key=%2" )
                                         .arg( pRequest->m_sParentId )
                                         .arg( sKey );

                        CDSObject *pRoot = CreateContainer( sId, sTitle, pRequest->m_sParentId );

                        pRoot->SetChildCount( nCount );

                        pResults->Add( pRoot ); 
                    }
                }
            }

            break;
        }

        case CDS_BrowseUnknown:
            break;

    }

    return pResults;
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

int UPnpCDSExtension::GetDistinctCount( UPnpCDSRootInfo *pInfo )
{
    int nCount = 0;

    if ((pInfo == NULL) || (pInfo->column == NULL))
        return 0;

    MSqlQuery query(MSqlQuery::InitCon());

    if (query.isConnected())
    {
        // Note: Tried to use Bind, however it would not allow me to use it
        //       for column & table names

        QString sSQL;
        
        if (strncmp( pInfo->column, "*", 1) == 0)
        {
            sSQL = QString( "SELECT count( %1 ) FROM %2" )
                      .arg( pInfo->column )
                      .arg( GetTableName( pInfo->column ));
        }
        else
        {
            sSQL = QString( "SELECT count( DISTINCT %1 ) FROM %2" )
                      .arg( pInfo->column )
                      .arg( GetTableName( pInfo->column ) );
        }

        query.prepare( sSQL );

        if (query.exec() && query.next())
        {
            nCount = query.value(0).toInt();
        }
    }

    return( nCount );
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

int UPnpCDSExtension::GetCount( const QString &sColumn, const QString &sKey )
{
    int nCount = 0;

    MSqlQuery query(MSqlQuery::InitCon());

    if (query.isConnected())
    {
        QString sSQL = QString("SELECT count( %1 ) FROM %2")
                       .arg( sColumn ).arg( GetTableName( sColumn ) );

        if ( sKey.length() )
            sSQL += " WHERE " + sColumn + " = :KEY";

        query.prepare( sSQL );
        if ( sKey.length() )
            query.bindValue( ":KEY", sKey );

        if (query.exec() && query.next())
        {
            nCount = query.value(0).toInt();
        }
        VERBOSE(VB_UPNP+VB_EXTRA, "UPnpCDSExtension::GetCount() - " +
                                  sSQL + " = " + QString::number(nCount));
    }

    return( nCount );
}

/////////////////////////////////////////////////////////////////////////////
//
/////////////////////////////////////////////////////////////////////////////

void UPnpCDSExtension::CreateItems( UPnpCDSRequest          *pRequest,
                                    UPnpCDSExtensionResults *pResults,
                                    int                      nNodeIdx,
                                    const QString           &sKey, 
                                    bool                     bAddRef )
{
    pResults->m_nTotalMatches = 0;
    pResults->m_nUpdateID     = 1;

    UPnpCDSRootInfo *pInfo = GetRootInfo( nNodeIdx );

    if (pInfo == NULL)
        return;

    pResults->m_nTotalMatches = GetCount( pInfo->column, sKey );
    pResults->m_nUpdateID     = 1;

    if (pRequest->m_nRequestedCount == 0) 
        pRequest->m_nRequestedCount = SHRT_MAX;

    MSqlQuery query(MSqlQuery::InitCon());

    if (query.isConnected())
    {
        QString sWhere( "" );

        if ( sKey.length() > 0)
        {
           sWhere = QString( "WHERE %1=:KEY " )
                       .arg( pInfo->column );
        }

        QString sSQL = QString( "%1 %2 LIMIT %3, %4" )
                          .arg( GetItemListSQL( pInfo->column )  )
                          .arg( sWhere )
                          .arg( pRequest->m_nStartingIndex  )
                          .arg( pRequest->m_nRequestedCount );

        query.prepare  ( sSQL );
        if ( sKey.length() )
            query.bindValue(":KEY", sKey );

        if (query.exec())
        {
            while(query.next()) 
                AddItem( pRequest->m_sObjectId, pResults, bAddRef, query );

        }
    }
}
