/*
 * StackFrame.java
 * 
 * Created on Oct 27, 2007, 2:57:04 PM
 */

package saxon.xslt.stacktrace;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.transform.SourceLocator;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.instruct.Template;
import net.sf.saxon.instruct.UserFunction;
import net.sf.saxon.om.Axis;
import net.sf.saxon.om.AxisIterator;
import net.sf.saxon.om.Item;
import net.sf.saxon.om.NodeInfo;
import net.sf.saxon.om.SequenceIterator;
import net.sf.saxon.om.StandardNames;
import net.sf.saxon.om.StructuredQName;
import net.sf.saxon.pattern.NameTest;
import net.sf.saxon.pattern.NodeKindTest;
import net.sf.saxon.pattern.NodeTest;
import net.sf.saxon.pattern.NodeTestPattern;
import net.sf.saxon.pattern.Pattern;
import net.sf.saxon.trace.InstructionInfo;
import net.sf.saxon.trace.Location;
import net.sf.saxon.trans.Mode;
import net.sf.saxon.trans.Rule;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.Type;
import org.xml.sax.Locator;
import org.xml.sax.helpers.LocatorImpl;

/**
 * The base class for XSLT stack frames.
 * 
 * <p>The main entry point are {@link #makeStack(XPathContext,Locator)} and
 * {@link #makeStack(XPathException)}.</p>
 *
 * @author Florent Georges
 */
public abstract class StackFrame
{
    /**
     * Get the locator of the XSLT istruction this frame stands for.
     */
    public Locator getLocator()
    {
        return myLocator;
    }

    /**
     * Get the path of the current node if any.  May be null.
     */
    public String getPath()
    {
        return myPath;
    }

    /**
     * Get the next frame in the stack (that is, the frame that <em>called</em> this one).
     */
    public StackFrame getNext()
    {
        return myNext;
    }

    /**
     * Set the next frame in the stack (that is, the frame that <em>called</em> this one).
     */
    public void setNext(StackFrame next)
    {
        myNext = next;
    }

    /**
     * Accept a visitor.
     * 
     * <p>The visitor will visit this frame, then the rest of the stack (the next
     * frame, then its next frame, etcetera).</p>
     */
    public abstract void acceptVisitor(StackVisitor visitor);

    /**
     * Make a stack representation from the XPath exception.
     * 
     * <p>Just extract the locator from the exception, then call
     * {@link #makeStack(XPathContext,Locator)}.</p>
     */
    public static StackFrame makeStack(XPathException ex)
    {
        Locator locator = jaxpToSaxLocator(ex.getLocator());
        XPathContext ctxt = ex.getXPathContext();
        return makeStack(ctxt, locator);
    }

    /**
     * Make a stack representation from the XPath context.
     * 
     * <p><b>Discussion</b>: The Saxon's XPath context <em>seems</em> to be
     * organised as following.  One context represent either a template, a
     * call-template, an apply-template or a function call (and a few other
     * like for-each, not relevant here and ignored).  On the one hand, that's
     * important for a template to have all those things in the contexts, because
     * a same template can be called or applied, and a same apply-template can
     * apply different template rules.  For a function, on the other hand, a
     * call identify clearly the function.</p>
     * 
     * <p>So you have to different types of context to identify the template or
     * function you are in: template and function call (as there is no function
     * context).  And you have three different way of knowing where you are in
     * a template or function (where you leave it to another template or
     * function, the line number that interests you): apply-templates,
     * call-template and function call.  The later are encountered first, the
     * former later.</p>
     * 
     * <p>The locaction of the error (the starting point) is again something
     * different, that the {@link TransformerException} will give you.</p>
     * 
     * <p>So the idea is the following.  We know the location to put in the
     * stack one (or more) context before the context telling the template or
     * function name, mode, arity, etc.  The function call does both things:
     * it tells the next context the line number to use, and it tell which
     * function to use with the current line number.</p>
     * 
     * <p>So we start by providing the locator, then recurse the contexts.  If
     * the context is a template, we make a new template frame with the locator,
     * then recurse with a null locator.  If the context is an apply-templates
     * or a call-template, we recurse with the locator the give us.  If the
     * context is a function call, both things are done: we use the current
     * locator to create a new function frame with the function we have, then
     * we recurse with the locator the function call gives us.</p>
     * 
     * <p>Locators are copied to not keep references to a lot of Saxon objects,
     * directly or indirectly.</p>
     */
    public static StackFrame makeStack(XPathContext ctxt, Locator locator)
    {
        return makeStack(ctxt, locator, null);
    }

    /**
     * Implementation of {@link #makeStack(XPathContext ctxt, Locator locator)}.
     * 
     * <p>To deal with template frames, we need to keep a reference to the
     * <em>called</em> frame.  Thus this method with an extra parameter.</p>
     */
    private static StackFrame makeStack(XPathContext ctxt, Locator locator, StackFrame called)
    {
        // stop recursion
        if ( ctxt == null ) {
            return null;
        }

        InstructionInfo info = ctxt.getOrigin().getInstructionInfo();
        switch ( info.getConstructType() ) {
            case StandardNames.XSL_TEMPLATE: {
                // path
                String path = makePathToCurrentNode(ctxt);
                // pattern
                Rule rule = ctxt.getCurrentTemplateRule();
                String pattern = getPatternText(rule.getPattern());
                // name
                Template t = (Template) rule.getAction();
                QName name = structuredToQName(t.getTemplateName());
                // frame & recurse
                StackFrame frame = new TemplateFrame(path, pattern, name, null, locator);
                frame.setNext(makeStack(ctxt.getCaller(), null, frame));
                return frame;
            }
            case Location.FUNCTION_CALL: {
                // path
                String path = makePathToCurrentNode(ctxt);
                // name
                QName name = structuredToQName(info.getObjectName(ctxt.getNamePool()));
                // arity
                UserFunction fun = (UserFunction) info.getProperty("target");
                int arity = fun.getNumberOfArguments();
                // frame & recurse
                StackFrame frame = new FunctionFrame(path, name, arity, locator);
                frame.setNext(makeStack(ctxt.getCaller(), new LocatorImpl(info), frame));
                return frame;
            }
            case StandardNames.XSL_CALL_TEMPLATE: {
                // Should always be, but is not...?
                if ( called instanceof TemplateFrame ) {
                    TemplateFrame frame = (TemplateFrame) called;
                    frame.setCalled(true);
                }
                return makeStack(ctxt.getCaller(), new LocatorImpl(info), called);
            }
            case StandardNames.XSL_APPLY_TEMPLATES: {
                // Should always be, but is not...?
                if ( called instanceof TemplateFrame ) {
                    TemplateFrame frame = (TemplateFrame) called;
                    // mode
                    Object pmode = info.getProperty("mode");
                    if ( pmode != null ) {
                        StructuredQName smode = ((Mode) pmode).getModeName();
                        frame.setMode(structuredToQName(smode));
                    }
                }
                // recurse with a new locator
                return makeStack(ctxt.getCaller(), new LocatorImpl(info), called);
            }
            default: {
                // just recurse the next context
                return makeStack(ctxt.getCaller(), locator, called);
            }
        }
    }

    /**
     * Internal helper, returning a representation of a QName for human readers.
     */
    protected static String displayQName(QName name)
    {
        if ( XMLConstants.DEFAULT_NS_PREFIX.equals(name.getPrefix()) ) {
            return name.getLocalPart();
        }
        else {
            return name.getPrefix() + ":" + name.getLocalPart();
        }
    }

    /**
     * Return the text view of a <em>compiled</em> pattern.
     * 
     * <p>Could be improved to be more human-friendly in some cases.</p>
     */
    private static String getPatternText(Pattern pattern)
    {
        NodeTest test = pattern.getNodeTest();
        if ( pattern instanceof NodeTestPattern && test instanceof NodeKindTest ) {
            if ( test == NodeKindTest.ATTRIBUTE ) {
                return "@*";
            }
            else if ( test == NodeKindTest.DOCUMENT ) {
                return "/";
            }
            else if ( test == NodeKindTest.ELEMENT ) {
                return "*";
            }
        }
        return pattern.toString();
    }

    /**
     * Build a new SAX {@link Locator} from a JAXP {@link SourceLocator}.
     */
    private static Locator jaxpToSaxLocator(SourceLocator jaxp)
    {
        LocatorImpl sax = new LocatorImpl();
        sax.setColumnNumber(jaxp.getColumnNumber());
        sax.setLineNumber(jaxp.getLineNumber());
        sax.setPublicId(jaxp.getPublicId());
        sax.setSystemId(jaxp.getSystemId());
        return sax;
    }

    /**
     * Build a new JAXP {@link QName} from a Saxon {@link StructuredQName}.
     */
    private static QName structuredToQName(StructuredQName sname)
    {
        if ( sname == null ) {
            return null;
        }
        String uri = sname.getNamespaceURI();
        String local = sname.getLocalName();
        String prefix = sname.getPrefix();
        return new QName(uri, local, prefix);
    }

    /**
     * Return a human-friendly view of the path to the current node if any.
     */
    private static String makePathToCurrentNode(XPathContext ctxt)
    {
        SequenceIterator it = ctxt.getCurrentIterator();
        if ( it == null ) {
            return null;
        }
        Item item = it.current();
        if ( item instanceof NodeInfo ) {
            return makePathTo((NodeInfo) item);
        }
        return null;
    }

    /**
     * Return a human-friendly view of the path to a node within its document.
     */
    private static String makePathTo(NodeInfo node)
    {
        if ( node == null ) {
            return null;
        }
        String path = null;
        switch ( node.getNodeKind() ) {
            case Type.DOCUMENT: {
                return "/";
            }
            case Type.ELEMENT: {
                String name = node.getNamePool().getDisplayName(node.getNameCode());
                AxisIterator ai = node.iterateAxis(Axis.PRECEDING, new NameTest(node));
                int pos = 1;
                while ( ai.moveNext() ) {
                    ++ pos;
                }
                path = name + "[" + pos + "]";
                break;
            }
            case Type.ATTRIBUTE: {
                String name = node.getNamePool().getDisplayName(node.getNameCode());
                path = "@" + name;
                break;
            }
            case Type.TEXT: {
                AxisIterator ai = node.iterateAxis(Axis.PRECEDING, NodeKindTest.TEXT);
                int pos = 1;
                while ( ai.moveNext() ) {
                    ++ pos;
                }
                path = "text()[" + pos + "]";
                break;
            }
            case Type.COMMENT: {
                AxisIterator ai = node.iterateAxis(Axis.PRECEDING, NodeKindTest.COMMENT);
                int pos = 1;
                while ( ai.moveNext() ) {
                    ++ pos;
                }
                path = "comment()[" + pos + "]";
                break;
            }
            case Type.PROCESSING_INSTRUCTION: {
                String name = node.getNamePool().getDisplayName(node.getNameCode());
                AxisIterator ai = node.iterateAxis(Axis.PRECEDING, new NameTest(node));
                int pos = 1;
                while ( ai.moveNext() ) {
                    ++ pos;
                }
                path = "processing-instruction(" + name + ")[" + pos + "]";
                break;
            }
            case Type.NAMESPACE: {
                int name_code = node.getNameCode();
                String name = name_code < 0 ? "" : node.getNamePool().getDisplayName(name_code);
                AxisIterator ai = node.iterateAxis(Axis.PRECEDING, new NameTest(node));
                int pos = 1;
                while ( ai.moveNext() ) {
                    ++ pos;
                }
                path = "namespace(" + name + ")[" + pos + "]";
                break;
            }
            default: {
                throw new RuntimeException("FIXME: What to do?!?");
            }
        }

        String parent = makePathTo(node.getParent());
        if ( parent == null ) {
            return path;
        }
        else if ( "/".equals(parent) ) {
            return "/" + path;
        }
        else {
            return parent + "/" + path;
        }
    }

    protected Locator myLocator;
    protected String myPath;
    private StackFrame myNext;
}
