/*-*
 * Copyright © 2010-2015 Atilika Inc. and contributors (see CONTRIBUTORS.md)
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.  A copy of the
 * License is distributed with this work in the LICENSE.md file.  You may
 * also obtain a copy of the License from
 *
 *     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 com.atilika.kuromoji.trie;

import com.atilika.kuromoji.trie.PatriciaTrie.KeyMapper;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;

/**
 * Utility class to format a {@link PatriciaTrie} to dot format for debugging, inspection, etc.
 *
 * @param <V> value type
 *
 * See @see <a href="http://graphviz.org/">Graphviz</a>
 */
public class PatriciaTrieFormatter<V> {

    private final static String FONT_NAME = "Helvetica";

    /**
     * Constructor
     */
    public PatriciaTrieFormatter() {}

    /**
     * Format trie
     *
     * @param trie  trie to format
     * @return formatted trie, not null
     */
    public String format(PatriciaTrie<V> trie) {
        return format(trie, true);
    }

    /**
     * Format trie
     *
     * @param trie  trie to format
     * @param formatBitString  true if the bits for this key should be included in the node
     * @return formatted trie, not null
     */
    public String format(PatriciaTrie<V> trie, boolean formatBitString) {
        StringBuilder builder = new StringBuilder();
        builder.append(formatHeader());
        builder.append(formatNode(trie.getRoot().getLeft(), -1, trie.getKeyMapper(), formatBitString));
        builder.append(formatTrailer());
        return builder.toString();
    }

    /**
     * Format trie and write to file
     *
     * @param trie  trie to format
     * @param file  file to write to
     * @throws FileNotFoundException if the file exists but is a directory rather than a regular file,
     * does not exist but cannot be created, or cannot be opened for any other reason
     */
    public void format(PatriciaTrie<V> trie, File file) throws FileNotFoundException {
        format(trie, file, false);
    }

    /**
     * Format trie and write to file
     *
     * @param trie  trie to format
     * @param file  file to write to
     * @param formatBitString  true if the bits for this key should be included in the node
     * @throws FileNotFoundException if the file exists but is a directory rather than a regular file,
     * does not exist but cannot be created, or cannot be opened for any other reason
     */
    public void format(PatriciaTrie<V> trie, File file, boolean formatBitString) throws FileNotFoundException {
        PrintWriter writer = new PrintWriter(new FileOutputStream(file));
        writer.println(format(trie, formatBitString));
        writer.close();
    }

    /**
     * Format header
     *
     * @return formatted header, not null
     */
    private String formatHeader() {
        StringBuilder builder = new StringBuilder();
        builder.append("digraph patricia {\n");
        //      builder.append("graph [ fontsize=30 labelloc=\"t\" label=\"\" splines=true overlap=false ];\n");
        //      builder.append("# A2 paper size\n");
        //      builder.append("size = \"34.4,16.5\";\n");
        //      builder.append("# try to fill paper\n");
        //      builder.append("ratio = fill;\n");
        //      builder.append("edge [ fontname=\"" + FONT_NAME + "\" fontcolor=\"red\" color=\"#606060\" ]\n");
        builder.append("nodesep=1.5;");
        builder.append("node [ style=\"filled\" fillcolor=\"#e8e8f0\" shape=\"Mrecord\" fontname=\"" + FONT_NAME
                        + "\" ]\n");
        //      builder.append("edge [ fontname=\"" + FONT_NAME + "\" fontcolor=\"red\" color=\"#606060\" ]\n");
        //      builder.append("node [ shape=\"circle\" ]\n");
        return builder.toString();
    }

    /**
     * Format trailer
     *
     * @return formatted trailer
     */
    private String formatTrailer() {
        return "}";
    }

    /**
     * Formats nodes
     *
     * @param node  node to format
     * @param bit  bit for this node
     * @param keyMapper  keymapper to map keys to bits
     * @param formatBitString  true if the bits for this key should be included in the node
     * @return formatted node, not null
     */
    private String formatNode(PatriciaTrie.PatriciaNode<V> node, int bit, KeyMapper<String> keyMapper,
                    boolean formatBitString) {
        if (node.getBit() <= bit) {
            return "";
        } else {
            StringBuffer buffer = new StringBuffer();
            buffer.append("\"");
            buffer.append(getNodeId(node));
            buffer.append("\"");
            buffer.append(" [ ");
            buffer.append("label=");
            buffer.append(formatNodeLabel(node, keyMapper, formatBitString));
            buffer.append(" ]");
            buffer.append("\n");

            buffer.append(formatPointer(node, node.getLeft(), "l", "sw"));
            buffer.append(formatPointer(node, node.getRight(), "r", "se"));

            buffer.append(formatNode(node.getLeft(), node.getBit(), keyMapper, formatBitString));
            buffer.append(formatNode(node.getRight(), node.getBit(), keyMapper, formatBitString));

            return buffer.toString();
        }
    }

    /**
     * Formats a link between two nodes
     *
     * @param from  from node
     * @param to  to node
     * @param label  label for this link
     * @param tailport  tail port to use when formatting (dot-specific, "sw" or "se)
     * @return formatted link, not null
     */
    private String formatPointer(PatriciaTrie.PatriciaNode<V> from, PatriciaTrie.PatriciaNode<V> to, String label,
                    String tailport) {
        StringBuilder builder = new StringBuilder();
        builder.append(getNodeId(from));
        builder.append(" -> ");
        builder.append(getNodeId(to));
        builder.append(" [ ");
        builder.append("label=\"");
        builder.append(label);
        builder.append(" \"");
        builder.append("tailport=\"");
        builder.append(tailport);
        builder.append(" \"");
        builder.append("fontcolor=\"#666666\" ");
        builder.append(" ]");
        builder.append("\n");
        return builder.toString();
    }

    /**
     * Format node label
     *
     * @param node  node to format
     * @param keyMapper  keymapper to map keys to bits
     * @param formatBitString  true if the bits for this key should be included in the node
     * @return formatted node, not null
     */
    private String formatNodeLabel(PatriciaTrie.PatriciaNode<V> node, KeyMapper<String> keyMapper,
                    boolean formatBitString) {
        StringBuilder builder = new StringBuilder();
        builder.append("<<table border=\"0\" cellborder=\"0\">");
        // Key
        builder.append("<tr><td>");
        builder.append("key: <font color=\"#00a000\">");
        builder.append(getNodeLabel(node));
        builder.append("</font> </td></tr>");

        // Critical bit
        builder.append("<tr><td>");
        builder.append("bit: <font color=\"blue\">");
        builder.append(node.getBit());
        builder.append("</font> </td></tr>");

        // Bit string
        if (formatBitString) {
            builder.append("<tr><td>");
            builder.append("bitString: <font color=\"blue\">");
            String bitString = keyMapper.toBitString(node.getKey());
            int c = node.getBit() + node.getBit() / 4;
            builder.append(bitString.substring(0, c));
            builder.append("<font color=\"red\">");
            builder.append(bitString.charAt(c));
            builder.append("</font>");
            builder.append(bitString.substring(c + 1));
            builder.append("</font> </td></tr>");
        }

        // Value
        builder.append("<tr><td>");
        builder.append("value: <font color=\"#00a0a0\">");
        builder.append(node.getValue());
        builder.append("</font> </td></tr>");

        builder.append("</table>>");
        return builder.toString();
    }

    /**
     * Get node label
     *
     * @param node
     * @return label, not null
     */
    private String getNodeLabel(PatriciaTrie.PatriciaNode<V> node) {
        return node.getKey();
    }

    /**
     * Get node id used to distinguish nodes internally
     *
     * @param node
     * @return node id, not null
     */
    private String getNodeId(PatriciaTrie.PatriciaNode<V> node) {
        if (node == null) {
            return "null";
        } else {
            return node.getKey();
        }
    }
}
