/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.syntax;

import org.codehaus.groovy.GroovyBugError;

import java.io.StringWriter;
import java.io.PrintWriter;


/**
 *  An abstract base class for nodes in the concrete syntax tree that is
 *  the result of parsing.  Note that the CSTNode is inextricably linked
 *  with the Token in that every CSTNode has a Token as it's root.
 *
 *  @see antlr.Parser
 *  @see Token
 *  @see org.codehaus.groovy.syntax.Reduction
 *  @see org.codehaus.groovy.syntax.Types
 *
 *  @author <a href="mailto:bob@werken.com">bob mcwhirter</a>
 *  @author <a href="mailto:cpoirier@dreaming.org">Chris Poirier</a>
 */

public abstract class CSTNode
{

  //---------------------------------------------------------------------------
  // NODE IDENTIFICATION AND MEANING


   /**
    *  Returns the meaning of this node.  If the node isEmpty(), returns
    *  the type of Token.NULL.
    */

    public int getMeaning()
    {
        return getRoot( true ).getMeaning();
    }



   /**
    *  Sets the meaning for this node (and it's root Token).  Not
    *  valid if the node isEmpty().  Returns the node, for convenience.
    */

    public CSTNode setMeaning( int meaning )
    {
        getRoot().setMeaning( meaning );
        return this;
    }



   /**
    *  Returns the actual type of the node.  If the node isEmpty(), returns
    *  the type of Token.NULL.
    */

    public int getType()
    {
        return getRoot( true ).getType();
    }



   /**
    *  Returns true if the node can be coerced to the specified type.
    */

    public boolean canMean( int type )
    {
        return Types.canMean( getMeaning(), type );
    }



   /**
    *  Returns true if the node's meaning matches the specified type.
    */

    public boolean isA( int type )
    {
        return Types.ofType( getMeaning(), type );
    }



   /**
    *  Returns true if the node's meaning matches any of the specified types.
    */

    public boolean isOneOf( int[] types )
    {
        int meaning = getMeaning();
        for( int i = 0; i < types.length; i++ )
        {
            if( Types.ofType(meaning, types[i]) )
            {
                return true;
            }
        }

        return false;
    }



   /**
    *  Returns true if the node's meaning matches all of the specified types.
    */

    public boolean isAllOf( int[] types )
    {
        int meaning = getMeaning();
        for( int i = 0; i < types.length; i++ )
        {
            if( !Types.ofType(meaning, types[i]) )
            {
                return false;
            }
        }

        return true;
    }



   /**
    *  Returns the first matching meaning of the specified types.
    *  Returns Types.UNKNOWN if there are no matches.
    */

    public int getMeaningAs( int[] types )
    {

        for( int i = 0; i < types.length; i++ )
        {
            if( isA(types[i]) )
            {
                return types[i];
            }
        }

        return Types.UNKNOWN;
    }




  //---------------------------------------------------------------------------
  // TYPE SUGAR


   /**
    *  Returns true if the node matches the specified type.  Effectively
    *  a synonym for <code>isA()</code>.  Missing nodes are Token.NULL.
    */

    boolean matches( int type )
    {
        return isA(type);
    }



   /**
    *  Returns true if the node and it's first child match the specified
    *  types.  Missing nodes are Token.NULL.
    */

    boolean matches( int type, int child1 )
    {
        return isA(type) && get(1, true).isA(child1);
    }



   /**
    *  Returns true if the node and it's first and second child match the
    *  specified types.  Missing nodes are Token.NULL.
    */

    boolean matches( int type, int child1, int child2 )
    {
        return matches( type, child1 ) && get(2, true).isA(child2);
    }



   /**
    *  Returns true if the node and it's first three children match the
    *  specified types.  Missing nodes are Token.NULL.
    */

    boolean matches( int type, int child1, int child2, int child3 )
    {
        return matches( type, child1, child2 ) && get(3, true).isA(child3);
    }



   /**
    *  Returns true if the node an it's first four children match the
    *  specified types.  Missing nodes have type Types.NULL.
    */

    boolean matches( int type, int child1, int child2, int child3, int child4 )
    {
        return matches( type, child1, child2, child3 ) && get(4, true).isA(child4);
    }





  //---------------------------------------------------------------------------
  // MEMBER ACCESS


   /**
    *  Returns true if the node is completely empty (no root, even).
    */

    public boolean isEmpty()
    {
        return false;
    }



   /**
    *  Returns the number of elements in the node (including root).
    */

    public abstract int size();



   /**
    *  Returns true if the node has any non-root elements.
    */

    public boolean hasChildren()
    {
        return children() > 0;
    }



   /**
    *  Returns the number of non-root elements in the node.
    */

    public int children()
    {
        int size = size();
        if( size > 1 )
        {
            return size - 1;
        }
        return 0;
    }



   /**
    *  Returns the specified element, or null.
    */

    public abstract CSTNode get( int index );



   /**
    *  Returns the specified element, or Token.NULL if
    *  safe is set and the specified element is null (or doesn't
    *  exist).
    */

    public CSTNode get( int index, boolean safe )
    {
        CSTNode element = get( index );

        if( element == null && safe )
        {
            element = Token.NULL;
        }

        return element;
    }



   /**
    *  Returns the root of the node.  By convention, all nodes have
    *  a Token as the first element (or root), which indicates the type
    *  of the node.  May return null if the node <code>isEmpty()</code>.
    */

    public abstract Token getRoot();



   /**
    *  Returns the root of the node, the Token that indicates it's
    *  type.  Returns a Token.NULL if safe and the actual root is null.
    */

    public Token getRoot( boolean safe )
    {
        Token root = getRoot();

        if( root == null && safe )
        {
            root = Token.NULL;
        }

        return root;
    }



   /**
    *  Returns the text of the root.  Uses <code>getRoot(true)</code>
    *  to get the root, so you will only receive null in return if the
    *  root token returns it.
    */

    public String getRootText()
    {
        Token root = getRoot( true );
        return root.getText();
    }



   /**
    *  Returns a description of the node.
    */

    public String getDescription()
    {
        return Types.getDescription( getMeaning() );
    }



   /**
    *  Returns the starting line of the node.  Returns -1
    *  if not known.
    */

    public int getStartLine()
    {
        return getRoot(true).getStartLine();
    }



   /**
    *  Returns the starting column of the node.  Returns -1
    *  if not known.
    */

    public int getStartColumn()
    {
        return getRoot(true).getStartColumn();
    }



   /**
    *  Marks the node a complete expression.  Not all nodes support
    *  this operation!
    */

    public void markAsExpression()
    {
        throw new GroovyBugError( "markAsExpression() not supported for this CSTNode type" );
    }



   /**
    *  Returns true if the node is a complete expression.
    */

    public boolean isAnExpression()
    {
        return isA(Types.SIMPLE_EXPRESSION);
    }





  //---------------------------------------------------------------------------
  // OPERATIONS


   /**
    *  Adds an element to the node.  Returns the element for convenience.
    *  Not all nodes support this operation!
    */

    public CSTNode add( CSTNode element )
    {
        throw new GroovyBugError( "add() not supported for this CSTNode type" );
    }



   /**
    *  Adds all children of the specified node to this one.  Not all
    *  nodes support this operation!
    */

    public void addChildrenOf( CSTNode of )
    {
        for( int i = 1; i < of.size(); i++ )
        {
            add( of.get(i) );
        }
    }



   /**
    *  Sets an element node in at the specified index.  Returns the element
    *  for convenience.  Not all nodes support this operation!
    */

    public CSTNode set( int index, CSTNode element )
    {
        throw new GroovyBugError( "set() not supported for this CSTNode type" );
    }



   /**
    *  Creates a <code>Reduction</code> from this node.  Returns self if the
    *  node is already a <code>Reduction</code>.
    */

    public abstract Reduction asReduction();




  //---------------------------------------------------------------------------
  // STRING CONVERSION


   /**
    *  Formats the node as a <code>String</code> and returns it.
    */

    public String toString()
    {
        StringWriter string = new StringWriter();
        write( new PrintWriter(string) );

        string.flush();
        return string.toString();
    }


   /**
    *  Formats the node and writes it to the specified <code>Writer</code>.
    */

    public void write( PrintWriter writer )
    {
        write( writer, "" );
    }


   /**
    *  Formats the node and writes it to the specified <code>Writer</code>.
    *  The indent is prepended to each output line, and is increased for each
    *  recursion.
    */

    protected void write( PrintWriter writer, String indent )
    {
        writer.print( "(" );

        if( !isEmpty() )
        {
            Token  root = getRoot( true );
            int    type = root.getType();
            int meaning = root.getMeaning();


            //
            // Display our type, text, and (optional) meaning

            writer.print( Types.getDescription(type) );

            if( meaning != type )
            {
                writer.print( " as " );
                writer.print( Types.getDescription(meaning) );
            }

            if( getStartLine() > -1 )
            {
                writer.print( " at " + getStartLine() + ":" + getStartColumn() );
            }

            String text = root.getText();
            int  length = text.length();
            if( length > 0 )
            {
                writer.print( ": " );
                if( length > 40 )
                {
                   text = text.substring( 0, 17 ) + "..." + text.substring( length - 17, length );
                }

                writer.print( " \"" );
                writer.print( text );
                writer.print( "\" " );
            }
            else if( children() > 0 )
            {
                writer.print( ": " );
            }



            //
            // Recurse to display the children.

            int count = size();
            if( count > 1 )
            {
                writer.println( "" );

                String indent1 = indent + "  ";
                String indent2 = indent + "   ";
                for( int i = 1; i < count; i++ )
                {
                    writer.print( indent1 );
                    writer.print( i );
                    writer.print( ": " );

                    get( i, true ).write( writer, indent2 );
                }

                writer.print( indent );
            }
        }

        if( indent.length() > 0 )
        {
            writer.println( ")" );
        }
        else
        {
            writer.print( ")" );
        }
    }
}
