/*
 *
 *  Copyright (C) 2017-2022, OFFIS e.V.
 *  All rights reserved.  See COPYRIGHT file for details.
 *
 *  This software and supporting documentation were developed by
 *
 *    OFFIS e.V.
 *    R&D Division Health
 *    Escherweg 2
 *    D-26121 Oldenburg, Germany
 *
 *
 *  Module:  dcmdata
 *
 *  Author:  Jan Schlamelcher
 *
 *  Purpose: Implementing attribute matching for being used in dcmqrdb and dcmwlm etc.
 *
 */

#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */

#include "dcmtk/ofstd/ofmem.h"
#include "dcmtk/ofstd/ofdiag.h"
#include "dcmtk/dcmdata/dcmatch.h"
#include "dcmtk/dcmdata/dcvr.h"
#include "dcmtk/dcmdata/dcvrda.h"
#include "dcmtk/dcmdata/dcvrdt.h"
#include "dcmtk/dcmdata/dcvrtm.h"

class DcmAttributeMatching::WildCardMatcher
{
public:

#include DCMTK_DIAGNOSTIC_PUSH
#include DCMTK_DIAGNOSTIC_IGNORE_SHADOW
    // constructor, remembering the end of the query and candidate strings
    WildCardMatcher( const char* queryDataEnd, const char* candidateDataEnd )
    : queryDataEnd( queryDataEnd )
    , candidateDataEnd( candidateDataEnd )
    {

    }
#include DCMTK_DIAGNOSTIC_POP

    // the actual match function, taking two pointers to the beginning of
    // the query and the candidate string
    OFBool match( const char* queryData, const char* candidateData ) const
    {
        // matches all regular chars and '?' wildcard with the candidate string
        while( queryData != queryDataEnd && candidateData != candidateDataEnd && *queryData != '*' )
        {
            if( *queryData == '?' || *queryData == *candidateData )
            {
                ++queryData;
                ++candidateData;
            }
            else
            {
                return OFFalse;
            }
        }
        // if the end of the query is reached, there was no '*' wildcard
        // therefore it is either a match (if the end of the candidate was
        // also reached) or not
        if( queryData == queryDataEnd )
            return candidateData == candidateDataEnd;
        // if the current char in the query is not the '*' wildcard, the
        // values don't match, since all other chars would have been
        // matched by the previous while loop
        if( *queryData != '*' )
            return OFFalse;
        // skip all '*' wildcard characters, because even a string like "****"
        // equals the semantics of '*'. If the end of the query is reached
        // any remaining part of the candidate is a match, therefore return
        // OFTrue
        do if( ++queryData == queryDataEnd )
            return OFTrue;
        while( *queryData == '*' );
        // If this part of the code is reached, at least one non wildcard
        // character exists in the query after the previously skipped
        // wildcards. Search for a match of the remaining query characters
        // in the remaining candidate characters, by recursively calling
        // match.
        while( candidateData != candidateDataEnd )
        {
            if( !match( queryData, candidateData ) )
                ++candidateData;
            else
                return OFTrue;
        }
        // if the end of the candidate is reached, both strings don't match.
        return OFFalse;
    }

private:
    // the ends of both the query and the candidate string, will remain
    // constant per match operation
    const char* const queryDataEnd;
    const char* const candidateDataEnd;
};

DcmAttributeMatching::Range::Range( const void* const data, const size_t size, const char separator )
: first( OFreinterpret_cast( const char* const, data ) )
, firstSize( 0 )
, second( first )
, secondSize( size )
{
    while( firstSize != secondSize && separator != first[firstSize] )
        ++firstSize;
    if( firstSize != secondSize )
    {
        secondSize = secondSize - firstSize - 1;
        second = second + firstSize + 1;
    }
}

OFBool DcmAttributeMatching::Range::isRange() const
{
    return first != second;
}


OFBool DcmAttributeMatching::Range::hasOpenBeginning() const
{
    return !firstSize;
}

OFBool DcmAttributeMatching::Range::hasOpenEnd() const
{
    return !secondSize;
}

OFBool DcmAttributeMatching::singleValueMatching( const void* queryData, const size_t querySize,
                                                  const void* candidateData, const size_t candidateSize )
{
    return !querySize || ( querySize == candidateSize && !memcmp( queryData, candidateData, querySize ) );
}

OFBool DcmAttributeMatching::wildCardMatching( const void* queryData, const size_t querySize,
                                               const void* candidateData, const size_t candidateSize )
{
    return !querySize || WildCardMatcher
    (
        OFreinterpret_cast( const char*, queryData ) + querySize,
        OFreinterpret_cast( const char*, candidateData ) + candidateSize
    )
    .match
    (
        OFreinterpret_cast( const char*, queryData ),
        OFreinterpret_cast( const char*, candidateData )
    );
}

OFBool DcmAttributeMatching::checkRangeQuery( OFBool (*check)(const char*,const size_t),
                                              const void* queryData, const size_t querySize )
{
    const Range range( queryData, querySize );
    if( !range.isRange() )
        return check( range.first, range.firstSize );
    return ( range.hasOpenBeginning() || check( range.first, range.firstSize ) ) &&
        ( range.hasOpenEnd() || check( range.second, range.secondSize ) )
    ;
}

template<typename T>
OFBool DcmAttributeMatching::rangeMatchingTemplate( OFCondition (*parse)(const char*,const size_t,T&),
                                                    const Range& query, const T& candidate )
{
    T first;
    if( query.hasOpenBeginning() || parse( query.first, query.firstSize, first ).good() )
    {
        if( !query.isRange() )
            return query.firstSize && first == candidate;
        T second;
        if( query.hasOpenEnd() || parse( query.second, query.secondSize, second ).good() )
            return ( query.hasOpenBeginning() || first <= candidate )
                && ( query.hasOpenEnd() || second >= candidate );
    }
    return OFFalse;
}

template<typename T>
OFBool DcmAttributeMatching::rangeMatchingTemplate( OFCondition (*parse)(const char*,const size_t,T&),
                                                    const void* queryData, const size_t querySize,
                                                    const void* candidateData, const size_t candidateSize )
{
    if( !querySize )
        return OFTrue;
    T candidate;
    if( parse( OFreinterpret_cast( const char*, candidateData ), candidateSize, candidate ).bad() )
        return OFFalse;
    return rangeMatchingTemplate( parse, Range( queryData, querySize ), candidate );
}

OFBool DcmAttributeMatching::isDateQuery( const void* queryData, const size_t querySize )
{
    return checkRangeQuery( &DcmDate::check, queryData, querySize );
}

OFBool DcmAttributeMatching::isTimeQuery( const void* queryData, const size_t querySize )
{
    return checkRangeQuery( &DcmTime::check, queryData, querySize );
}

OFBool DcmAttributeMatching::isDateTimeQuery( const void* queryData, const size_t querySize )
{
    return checkRangeQuery( &DcmDateTime::check, queryData, querySize );
}

OFBool DcmAttributeMatching::rangeMatchingDate( const void* queryData, const size_t querySize,
                                                const void* candidateData, const size_t candidateSize )
{
    return rangeMatchingTemplate( &DcmDate::getOFDateFromString, queryData, querySize, candidateData, candidateSize );
}

OFBool DcmAttributeMatching::rangeMatchingTime( const void* queryData, const size_t querySize,
                                                const void* candidateData, const size_t candidateSize )
{
    return rangeMatchingTemplate( &DcmTime::getOFTimeFromString, queryData, querySize, candidateData, candidateSize );
}

OFBool DcmAttributeMatching::rangeMatchingDateTime( const void* queryData, const size_t querySize,
                                                    const void* candidateData, const size_t candidateSize )
{
    return rangeMatchingTemplate( &DcmDateTime::getOFDateTimeFromString, queryData, querySize, candidateData, candidateSize );
}

OFBool DcmAttributeMatching::rangeMatchingDateTime( const void* dateQueryData, const size_t dateQuerySize,
                                                    const void* timeQueryData, const size_t timeQuerySize,
                                                    const void* dateCandidateData, const size_t dateCandidateSize,
                                                    const void* timeCandidateData, const size_t timeCandidateSize )
{
    if( !dateQuerySize )
        return rangeMatchingTime( timeQueryData, timeQuerySize, timeCandidateData, timeCandidateSize );
    if( !timeQuerySize )
        return rangeMatchingDate( dateQueryData, dateQuerySize, dateCandidateData, dateCandidateSize );
    OFDateTime candidate;
    if( DcmDate::getOFDateFromString( OFreinterpret_cast( const char*, dateCandidateData ), dateCandidateSize, candidate.Date ).bad() )
        return OFFalse;
    if( timeCandidateSize && DcmTime::getOFTimeFromString( OFreinterpret_cast( const char*, timeCandidateData ), timeCandidateSize, candidate.Time ).bad() )
        return OFFalse;
    const Range dateQuery( dateQueryData, dateQuerySize );
    const Range timeQuery( timeQueryData, timeQuerySize );
    // check that both date/time ranges have the same structure
    if
    (
        ( dateQuery.isRange() != timeQuery.isRange() )                    ||
        ( dateQuery.hasOpenBeginning() && !timeQuery.hasOpenBeginning() ) ||
        ( dateQuery.hasOpenEnd() && !timeQuery.hasOpenEnd() )
    )
    {
        // fall back to individually matching them in case they don't
        return rangeMatchingTemplate( &DcmDate::getOFDateFromString, dateQuery, candidate.getDate() )
            && rangeMatchingTemplate( &DcmTime::getOFTimeFromString, timeQuery, candidate.getTime() );
    }
    OFDateTime first;
    // parse the first date/time
    if( !dateQuery.hasOpenBeginning() )
    {
        if( DcmDate::getOFDateFromString( dateQuery.first, dateQuery.firstSize, first.Date ).bad() )
            return OFFalse;
        if( !timeQuery.hasOpenBeginning() && DcmTime::getOFTimeFromString( timeQuery.first, timeQuery.firstSize, first.Time ).bad() )
            return OFFalse;
    }
    if( !dateQuery.isRange() )
        return dateQuery.firstSize && first == candidate;
    OFDateTime second;
    // parse the second date/time
    if( !dateQuery.hasOpenEnd() )
    {
        if( DcmDate::getOFDateFromString( dateQuery.second, dateQuery.secondSize, second.Date ).bad() )
            return OFFalse;
        if( !timeQuery.hasOpenEnd() && DcmTime::getOFTimeFromString( timeQuery.second, timeQuery.secondSize, second.Time ).bad() )
            return OFFalse;
    }
    // compare candidate with the date/time range
    return ( dateQuery.hasOpenBeginning() || first <= candidate )
        && ( dateQuery.hasOpenEnd() || second >= candidate );
}

OFBool DcmAttributeMatching::listOfUIDMatching( const void* queryData, const size_t querySize,
                                                const void* candidateData, const size_t candidateSize )
{
    if( !querySize )
        return OFTrue;
    const char* pQuery = OFreinterpret_cast( const char*, queryData );
    const char* const pQueryEnd = pQuery + querySize;
    const char* pCandidate = OFreinterpret_cast( const char*, candidateData );
    const char* const pCandidateEnd = pCandidate + candidateSize;
    // character wise match both strings, reset candidate pointer whenever a
    // '\\' character is encountered within a multi-valued query.
    while( pQuery != pQueryEnd )
    {
        if( pCandidate != pCandidateEnd && *pQuery == *pCandidate )
        {
            ++pQuery;
            ++pCandidate;
        }
        else
        {
            // test whether the candidate matches with the current value from the query
            if( pCandidate == pCandidateEnd && *pQuery == '\\' )
                return OFTrue;
            // mismatch, search for a '\\' char to try again with the next value from the query,
            // return OFFalse if none can be found, i.e. this was the last value.
            while( *pQuery != '\\' )
                if( ++pQuery == pQueryEnd )
                    return OFFalse;
            // skip the '\\' character
            ++pQuery;
            // reset candidate pointer to the beginning of the candidate
            pCandidate = OFreinterpret_cast( const char*, candidateData );
        }
    }
    // the query is at its end, we have a match if the candidate is also
    return pCandidate == pCandidateEnd;
}

DcmAttributeMatching::DcmAttributeMatching()
: m_pMatch( OFnullptr )
{

}

DcmAttributeMatching::DcmAttributeMatching( const DcmVR vr )
: m_pMatch( OFnullptr )
{
    switch( vr.getEVR() )
    {
    default:
        m_pMatch = &DcmAttributeMatching::singleValueMatching;
        break;

    case EVR_AE:
    case EVR_CS:
    case EVR_LO:
    case EVR_LT:
    case EVR_PN:
    case EVR_SH:
    case EVR_ST:
    case EVR_UC:
    case EVR_UR:
    case EVR_UT:
        m_pMatch = &DcmAttributeMatching::wildCardMatching;
        break;

    case EVR_DA:
        m_pMatch = &DcmAttributeMatching::rangeMatchingDate;
        break;

    case EVR_TM:
        m_pMatch = &DcmAttributeMatching::rangeMatchingTime;
        break;

    case EVR_DT:
        m_pMatch = &DcmAttributeMatching::rangeMatchingDateTime;
        break;

    case EVR_UI:
        m_pMatch = &DcmAttributeMatching::listOfUIDMatching;
        break;
    }
}

DcmAttributeMatching::operator OFBool() const
{
#include DCMTK_DIAGNOSTIC_PUSH
#include DCMTK_DIAGNOSTIC_IGNORE_VISUAL_STUDIO_PERFORMANCE_WARNING
    return m_pMatch;
#include DCMTK_DIAGNOSTIC_POP
}

OFBool DcmAttributeMatching::operator!() const
{
    return !m_pMatch;
}

OFBool DcmAttributeMatching::operator()( const void* queryData, const size_t querySize,
                                         const void* candidateData, const size_t candidateSize ) const
{
    assert( m_pMatch );
    return m_pMatch( queryData, querySize, candidateData, candidateSize );
}
