/*_############################################################################
  _##
  _##  SNMP4J-Agent 3 - MarkdownMIBTreeFormatter.java
  _##
  _##  Copyright (C) 2005-2026  Frank Fock (SNMP4J.org)
  _##
  _##  Licensed 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.snmp4j.agent.util;

import org.snmp4j.agent.io.prop.MIBTree;
import org.snmp4j.agent.io.prop.MIBTree.Node;
import org.snmp4j.agent.io.prop.MIBTree.Row;
import org.snmp4j.agent.io.prop.MIBTree.Table;
import org.snmp4j.agent.io.prop.UnresolvedReference;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;

import java.io.IOException;

/**
 * Renders a {@link MIBTree} as a compact GitHub-flavored Markdown document that gives a human
 * readable overview of a MIB configuration.
 * <p>
 * The OID hierarchy is rendered as a nested bullet list in which chains of intermediate nodes with
 * a single child are collapsed, so each bullet is the full OID of a branching point or a configured
 * managed object. Scalar objects show their SMI syntax and value inline; conceptual tables are
 * rendered as Markdown tables (one row per configured row, one column per configured column).
 * Values that reference a runtime instance which could not be resolved statically are shown as
 * their configured {@link UnresolvedReference reference} token.
 *
 * @author SNMP4J.org
 * @version 3.9.2
 * @since 3.9.2
 */
public class MarkdownMIBTreeFormatter implements MIBTreeFormatter {

    private static final String NL = "\n";

    @Override
    public String format(MIBTree tree) {
        StringBuilder out = new StringBuilder(4096);
        out.append("# MIB Configuration Overview").append(NL).append(NL);
        out.append("_Generated from an SNMP4J-Agent `MOInput` property configuration._")
                .append(NL).append(NL);

        boolean any = false;
        for (OctetString context : tree.getContexts()) {
            any = true;
            Node root = tree.getRoot(context);
            out.append("## ").append(contextTitle(context)).append(NL).append(NL);
            int[] counts = new int[2]; // [scalars, tables]
            countObjects(root, counts);
            out.append("_").append(counts[0]).append(" scalar object(s), ")
                    .append(counts[1]).append(" table(s)._").append(NL).append(NL);
            renderChildren(root, 0, out);
            out.append(NL);
        }
        if (!any) {
            out.append("_No managed objects configured._").append(NL);
        }
        return out.toString();
    }

    @Override
    public void format(MIBTree tree, Appendable out) throws IOException {
        out.append(format(tree));
    }

    // ---- tree rendering ----------------------------------------------------

    private void renderChildren(Node node, int depth, StringBuilder out) {
        for (Node child : node.getChildren()) {
            // Collapse chains of single-child, non-object nodes into a single line.
            Node n = child;
            while (!n.isManagedObject() && n.getChildren().size() == 1) {
                n = n.getChildren().iterator().next();
            }
            out.append(indent(depth)).append("- `").append(n.getOID().toDottedString()).append('`');
            if (n.isManagedObject()) {
                if (n.isTable()) {
                    Table table = n.getTable();
                    out.append(" — table (").append(table.getRows().size()).append(" row(s), ")
                            .append(table.getColumnCount()).append(" column(s))").append(NL);
                    renderTable(table, out);
                } else {
                    out.append(" — ").append(scalarDescription(n.getScalarValue())).append(NL);
                }
            } else {
                out.append(NL);
            }
            renderChildren(n, depth + 1, out);
        }
    }

    private void renderTable(Table table, StringBuilder out) {
        int cols = table.getColumnCount();
        out.append(NL);
        // header
        out.append("| # | Index |");
        for (int c = 0; c < cols; c++) {
            out.append(" Col ").append(c).append(" |");
        }
        out.append(NL);
        // separator
        out.append("|---|-------|");
        for (int c = 0; c < cols; c++) {
            out.append("-------|");
        }
        out.append(NL);
        // rows
        int rowNo = 0;
        for (Row row : table.getRows()) {
            out.append("| ").append(rowNo++).append(" | ").append(code(row.getIndex().toDottedString())).append(" |");
            Variable[] values = row.getValues();
            for (int c = 0; c < cols; c++) {
                Variable v = (values != null && c < values.length) ? values[c] : null;
                out.append(' ').append(cell(v)).append(" |");
            }
            out.append(NL);
        }
        out.append(NL);
    }

    // ---- value rendering ---------------------------------------------------

    private String scalarDescription(Variable value) {
        if (value == null) {
            return "*(empty)*";
        }
        if (value instanceof UnresolvedReference) {
            return "unresolved reference " + code(value.toString());
        }
        return "`" + value.getSyntaxString() + "` " + code(value.toString());
    }

    /** Renders a table cell value as escaped inline code (empty for {@code null}). */
    private String cell(Variable v) {
        if (v == null) {
            return "";
        }
        return code(v.toString());
    }

    /** Wraps {@code s} as inline code, escaping characters that would break a Markdown table. */
    private String code(String s) {
        if (s == null || s.isEmpty()) {
            return "";
        }
        String escaped = s.replace("\r", " ")
                .replace("\n", " ")
                .replace("`", "'")
                .replace("|", "\\|");
        return "`" + escaped + "`";
    }

    // ---- helpers -----------------------------------------------------------

    private static void countObjects(Node node, int[] counts) {
        if (node == null) {
            return;
        }
        if (node.isManagedObject()) {
            if (node.isTable()) {
                counts[1]++;
            } else {
                counts[0]++;
            }
        }
        for (Node child : node.getChildren()) {
            countObjects(child, counts);
        }
    }

    private static String contextTitle(OctetString context) {
        if (context == null) {
            return "Context-independent (default)";
        }
        return "Context \"" + context + "\"";
    }

    private static String indent(int depth) {
        StringBuilder b = new StringBuilder(depth * 2);
        for (int i = 0; i < depth; i++) {
            b.append("  ");
        }
        return b.toString();
    }
}
