package de.uni_hamburg.fs;

import collections.CollectionEnumeration;


/**
 * A utility class for creating string representations of feature structures.
 * PrettyPrinter creates formatted text output for nodes and feature structures,
 * with proper indentation and cycles handling through a tag system.
 */
public class PrettyPrinter {
    /**
     * The tag map used to keep track of visited nodes and handle cycles
     */
    private TagMap _map;

    /**
     * Constructs a new PrettyPrinter for the given node.
     * Initializes the tag map to handle potential cycles in the node structure.
     *
     * @param node the root node of the structure to be printed
     */
    private PrettyPrinter(Node node) {
        _map = new TagMap(node);
    }

    /**
     * Prints the feature structure to the standard output with formatting.
     *
     * @param fs the feature structure to print
     */
    public static void println(FeatureStructure fs) {
        System.out.println(toString(fs));
    }

    /**
     * Converts a feature structure to its string representation.
     * The output includes proper formatting with newlines and indentation.
     *
     * @param fs the feature structure to convert
     * @return a formatted string representation of the feature structure
     */
    public static String toString(FeatureStructure fs) {
        Node node = fs.getRoot();
        return "\n" + new PrettyPrinter(node).toString(node, "\n ", Type.TOP) + "\n";
    }

    private String toString(Node thiz, String indent, Type defaultType) {
        StringBuffer output = new StringBuffer();

        //output.append('*').append(seqNo); indent += FeatureStructure.indent(output.length());
        Type nodetype = thiz.getType();
        boolean isNode = !(nodetype instanceof BasicType && ((BasicType) nodetype).isObject()
            || nodetype instanceof NullObject);
        if (isNode) {
            Name tag = _map.getTag(thiz);
            if (!tag.equals(Name.EMPTY)) {
                output = output.append("#").append(tag._name);
                indent += indent(output.length());
            }
            if (!_map.visit(thiz)) {
                if (thiz instanceof JavaObject) {
                    output.append(thiz.toString());
                } else {
                    output.append(untaggedToString(thiz, indent, defaultType));
                }
            }
        } else {
            output.append(untaggedToString(thiz, indent, defaultType));
        }
        return output.toString();
    }

    private String listToString(Node thiz, String indent, Type defaultType) {
        StringBuffer output = new StringBuffer();
        try { // catch ClassCast between Type and ListType!
            ListType list = (ListType) thiz.getType();
            Type elemtype = list.getBaseType();

            if (list.getSubtype() == ListType.LIST) {
                output.append("| ").append(toString(thiz, indent + "   ", defaultType));
            } else {
                Type defaultElemType = null;
                if (defaultType instanceof ListType) {
                    defaultElemType = ((ListType) defaultType).getBaseType();
                } else {
                    // default type is not a list type; does not matter.
                    defaultElemType = Type.TOP;
                }
                if (!elemtype.equals(defaultElemType)) {
                    String typename = elemtype.getName();
                    output.append(typename);
                    indent += indent(typename.length());
                }
                if (list.getSubtype() == ListType.NELIST) {
                    //if (thiz.hasFeature(ListType.HEAD))
                    output =
                        output.append(toString(thiz.delta(ListType.HEAD), indent + " ", elemtype))
                            .append(indent);


                    //else
                    //output = output.append("(no head feature!)");
                    //if (thiz.hasFeature(ListType.TAIL)) {
                    Node tailfs = thiz.delta(ListType.TAIL);
                    Name tag = _map.getTag(tailfs);
                    Type listtype = tailfs.getType();
                    boolean isTailList = false;
                    if (listtype instanceof ListType) {
                        ListType tailtype = (ListType) tailfs.getType();
                        if (tag.equals(Name.EMPTY) && tailtype.getBaseType().equals(elemtype)
                            || tailtype.getSubtype() == ListType.ELIST) {
                            output = output
                                .append(listToString(tailfs, indent, ListType.getList(elemtype)));
                            isTailList = true;
                        }
                    }
                    if (!isTailList) {
                        output = output.append("| ")
                            .append(toString(tailfs, indent + "   ", ListType.getList(elemtype)));
                    }


                    //} else
                    //  output = output.append("(no tail feature!)");
                }
            }
        } catch (ClassCastException ex) {
            output.append("!!!corrupted list!!!");
        }
        return output.toString();
    }

    private String innerToString(Node thiz, String indent, Type defaultType) {
        StringBuffer output = new StringBuffer();
        String whitespace = "";
        Type nodetype = thiz.getType();
        if (!nodetype.equals(defaultType)) {
            output = output.append(nodetype.getName());
            whitespace = indent;
        }
        CollectionEnumeration features = thiz.featureNames();
        while (features.hasMoreElements()) {
            Name featureName = (Name) features.nextElement();
            String feature = featureName.toString();
            output = output.append(whitespace).append(feature).append(": ").append(
                toString(
                    thiz.delta(featureName), indent + indent(3 + feature.length()),
                    thiz.getType().appropType(featureName)));
            whitespace = indent;
        }
        return output.toString();
    }

    /**
     * Converts a node to its string representation without tag information.
     * Handles different node types (List, Basic, and others) appropriately.
     *
     * @param thiz        the node to convert
     * @param indent      the current indentation string
     * @param defaultType the default type to use for comparison
     * @return a string representation of the node without tags
     */
    public String untaggedToString(Node thiz, String indent, Type defaultType) {
        StringBuffer output = new StringBuffer();
        Type nodetype = thiz.getType();
        if (nodetype instanceof ListType && ((ListType) nodetype).getSubtype() != ListType.LIST) {
            output =
                output.append("<").append(listToString(thiz, indent, defaultType)).append(" >");
        } else if (nodetype instanceof BasicType) {
            output.append(innerToString(thiz, indent, defaultType));
        } else {
            output.append("[").append(innerToString(thiz, indent, defaultType)).append("]");
        }
        return output.toString();
    }

    /**
     * Creates a string of spaces for indentation.
     *
     * @param depth the number of spaces to include in the indentation
     * @return a string containing the specified number of spaces
     */
    public final static String indent(int depth) {
        StringBuffer space = new StringBuffer(depth + 1);
        for (int i = 0; i < depth; ++i) {
            space.append(' ');
        }
        return space.toString();
    }
}