/*
 *  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.apache.groovy.io.StringBuilderWriter;
import org.codehaus.groovy.GroovyBugError;

import java.io.PrintWriter;
import java.io.Writer;


/**
 * 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 its root.
 *
 * @see Token
 * @see org.codehaus.groovy.syntax.Reduction
 * @see org.codehaus.groovy.syntax.Types
 */
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 its 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 type : types) {
            if (Types.ofType(meaning, type)) {
                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 type : types) {
            if (!Types.ofType(meaning, type)) {
                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 type : types) {
            if (isA(type)) {
                return type;
            }
        }

        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 its 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 its 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 its 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 and its 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 its
     * 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.
     */
    @Override
    public String toString() {
        Writer string = new StringBuilderWriter();
        write(new PrintWriter(string));

        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.isEmpty()) {
            writer.println(")");
        } else {
            writer.print(")");
        }
    }
}
