//+-------------------------------------------------------------------------
//
//  Microsoft Windows
//  Copyright (C) Microsoft Corporation, 1992 - 2000.
//
//  File:       document.cxx
//
//  Contents:   The Document part of the browser
//
//--------------------------------------------------------------------------

#include <pch.cxx>
#pragma hdrstop

#define TheSearch pSearch

const int UNICODE_PARAGRAPH_SEPARATOR=0x2029;

const GUID guidStorage = PSGUID_STORAGE;

//+-------------------------------------------------------------------------
//
//  Member:     Position::Compare, public
//
//  Synopsis:   Compare two positions
//
//--------------------------------------------------------------------------

int Position::Compare( const Position& pos ) const
{
   int diff = _para - pos.Para();
   if ( diff == 0 )
      diff = _begOff - pos.BegOff();
   return diff;
}

//+-------------------------------------------------------------------------
//
//  Member:     Hit::Hit, public
//
//  Synopsis:   Create hit from an array of positions
//
//--------------------------------------------------------------------------

Hit::Hit( const Position * aPos, unsigned cPos )
: _cPos(cPos)
{
    _aPos = new Position[cPos];

    memcpy( _aPos, aPos, sizeof(Position) * cPos );
}

Hit::~Hit()
{
    delete _aPos;
}

//+-------------------------------------------------------------------------
//
//  Member:     HitIter::GetPositionCount, public
//
//  Synopsis:   return number of positions or zero
//
//--------------------------------------------------------------------------

int HitIter::GetPositionCount() const
{
    if (_iHit < _pDoc->_cHit && _pDoc->_aHit[_iHit])
        return _pDoc->_aHit[_iHit]->Count();

    return 0;
}

//+-------------------------------------------------------------------------
//
//  Member:     HitIter::GetPosition, public
//
//  Synopsis:   return position by value
//
//--------------------------------------------------------------------------

Position HitIter::GetPosition ( int i ) const
{
     if ( _iHit < _pDoc->_cHit && _pDoc->_aHit[_iHit] )
          return _pDoc->_aHit[_iHit]->GetPos(i);
     else
     {
          Position pos;
          return( pos );
     }
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::Document, public
//
//  Synopsis:   Initialize document with filename
//
//--------------------------------------------------------------------------

Document::Document(WCHAR const* filename, LONG rank, BOOL fDelete)
: _filename(0),
  _rank (rank),
  _buffer(0),
  _bufLen(0),
  _bufEnd(0),
  _pFilter(0),
  _aParaOffset(0),
  _isInit(FALSE),
  _cHit(0),
  _aParaLine(0),
  _maxParaLen(0),
  _cPara(0),
  _chunkCount(0),
  _fDelete( fDelete )
{
    _filename = new WCHAR[ wcslen( filename ) + 1 ];
    wcscpy( _filename, filename );
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::Document, public
//
//  Synopsis:   Initialize document
//
//--------------------------------------------------------------------------

Document::Document()
: _filename(0),
  _buffer(0),
  _bufLen(0),
  _bufEnd(0),
  _pFilter(0),
  _aParaOffset(0),
  _isInit(FALSE),
  _cHit(0),
  _aParaLine(0),
  _maxParaLen(0),
  _cPara(0),
  _chunkCount(0),
  _fDelete( FALSE )
{}

//+-------------------------------------------------------------------------
//
//  Member:     Document::~Document, public
//
//  Synopsis:   Free document
//
//--------------------------------------------------------------------------

Document::~Document()
{
    Free();
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::Free, public
//
//  Synopsis:   Free document storage
//
//--------------------------------------------------------------------------

void Document::Free()
{
    if ( 0 != _filename )
    {
        if ( _fDelete )
            DeleteFile( _filename );

        delete [] _filename;
    }

    if (!_isInit)
        return;

    for ( unsigned i = 0; i < _cHit; i++ )
    {
        delete _aHit[i];
        _aHit[i] = 0;
    }

    // _aHit is embedded

    delete []_aParaOffset;
    _aParaOffset = 0;

    if (_aParaLine)
    {
        for (int i = 0; i < _cPara; i++)
        {
            while (_aParaLine[i].next != 0)
            {
                ParaLine* p = _aParaLine[i].next;
                _aParaLine[i].next = _aParaLine[i].next->next;
                delete p;
            }
        }
        delete _aParaLine;
    }

    delete _buffer;

    _buffer = 0;

    _bufEnd = 0;
    _cHit = 0;

    _isInit = FALSE;
} //Free

//+-------------------------------------------------------------------------
//
//  Member:     Document::Init, public
//
//  Synopsis:   Read-in file, fill array of hits
//
//--------------------------------------------------------------------------

SCODE Document::Init(ISearchQueryHits *pSearch)
{
    BOOL noHits = FALSE;

    SCODE sc = S_OK;

    TRY
    {
        AllocBuffer( _filename );
        BindToFilter( _filename );

        ULONG ulFlags;
        sc = _pFilter->Init( IFILTER_INIT_CANON_PARAGRAPHS |
                             IFILTER_INIT_CANON_HYPHENS |
                             IFILTER_INIT_APPLY_INDEX_ATTRIBUTES,
                             0, 0, &ulFlags );

        if (FAILED (sc))
            THROW (CException(sc));

        ReadFile();

        BreakParas();

        if (Paras() != 0)
        {
            BreakLines();

#if 0
            // some filters don't behave correctly if you just re-init them,
            // so release the filter and re-open it.

            _pFilter->Release();
            _pFilter = 0;
            BindToFilter();
#endif

            sc = _pFilter->Init ( IFILTER_INIT_CANON_PARAGRAPHS |
                                  IFILTER_INIT_CANON_HYPHENS |
                                  IFILTER_INIT_APPLY_INDEX_ATTRIBUTES,
                                  0, 0, &ulFlags );
            sc = TheSearch->Init( _pFilter, ulFlags );

            if (FAILED (sc))
            {
                if ( QUERY_E_ALLNOISE != sc )
                    THROW (CException(sc));
                // we can still show the file

                sc = S_OK;
                noHits = TRUE;
            }

            // SUCCESS
            _isInit = TRUE;
        }
    }
    CATCH ( CException, e )
    {
        _isInit = FALSE;
        sc = e.GetErrorCode();
    }
    END_CATCH;

    if (!noHits)
    {
        //
        // pull up all the hits
        //

        ULONG count;
        FILTERREGION* aRegion;
        SCODE sc = TheSearch->NextHitOffset ( &count, &aRegion );

        while (sc == S_OK)
        {
            XCoMem<FILTERREGION> xRegion( aRegion );

            CDynArrayInPlace<Position> aPos( count );

            for (unsigned i = 0; i < count; i++)
                aPos [i] = RegionToPos ( aRegion [i] );

            xRegion.Free();

            XPtr<Hit> xHit( new Hit( aPos.GetPointer(), count ) );

            _aHit[_cHit] = xHit.Get();
            _cHit++;
            xHit.Acquire();

            sc = TheSearch->NextHitOffset ( &count, &aRegion );
        }
    }
    else
    {
        _cHit = 0;
        _isInit = (_bufEnd - _buffer) != 0;
    }

    if ( _pFilter )
    {
        _pFilter->Release();
        _pFilter = 0;
    }

    return _isInit ? S_OK : sc;
}

Position Document::RegionToPos ( FILTERREGION& region )
{
    static int paraHint = 0;
    static int iChunkHint = 0;
    static Position posNull;

    ULONG offset = ULONG (-1);

    // translate region to offset into buffer
    if (iChunkHint >= _chunkCount || _chunk[iChunkHint].ChunkId() != region.idChunk )
    {
        iChunkHint = 0;

        while ( iChunkHint < _chunkCount && _chunk[iChunkHint].ChunkId() < region.idChunk )
        {
            iChunkHint++;
        }

        if (iChunkHint >= _chunkCount || _chunk[iChunkHint].ChunkId() != region.idChunk)
            return posNull;
    }

    Win4Assert ( iChunkHint < _chunkCount );
    Win4Assert ( _chunk[iChunkHint].ChunkId() == region.idChunk );

    offset = _chunk[iChunkHint].Offset() + region.cwcStart;

    if (paraHint >= _cPara || _aParaOffset[paraHint] > offset )
        paraHint = 0;

    Win4Assert ( _aParaOffset[paraHint] <= offset );

    for ( ; paraHint <= _cPara; paraHint++)
    {
        // _aParaOffset[_cPara] is valid!

        if (_aParaOffset[paraHint] > offset)
        {
            Win4Assert (paraHint > 0);
            paraHint--;
            return Position ( paraHint,
                              offset - _aParaOffset[paraHint],
                              region.cwcExtent );
        }
    }

    return posNull;
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::AllocBuffer, public
//
//  Synopsis:   Allocate buffer for file text
//
//--------------------------------------------------------------------------

void Document::AllocBuffer ( WCHAR const * pwcPath )
{
    //
    //  We should keep allocating buffers on demand,
    //  but for this simple demo we'll just get the
    //  file size up front and do a single buffer
    //  allocation of 2.25 the size (to accommodate
    //  Unicode expansion). THIS IS JUST A DEMO!
    //

    HANDLE hFile = CreateFile ( pwcPath,
                               GENERIC_READ,
                               FILE_SHARE_READ,
                               0, // security
                               OPEN_EXISTING,
                               FILE_ATTRIBUTE_NORMAL,
                               0 ); // template

    if ( INVALID_HANDLE_VALUE == hFile )
        THROW( CException() );

    _bufLen = GetFileSize(hFile, 0 );
    CloseHandle ( hFile );

    // Unicode from ASCII, twice and then some

    _bufLen = 2 * _bufLen + _bufLen / 4 + 1;

    _buffer = new WCHAR [_bufLen + 1];
    _buffer[ _bufLen ] = 0;
}

typedef HRESULT (__stdcall * PFnLoadTextFilter)( WCHAR const * pwcPath,
                                                 IFilter ** ppIFilter );

PFnLoadTextFilter g_pLoadTextFilter = 0;

SCODE MyLoadTextFilter( WCHAR const *pwc, IFilter **ppFilter )
{
    if ( 0 == g_pLoadTextFilter )
    {
        g_pLoadTextFilter = (PFnLoadTextFilter) GetProcAddress( GetModuleHandle( L"query.dll" ), "LoadTextFilter" );

        if ( 0 == g_pLoadTextFilter )
            return HRESULT_FROM_WIN32( GetLastError() );
    }

    return g_pLoadTextFilter( pwc, ppFilter );
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::BindToFilter, public
//
//  Synopsis:   Bind to appropriate filter for the document
//
//--------------------------------------------------------------------------

void Document::BindToFilter( WCHAR const * pwcPath )
{
    //
    // Bind to the filter interface
    //

    SCODE sc = LoadIFilter( pwcPath, 0, (void **)&_pFilter );

    if ( FAILED(sc) )
    {
        sc = MyLoadTextFilter( pwcPath, &_pFilter );
        if ( FAILED(sc) )
            THROW( CException(sc) );
    }
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::ReadFile, public
//
//  Synopsis:   Read file into buffer using the filter
//
//--------------------------------------------------------------------------

void Document::ReadFile ()
{
    SCODE sc;
    ULONG lenSoFar = 0;
    int   cChunk = 0;
    BOOL  fSeenProp = FALSE;

    STAT_CHUNK statChunk;
    sc = _pFilter->GetChunk ( &statChunk );

    // what about all these glueing flags?
    // Take them into account at some point
    // to test more complicated chunking

    while (SUCCEEDED(sc)
          || FILTER_E_LINK_UNAVAILABLE == sc
          || FILTER_E_EMBEDDING_UNAVAILABLE == sc )
    {

        if ( SUCCEEDED( sc ) && (statChunk.flags & CHUNK_TEXT) )
        {
            // read the contents only

            if ( statChunk.attribute.guidPropSet == guidStorage &&
                 statChunk.attribute.psProperty.ulKind == PRSPEC_PROPID &&
                 statChunk.attribute.psProperty.propid == PID_STG_CONTENTS )
            {
                if ( statChunk.breakType != CHUNK_NO_BREAK )
                {
                    switch( statChunk.breakType )
                    {
                        case CHUNK_EOW:
                        case CHUNK_EOS:
                            _buffer[lenSoFar++] = L' ';
                            break;
                        case CHUNK_EOP:
                        case CHUNK_EOC:
                            _buffer[lenSoFar++] = UNICODE_PARAGRAPH_SEPARATOR;
                            break;
                    }
                }

                _chunk [cChunk].SetChunkId (statChunk.idChunk);
                Win4Assert ( cChunk == 0 || statChunk.idChunk > _chunk [cChunk - 1].ChunkId () );
                _chunk [cChunk].SetOffset (lenSoFar);
                cChunk++;

                do
                {
                    ULONG lenThis = _bufLen - lenSoFar;
                    if (lenThis == 0)
                        break;

                    sc = _pFilter->GetText( &lenThis, _buffer+lenSoFar );

                    // The buffer may be filled with zeroes.  Nice filter.

                    if ( SUCCEEDED(sc) && 0 != lenThis )
                    {
                        lenThis = __min( lenThis,
                                         wcslen( _buffer + lenSoFar ) );
                        lenSoFar += lenThis;
                    }
                }
                while (SUCCEEDED(sc));
            }
        } // if SUCCEEDED( sc )

        // next chunk, please
        sc = _pFilter->GetChunk ( &statChunk );
    }

    _bufEnd = _buffer + lenSoFar;

    Win4Assert( lenSoFar <= _bufLen );

    _chunkCount = cChunk;
}


//+-------------------------------------------------------------------------
//
//  Member:     Document::BreakParas, public
//
//  Synopsis:   Break document into paragraphs separated by line feeds
//
//--------------------------------------------------------------------------

#define PARAS 25

void Document::BreakParas()
{
    int maxParas = PARAS;
    _aParaOffset = new unsigned [ maxParas ];
    WCHAR * pCur = _buffer;
    _cPara = 0;
    _maxParaLen = 0;

    do
    {
        if ( _cPara == maxParas )
        {
            // grow array
            unsigned * tmp = new unsigned [maxParas * 2];
            for ( int n = 0; n < maxParas; n++ )
                tmp[n] = _aParaOffset[n];
            delete []_aParaOffset;
            _aParaOffset = tmp;
            maxParas *= 2;
        }
        _aParaOffset [_cPara] = (UINT)(pCur - _buffer);

        pCur = EatPara(pCur);

        _cPara++;

    } while ( pCur < _bufEnd );

    // store end of buffer offset as _aParaOffset[_cPara]

    if ( _cPara == maxParas )
    {
        // grow array
        unsigned * tmp = new unsigned [maxParas + 1];
        for ( int n = 0; n < maxParas; n++ )
            tmp[n] = _aParaOffset[n];
        delete []_aParaOffset;
        _aParaOffset = tmp;
        maxParas += 1;
    }

    _aParaOffset [_cPara] = (UINT)(pCur - _buffer - 1);
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::EatPara, private
//
//  Synopsis:   Skip till the line feed
//
//--------------------------------------------------------------------------

WCHAR * Document::EatPara( WCHAR * pCur )
{
    // search for newline or null
    int pos = 0;
    int c;

    while ( pCur < _bufEnd
            && (c = *pCur) != L'\n'
            && c != L'\r'
            && c != L'\0'
            && c != UNICODE_PARAGRAPH_SEPARATOR )
    {
        pos++;
        pCur++;
    }
    // eat newline and/or carriage return
    pCur++;
    if ( pCur < _bufEnd
         && *(pCur-1) == L'\r'
         && *pCur == L'\n' )
         pCur++;

    if ( pos > _maxParaLen )
        _maxParaLen = pos;
    return pCur;
}

int BreakLine ( WCHAR* buf, int cwcBuf, int cwcMax )
{
    if (cwcBuf <= cwcMax)
        return cwcBuf;
    Win4Assert (cwcMax > 0);
    // look backwards for whitespace
    int len = cwcMax;
    int c = buf[len-1];
    while (c != L' ' && c != L'\t')
    {
        len--;
        if (len < 1)
            break;
        c = buf[len-1];
    }
    if (len == 0)
    {
        // a single word larger than screen width
        // try scanning forward
        len = cwcMax;
        c = buf[len];
        while (c != L' ' && c != L'\t')
        {
            len++;
            if (len == cwcBuf)
                break;
            c = buf[len];
        }
    }
    return len;
}

const int MAX_LINE_LEN = 110;

void Document::BreakLines()
{
    _aParaLine = new ParaLine [_cPara];
    for (int i = 0; i < _cPara; i++)
    {
        int cwcLeft = _aParaOffset[i+1] - _aParaOffset[i];

        if (cwcLeft < MAX_LINE_LEN)
            _aParaLine[i].offEnd = cwcLeft;
        else
        {
            ParaLine* pParaLine = &_aParaLine[i];
            WCHAR* buf = _buffer + _aParaOffset[i];
            int cwcOffset = 0;

            for (;;)
            {
                int cwcLine = BreakLine ( buf + cwcOffset, cwcLeft, MAX_LINE_LEN );
                cwcOffset += cwcLine;
                pParaLine->offEnd = cwcOffset;
                cwcLeft -= cwcLine;
                if (cwcLeft == 0)
                    break;
                pParaLine->next = new ParaLine;
                pParaLine = pParaLine->next;
            };
        }
    }
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::GetLine, public
//
//  Arguments:  [nPara] -- paragraph number
//              [off] -- offset within paragraph
//              [cwc] -- in/out chars to copy / copied
//              [buf] -- target buffer
//
//  Synopsis:   Copy text from paragraph to buffer
//
//--------------------------------------------------------------------------


BOOL Document::GetLine(int nPara, int off, int& cwc, WCHAR* buf)
{
    Win4Assert (_buffer != 0);
    if (nPara >= _cPara)
        return FALSE;

    const WCHAR * pText = _buffer + _aParaOffset[nPara] + off;

    // _aParaOffset [_cPara] is the offset of the end of buffer
    int cwcPara = _aParaOffset[nPara+1] - (_aParaOffset[nPara] + off);

    cwc = __min ( cwc, cwcPara );
    memcpy ( buf, pText, cwc * sizeof(WCHAR));
    return TRUE;
}

//+-------------------------------------------------------------------------
//
//  Member:     Document::GetWord, public
//
//  Synopsis:
//  Copy the string into buffer
//
//--------------------------------------------------------------------------

void Document::GetWord(int nPara, int offSrc, int cwcSrc, WCHAR* buf)
{
    Win4Assert (_buffer != 0);
    Win4Assert ( nPara < _cPara );

    WCHAR * p = _buffer + _aParaOffset[nPara];

    Win4Assert ( p + offSrc + cwcSrc <= _bufEnd );

    memcpy ( buf, p + offSrc, cwcSrc * sizeof(WCHAR));
}