/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2001-04 The eXist Project
 *  http://exist-db.org
 *  
 *  This program 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
 *  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 Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *  
 *  $Id$
 */
package org.exist.xquery.functions.text;

import java.util.ArrayList;
import java.util.List;

import org.exist.dom.Match;
import org.exist.dom.NodeProxy;
import org.exist.dom.QName;
import org.exist.dom.TextImpl;
import org.exist.memtree.MemTreeBuilder;
import org.exist.storage.serializers.Serializer;
import org.exist.util.FastQSort;
import org.exist.util.XMLString;
import org.exist.xquery.BasicFunction;
import org.exist.xquery.Cardinality;
import org.exist.xquery.FunctionCall;
import org.exist.xquery.FunctionSignature;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.value.FunctionReference;
import org.exist.xquery.value.NodeValue;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceIterator;
import org.exist.xquery.value.SequenceType;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.Type;
import org.exist.xquery.value.ValueSequence;
import org.w3c.dom.DOMException;

public class HighlightMatches extends BasicFunction {

    public final static FunctionSignature signature = new FunctionSignature(
            new QName("highlight-matches", TextModule.NAMESPACE_URI, TextModule.PREFIX),
            "Highlight matching strings within text nodes that resulted from a fulltext search. " +
            "When searching with one of the fulltext operators or functions, eXist keeps track of " +
            "the fulltext matches within the text. Usually, the serializer will mark those matches by enclosing them " +
            "into an 'exist:match' element. One can then use an XSLT stylesheet to replace those match elements " +
            "and highlight matches to the user. However, this is not always possible, so Instead of using an XSLT " +
            "to post-process the serialized output, the " +
            "highlight-matches function provides direct access to the matching portions of the text within XQuery. " +
            "The function takes a sequence of text nodes as first argument $a and a callback function (defined with " +
            "util:function) as second parameter $b. $c may contain a sequence of additional values that will be passed " +
            "to the callback functions third parameter. Text nodes without matches will be returned as they are. However, " +
            "if the text contains a match marker, the matching character sequence is reported to the callback function, and the " +
            "result of the function call is inserted into the resulting node set where the matching sequence occurred. For example, " +
            "you can use this to mark all matching terms with a <span class=\"highlight\">abc</span>.",
            new SequenceType[]{
                    new SequenceType(Type.TEXT, Cardinality.ZERO_OR_MORE),
                    new SequenceType(Type.FUNCTION_REFERENCE, Cardinality.EXACTLY_ONE),
                    new SequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE)
            },
            new SequenceType(Type.NODE, Cardinality.ZERO_OR_MORE));
    
    private final static QName MATCH_ELEMENT = new QName("match", Serializer.EXIST_NS, "exist");
    
    public HighlightMatches(XQueryContext context) {
        super(context, signature);
    }

    public Sequence eval(Sequence[] args, Sequence contextSequence)
            throws XPathException {
        if (args[0].isEmpty())
            return Sequence.EMPTY_SEQUENCE;
        
        FunctionReference func = (FunctionReference) args[1].itemAt(0);
        FunctionCall call = func.getFunctionCall();
        
        context.pushDocumentContext();
        
        MemTreeBuilder builder = context.getDocumentBuilder();
        ValueSequence result = new ValueSequence();
        for (SequenceIterator i = args[0].iterate(); i.hasNext(); ) {
            NodeValue v = (NodeValue) i.nextItem();
            if (v.getImplementationType() == NodeValue.IN_MEMORY_NODE) {
                result.add(v);
            } else {
                NodeProxy p = (NodeProxy) v;
                processText(builder, p, result, call, args[2]);
            }
        }
        context.popDocumentContext();
        return result;
    }

    private final void processText(MemTreeBuilder builder, NodeProxy proxy, Sequence result, 
            FunctionCall callback, Sequence extraArgs) 
    throws DOMException, XPathException {
        TextImpl text = (TextImpl) proxy.getNode();
        Match match = proxy.getMatches();
        int nodeNr;
        if (match == null) {
            nodeNr = builder.characters(text.getXMLString());
            result.add(builder.getDocument().getNode(nodeNr));
        } else {
            List offsets = null;
            Match next = match;
            while (next != null) {
                if (next.getNodeId().equals(text.getNodeId())) {
                    if (offsets == null)
                        offsets = new ArrayList();
                    int freq = next.getFrequency();
                    for (int i = 0; i < freq; i++) {
                        offsets.add(next.getOffset(i));
                    }
                }
                next = next.getNextMatch();
            }
            
            if (offsets != null) {
                FastQSort.sort(offsets, 0, offsets.size() - 1);
                
                XMLString str = text.getXMLString();
                Match.Offset offset;
                int pos = 0;
                for (int i = 0; i < offsets.size(); i++) {
                    offset = (Match.Offset) offsets.get(i);
                    if (offset.getOffset() > pos) {
                        nodeNr = builder.characters(str.substring(pos, offset.getOffset() - pos));
                        result.add(builder.getDocument().getNode(nodeNr));
                    }
                    
                    Sequence params[] = { 
                            new StringValue(str.substring(offset.getOffset(), offset.getLength())),
                            proxy,
                            extraArgs
                    };
                    result.addAll(callback.evalFunction(null, null, params));
                    
                    pos = offset.getOffset() + offset.getLength();
                }
                if (pos < str.length()) {
                    nodeNr = builder.characters(str.substring(pos, str.length() - pos));
                    result.add(builder.getDocument().getNode(nodeNr));
                }
            } else {
                nodeNr = builder.characters(text.getXMLString());
                result.add(builder.getDocument().getNode(nodeNr));
            }
        }
    }
}