/*
 * Copyright (c) 2002-2003 Nicolas HADACEK (hadacek@kde.org)
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.

 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#include "FilterPage.h"

#include <math.h>

#include <kglobal.h>
#include <kdebug.h>

#include "data.h"
#include "transform.h"
#include "dialog.h"

#define TIME_START(str) { \
    kdDebug(30516) << str << endl; \
    _time.restart(); \
}
#define TIME_END { kdDebug(30516) << "elapsed=" << _time.elapsed() << endl; }


namespace PDFImport
{

//-----------------------------------------------------------------------------
Page::Page(Data &data)
    : TextPage(false), _data(data), _lastStr(0), _rects(Nb_ParagraphTypes)
{
    _links.setAutoDelete(true);
}

void Page::clear()
{
    TextPage::clear();
    _lastStr = 0;
    _links.clear();
    _pars.clear();
}

void Page::beginString(GfxState *state, double x0, double y0)
{
    // This check is needed because Type 3 characters can contain
    // text-drawing operations.
    if (curStr) {
        ++nest;
        return;
    }

//    _data.checkTextFrameset();
    curStr = new String(state, x0, y0, fontSize, _data.textIndex());
//    kdDebug(30516) << "---" << endl;
}

void Page::endString()
{
//    kdDebug(30516) << "endString..." << " len=" << curStr->len
//                   << " " << _lastStr
//                   << " len=" << (_lastStr ? _lastStr->len : -1) << endl;
    TextPage::endString();
//    kdDebug(30516) << "  ...endString done" << endl;
}

void Page::addString(TextString *str)
{
//    kdDebug(30516) << "addString..." << endl;
//    if ( str->len==0 ) kdDebug(30516) << "empty string !" << endl;
    if (_lastStr) _lastStr->checkCombination(str);
    _lastStr = (str->len==0 ? 0 : static_cast<String *>(str));
//    QString s;
//    for (int i=0; i<str->len; i++) s += QChar(str->text[i]);
//    kdDebug(30516) << "string: " << s << " ("
//                   << (str->len>0 ? s[0].unicode() : 0) << ")" << endl;
    TextPage::addString(str);
//    kdDebug(30516) << " ...addString done" << endl;
}

TextBlock *Page::block(TextLine *line, int index)
{
    uint k = 0;
    if ( index<0 )
        for (TextBlock *block = line->blocks; block; block = block->next) k++;
    k += index;
    uint i = 0;
    for (TextBlock *block = line->blocks; block; block = block->next) {
        if ( i==k ) return block;
        i++;
    }
    return 0;
}

//-----------------------------------------------------------------------------
bool Page::isLastParagraphLine(TextLine *line, const Paragraph &par)
{
    // last line of page
    if ( line->next==0 ) return true;
    double dy = line->next->yMin - line->yMax;
    double ndy = line->next->yMax - line->next->yMin;
    String *str = static_cast<String *>(line->blocks->strings);
    String *nStr = static_cast<String *>(line->next->blocks->strings);
    // next line far below
    if ( dy>0.5*ndy ) return true;
    // image displayed before next line (?)
    if ( str->frameIndex()!=nStr->frameIndex() ) return true;
    if ( line->blocks==0 ) return false; // should not happen
    // if contains one or more inside tabs
    if (line->blocks->next) return true;
    if ( line->next && line->next->blocks==0 ) return false;//should not happen
    // if next line contains one or more inside tabs
    if ( line->next && line->next->blocks->next ) return true;
    TextBlock *b = block(line, -1);
    if ( b==0 || b->len==0 ) return false; // should not happen
    QChar c = QChar(b->text[b->len-1]);
    // last line char is not '.' or ':'
    if ( c!='.' && c!=':' ) return false;
    // if at line end and block aligned : same paragraph
    return ( !equal(b->xMax, par.rect().right()) );
}

void Page::createParagraphs()
{
    TextLine *first = lines;
    uint nbLines = 0;
    for (TextLine *line = lines; line; line = line->next) {
        nbLines++;
        Paragraph par(first, nbLines);
        if ( isLastParagraphLine(line, par) ) {
            _pars.push_back(par);
            nbLines = 0;
            first = line->next;
        }
    }
}

void Page::checkHeader()
{
    uint s = _pars.size();
    if ( s==0 ) return;
    Paragraph &par = _pars[0];
    if ( par.lines().count()!=1 ) return;
    const TextLine *first = par.lines().first();
    const TextLine *second = (s>1 ? _pars[1].lines().first() : 0);
    double limit = 0.2 * _data.pageRect().height();
    double delta = 2 * kMin(first->yMax - first->yMin, 12.0);
//    kdDebug(30516) << "first: " << first->yMax << " (" << limit << ")" << endl;
//    if (second) kdDebug(30516) << "second: " << second->yMin << " "
//                               << second->yMin-first->yMax << " (" << delta
//                               << ")" << endl;
    if ( first->yMax>limit ) return;
    if ( second && (second->yMin-first->yMax)<delta ) return;
    par.type = Header;
    _rects[Header] = par.rect();
}

bool Page::hasHeader() const
{
    return (_pars.size()>0 ? _pars[0].type==Header : false);
}

void Page::checkFooter()
{
    uint s = _pars.size();
    if ( s==0 ) return;
    Paragraph &par = _pars[s-1];
    if ( par.lines().count()!=1 ) return;
    const TextLine *last = par.lines().first();
    const TextLine *blast = (s>1 ? _pars[s-2].lines().last() : 0);
    double limit = 0.8 * _data.pageRect().height();
    double delta = 2 * kMin(last->yMax-last->yMin, 12.0);
//    kdDebug(30516) << "last: " << last->yMax << " (" << limit << ")" << endl;
//    if (blast) kdDebug(30516) << "blast: " << blast->yMin << " "
//                              <<  last->yMin-blast->yMax << " (" << delta
//                              << ")" << endl;
    if ( last->yMin<limit ) return;
    if ( blast && (last->yMin-blast->yMax)<delta ) return;
    par.type = Footer;
    _rects[Footer] = par.rect();
}

bool Page::hasFooter() const
{
    return (_pars.size()>0 ? _pars[_pars.size()-1].type==Footer
            : false);
}

void Page::endPage()
{
    TIME_START("coalesce strings");
    TextPage::coalesce();
    TIME_END;

    createParagraphs();

    // check header and footer
    checkHeader();
//    if ( hasHeader() ) kdDebug(30516) << "has header" << endl;
    checkFooter();
//    if ( hasFooter() ) kdDebug(30516) << "has footer" << endl;

    // compute body rect
    uint begin = (hasHeader() ? 1 : 0);
    uint end = _pars.size() - (hasFooter() ? 1 : 0);
    for (uint i=begin; i<end; i++)
        _rects[Body].unite(_pars[i].rect());
}


//-----------------------------------------------------------------------------
void Page::initParagraph(Paragraph &par) const
{
    bool rightAligned = true, centered = true, leftAligned = true;
    const double pleft = _rects[par.type].left();
    const double pright = _rects[par.type].right();
    const double pmean = (pleft + pright) / 2;

    QValueList<TextLine *>::const_iterator it;
    for (it = par.lines().begin(); it!=par.lines().end(); ++it) {

        // compute tabulations
        Tabulator tab;
        for (TextBlock *blk = (*it)->blocks; blk; blk = blk->next) {
            // if tabulated text is aligned on right edge: put a tab
            // on right edge and the tab type will be right aligned...
            double tabRightAligned = equal(blk->xMax, pright);
            double dx = (tabRightAligned ? pright : blk->xMin) - pleft;
            // #### if the tab is just at the frame edge:
            // the text is sent to next line ???
            if (tabRightAligned) dx -= 0.1;
            int res = par.findTab(dx, *it);
            if ( res==-1 ) {
                tab.pos = dx;
                if (tabRightAligned) {
                    tab.alignment = Tabulator::Right;
                    kdDebug(30516) << "tabulated text right aligned.." << endl;
                } else tab.alignment = Tabulator::Left;
                par.tabs.push_back(tab);
            }
        }
        qHeapSort2(par.tabs);

        // compute indents
        double left = (*it)->blocks->xMin - pleft;
        if ( par.isFirst(*it) ) {
            par.firstIndent = left;
            par.leftIndent = left;
        } else if ( par.isSecond(*it) ) par.leftIndent = left;
        else par.leftIndent = kMin(par.leftIndent, left);
    }

    // compute alignment
    for (it = par.lines().begin(); it!=par.lines().end(); ++it) {
        double left = (*it)->blocks->xMin;
        double right = block(*it, -1)->xMax;
        double mean = (left + right) / 2;
//        QString text;
//        for (int i=0; i<kMin(4, (*it)->blocks->len); i++)
//            text += QChar((*it)->blocks->text[i]);
//        kdDebug(30516) << text << " left=" << left
//                       << " pleft=" << pleft + par.leftIndent
//                       << " indent=" << par.leftIndent
//                       << " findent=" << par.firstIndent << endl;
        if ( centered && !equal(mean, pmean) ) centered = false;
        if ( leftAligned && (!par.isFirst(*it) || par.hasOneLine())
             && !equal(left, pleft + par.leftIndent, 0.05) ) {
            kdDebug(30516) << "not left aligned" << endl;
            leftAligned = false;
        }
        if ( rightAligned && (!par.isLast(*it) || par.hasOneLine())
             && !equal(right, pright, 0.05) ) {
            kdDebug(30516) << "not right aligned" << endl;
            rightAligned = false;
        }
    }

    // finalize alignment
    if (rightAligned) par.align = (leftAligned ? AlignBlock : AlignRight);
    else if (centered) par.align = AlignCenter;
}

void Page::fillParagraph(Paragraph &par, double &offset) const
{
    const double pleft = _rects[par.type].left();
    const double pright = _rects[par.type].right();
    par.offset = par.lines().first()->yMin - offset;
//    kdDebug(30516) << "offset=" << offset
//                   << " yMin=" << par.lines().first()->yMin
//                   << " paroffset=" << par.offset << endl;
    if ( par.offset>0 ) offset += par.offset;

    QValueList<TextLine *>::const_iterator it;
    for (it = par.lines().begin(); it!=par.lines().end(); ++it) {
        // end of previous line (inside a paragraph)
        if ( !par.isFirst(*it) ) {
            bool hyphen = false;
            if (_data.options().smart) {
                // check hyphen
                uint bi, pbi;
                int si = par.charFromEnd(0, bi);
                Q_ASSERT( si>=0 );
                QChar c = par.blocks[bi].text[si];
                int psi = par.charFromEnd(1, pbi);
                QChar prev = (psi<0 ? QChar::null : par.blocks[pbi].text[psi]);
                if ( !prev.isNull() && type(c.unicode())==Hyphen )
                    kdDebug(30516) << "hyphen ? " << QString(prev)
                                   << " type=" << type(prev.unicode())
                                   << endl;
                TextString *next =
                    ((*it)->next ? (*it)->next->blocks->strings : 0);
                if ( !prev.isNull() && type(c.unicode())==Hyphen
                     && isLetter( type(prev.unicode()) )
                     && next && next->len>0
                     && isLetter( type(next->text[next->len-1]) ) ) {
                    kdDebug(30516) << "found hyphen" << endl;
                    hyphen = true;
                    par.blocks[bi].text.remove(si, 1);
                }
            }
            if ( !hyphen ) {
                Block b;
                bool remove = _data.options().smart;
                if ( remove && par.align!=AlignBlock )
                    remove = ( par.rect().right()>0.9*pright );
                b.text = (remove ? ' ' : '\n');
                b.font = static_cast<String *>((*it)->blocks->strings)->font();
                par.blocks.push_back(b);
            }
        }

        int lineHeight = 0;
        TextBlock *prevBlk = 0;
        for (TextBlock *blk = (*it)->blocks; blk; blk = blk->next) {

            // tabulations
            double tabRightAligned = equal(blk->xMax, pright);
            double dx = (tabRightAligned ? pright : blk->xMin) - pleft;
            int res = par.findTab(dx, *it);
            if ( res>=0 ) {
                if (prevBlk) {
                    double xMax = prevBlk->xMax - pleft;
                    res = par.findNbTabs(res, xMax);
                    if ( res==0 ) continue;
                } else res++;
                // no tabs for first block in AlignCenter and AlignRight
                // if smart mode
                if ( prevBlk || !_data.options().smart
                     || (par.align!=AlignCenter && par.align!=AlignRight) ) {
                    Block b;
                    b.font = static_cast<String *>(blk->strings)->font();
                    for (uint k=0; k<(uint)res; k++) b.text += '\t';
                    par.blocks.push_back(b);
                }
            }

            // text & format
            for (TextString *str = blk->strings; str; str = str->next) {
                Block b;
                for (uint k = 0; k<uint(str->len); k++)
                    b.text += QChar(str->text[k]);
                if (str->spaceAfter) b.text += ' ';
                String *fstr = static_cast<String *>(str);
                b.font = fstr->font();
                b.link = fstr->link;
                par.blocks.push_back(b);
                lineHeight = kMax(lineHeight, b.font.height());
            }

            prevBlk = blk;
        }

        offset += lineHeight;
    }
}

FontFamily Page::checkSpecial(QChar &c, const Font &font) const
{
    Unicode res = 0;
    switch ( PDFImport::checkSpecial(c.unicode(), res) ) {
    case Bullet:
        kdDebug(30516) << "found bullet" << endl;
        // #### FIXME : if list, use a COUNTER
        // temporarly replace by symbol
        c = res;
        return Symbol;
    case SuperScript:
        kdDebug(30516) << "found superscript" << endl;
        // #### FIXME
        break;
    case LatexSpecial:
        if ( !font.isLatex() ) break;
        kdDebug(30516) << "found latex special" << endl;
        return Times;
    case SpecialSymbol:
        kdDebug(30516) << "found symbol=" << c.unicode() << endl;
        return Times;
        //return Symbol;
    default:
        break;
    }

    return Nb_Family;
}

void Page::checkSpecialChars(Paragraph &par) const
{
    QValueList<Block> blocks;
    for (uint k=0; k<par.blocks.size(); k++) {
        const Block &b = par.blocks[k];
        QString res;
//            kdDebug(30516) << "check \"" << b.text << "\"" << endl;
        for (uint l=0; l<b.text.length(); l++) {
            QChar c = b.text[l];
            FontFamily family = checkSpecial(c, b.font);
            if ( family==Nb_Family ) res += c;
            else {
                if ( !res.isEmpty() ) {
                    blocks.push_back(b);
                    blocks.back().text = res;
                    res = QString::null;
                }
                blocks.push_back(b);
                blocks.back().font.setFamily(family);
                blocks.back().text = c;
            }
        }
        if ( !res.isEmpty() ) {
            blocks.push_back(b);
            blocks.back().text = res;
        }
    }
    par.blocks = blocks;
}

void Page::coalesce(Paragraph &par) const
{
    QValueList<Block> blocks;
    blocks.push_back(par.blocks[0]);
    for (uint k=1; k<par.blocks.size(); k++) {
        const Block &b = par.blocks[k];
        if ( b.link==blocks.back().link && b.font==blocks.back().font )
            blocks.back().text += b.text;
        else blocks.push_back(b);
    }
    par.blocks = blocks;
}

void Page::prepare()
{
    TIME_START("associate links");
    for (Link *link=_links.first(); link; link=_links.next()) {
        const DRect &r = link->rect();
//        kdDebug(30516) << "link " << r.toString() << endl;
        for (TextLine *line = lines; line; line = line->next)
            for (TextBlock *blk = line->blocks; blk; blk = blk->next)
                for (TextString *str = blk->strings; str; str = str->next) {
                    String *fstr = static_cast<String *>(str);
                    DRect sr = fstr->rect();
//                    kdDebug(30516) << "str " << sr.toString() << " "
//                                   << r.isInside(sr) << endl;
                    if ( r.isInside(sr) ) fstr->link = link;
                }
    }
    TIME_END;

    TIME_START("init paragraphs");
    for (uint i=0; i<_pars.size(); i++) {
        initParagraph(_pars[i]);

        // special case for wide and centered one liner without tab
        if ( _pars[i].align==AlignBlock && _pars[i].hasOneLine()
             && _pars[i].tabs.size()==0
             && (_pars.size()==1
                 || (i!=0 && _pars[i-1].align==AlignCenter)
                 || ((i+1)!=_pars.size() && _pars[i+1].align==AlignCenter)) )
            _pars[i].align = AlignCenter;
    }
    TIME_END;

    TIME_START("fill paragraphs");
    uint begin = 0;
    if ( hasHeader() ) {
        double offset = _rects[Header].top();
        fillParagraph(_pars[0], offset);
        begin++;
    }
    uint end = _pars.size();
    if ( hasFooter() ) {
        double offset = _rects[Footer].top();
        end--;
        fillParagraph(_pars[end], offset);
    }
    double offset = _rects[Body].top();
    for (uint i=begin; i<end; i++)
        fillParagraph(_pars[i], offset);
    TIME_END;

    TIME_START("check for special chars");
    for (uint i=0; i<_pars.size(); i++)
        checkSpecialChars(_pars[i]);
    TIME_END;

    // this is not really required...
    TIME_START("coalesce formats");
    for (uint i=0; i<_pars.size(); i++)
        coalesce(_pars[i]);
    TIME_END;

    // if no paragraph : add an empty one
    if ( _pars.size()==0 ) {
        Block b;
        Paragraph par(0, 0);
        par.blocks.push_back(b);
        _pars.push_back(par);
    }
}

void Page::dump(const Paragraph &par)
{
    QValueVector<QDomElement> layouts;
    QValueVector<QDomElement> formats;

    // tabulations
    for (uint k=0; k<par.tabs.size(); k++) {
        QDomElement element = par.tabs[k].createElement(_data);
        layouts.push_back(element);
    }

    // indents
    if ( !_data.options().smart || par.align!=AlignCenter ) {
        QDomElement element = _data.createElement("INDENTS");
        element.setAttribute("left", par.leftIndent);
        double delta = par.firstIndent - par.leftIndent;
        if ( !equal(delta, 0) ) element.setAttribute("first", delta);
        layouts.push_back(element);
    }

    // offset before
    if ( par.offset>0 ) {
        QDomElement element = _data.createElement("OFFSETS");
        element.setAttribute("before", par.offset);
        layouts.push_back(element);
    }

    // flow
    if (_data.options().smart) {
        QString flow;
//        kdDebug(30516) << "flow=" << par.align << endl;
        switch (par.align) {
        case AlignLeft: break;
        case AlignRight: flow = "right"; break;
        case AlignCenter: flow = "center"; break;
        case AlignBlock: flow = "justify"; break;
        }
        if ( !flow.isEmpty() ) {
            QDomElement element = _data.createElement("FLOW");
            element.setAttribute("align", flow.utf8());
            layouts.push_back(element);
        }
    }

    // text and formats
    QString text;
    uint pos = 0;
    for (uint k=0; k<par.blocks.size(); k++) {
        const Block &b = par.blocks[k];
        text += (b.link ? "#" : b.text);
        uint len = (b.link ? 1 : b.text.length());
        QDomElement element = _data.createElement("FORMAT");
        QDomDocument document = _data.document();
        bool r = b.font.format(document, element, pos, len);
        if (b.link) b.link->format(document, element, pos, b.text);
        if ( r || b.link ) formats.push_back(element);
        pos += len;
    }

    _data.createParagraph(text, par.type, layouts, formats);
}

void Page::dump()
{
    prepare();

    TIME_START("dump XML");
    for (uint i=0; i<_pars.size(); i++)
        dump(_pars[i]);
    TIME_END;
}

} // namespace
