/*
 * 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.apache.kafka.common.protocol;

import org.apache.kafka.common.message.RequestHeaderData;
import org.apache.kafka.common.message.ResponseHeaderData;
import org.apache.kafka.common.protocol.types.BoundField;
import org.apache.kafka.common.protocol.types.Field;
import org.apache.kafka.common.protocol.types.Schema;
import org.apache.kafka.common.protocol.types.TaggedFields;
import org.apache.kafka.common.protocol.types.Type;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class Protocol {

    private static String indentString(int size) {
        return " ".repeat(Math.max(0, size));
    }

    private static void schemaToBnfHtml(Schema schema, StringBuilder b, int indentSize) {
        final String indentStr = indentString(indentSize);
        final Map<String, Type> subTypes = new LinkedHashMap<>();

        // Top level fields
        for (BoundField field: schema.fields()) {
            Type type = field.def.type;
            if (type.isArray()) {
                b.append("[");
                b.append(field.def.name);
                b.append("] ");
                if (!subTypes.containsKey(field.def.name)) {
                    subTypes.put(field.def.name, type.arrayElementType().get());
                }
            } else if (type instanceof TaggedFields) {
                Map<Integer, Field> taggedFields = new TreeMap<>(((TaggedFields) type).fields());
                taggedFields.forEach((tag, taggedField) -> {
                    if (taggedField.type.isArray()) {
                        b.append("[");
                        b.append(taggedField.name);
                        b.append("]");
                        if (!subTypes.containsKey(taggedField.name))
                            subTypes.put(taggedField.name + "&lt;tag: " + tag.toString() + "&gt;", taggedField.type.arrayElementType().get());
                    } else {
                        b.append(taggedField.name);
                        if (!subTypes.containsKey(taggedField.name))
                            subTypes.put(taggedField.name + "&lt;tag: " + tag.toString() + "&gt;", taggedField.type);
                    }
                    b.append("&lt;tag: ");
                    b.append(tag);
                    b.append("&gt; ");
                });
            } else {
                b.append(field.def.name);
                b.append(" ");
                if (!subTypes.containsKey(field.def.name))
                    subTypes.put(field.def.name, type);
            }
        }
        b.append("\n");

        // Sub Types/Schemas
        for (Map.Entry<String, Type> entry: subTypes.entrySet()) {
            if (entry.getValue() instanceof Schema) {
                // Complex Schema Type
                b.append(indentStr);
                b.append(entry.getKey());
                b.append(" => ");
                schemaToBnfHtml((Schema) entry.getValue(), b, indentSize + 2);
            } else {
                // Standard Field Type
                b.append(indentStr);
                b.append(entry.getKey());
                b.append(" => ");
                b.append(entry.getValue());
                b.append("\n");
            }
        }
    }

    private static void populateSchemaFields(Schema schema, Set<BoundField> fields) {
        for (BoundField field: schema.fields()) {
            fields.add(field);
            if (field.def.type.isArray()) {
                Type innerType = field.def.type.arrayElementType().get();
                if (innerType instanceof Schema)
                    populateSchemaFields((Schema) innerType, fields);
            } else if (field.def.type instanceof Schema)
                populateSchemaFields((Schema) field.def.type, fields);
        }
    }

    private static void appendFieldNameToTable(String name, StringBuilder b) {
        b.append("<td>");
        b.append(name);
        b.append("</td>");
    }

    private static void schemaToFieldTableHtml(Schema schema, StringBuilder b) {
        Set<BoundField> fields = new LinkedHashSet<>();
        populateSchemaFields(schema, fields);

        b.append("<table class=\"data-table\"><tbody>\n");
        b.append("<tr>");
        b.append("<th>Field</th>\n");
        b.append("<th>Description</th>\n");
        b.append("</tr>");
        for (BoundField field : fields) {
            b.append("<tr>\n");
            if (field.def.type instanceof TaggedFields) {
                TaggedFields taggedFields = (TaggedFields) field.def.type;
                // Only include the field in the table if there are actually tags defined
                if (taggedFields.numFields() > 0) {
                    taggedFields.fields().forEach((tag, taggedField) -> {
                        appendFieldNameToTable(taggedField.name + "&lt;tag: " + tag.toString() + "&gt;", b);
                        b.append("<td>");
                        b.append(taggedField.docString);
                        if (taggedField.type.isArray()) {
                            Type innerType = taggedField.type.arrayElementType().get();
                            if (innerType instanceof Schema) {
                                schemaToFieldTableHtml((Schema) innerType, b);
                            }
                        } else if (taggedField.type instanceof Schema) {
                            schemaToFieldTableHtml((Schema) taggedField.type, b);
                        }
                        b.append("</td>");
                        b.append("</tr>\n");
                    });
                }
            } else {
                appendFieldNameToTable(field.def.name, b);
                b.append("<td>");
                b.append(field.def.docString);
            }
            b.append("</td>");
            b.append("</tr>\n");
        }
        b.append("</tbody></table>\n");
    }

    public static String toHtml() {
        final StringBuilder b = new StringBuilder();
        b.append("<h5>Headers:</h5>\n");

        for (int i = RequestHeaderData.LOWEST_SUPPORTED_VERSION; i <= RequestHeaderData.HIGHEST_SUPPORTED_VERSION; i++) {
            b.append("<pre>");
            b.append("Request Header v").append(i).append(" => ");
            schemaToBnfHtml(RequestHeaderData.SCHEMAS[i], b, 2);
            b.append("</pre>\n");
            schemaToFieldTableHtml(RequestHeaderData.SCHEMAS[i], b);
        }
        for (int i = ResponseHeaderData.LOWEST_SUPPORTED_VERSION; i <= ResponseHeaderData.HIGHEST_SUPPORTED_VERSION; i++) {
            b.append("<pre>");
            b.append("Response Header v").append(i).append(" => ");
            schemaToBnfHtml(ResponseHeaderData.SCHEMAS[i], b, 2);
            b.append("</pre>\n");
            schemaToFieldTableHtml(ResponseHeaderData.SCHEMAS[i], b);
        }
        for (ApiKeys key : ApiKeys.clientApis()) {
            // Key
            b.append("<h5>");
            b.append("<a name=\"The_Messages_" + key.name + "\">");
            b.append(key.name);
            b.append(" API (Key: ");
            b.append(key.id);
            b.append("):</a></h5>\n\n");
            // Requests
            b.append("<b>Requests:</b><br>\n");
            Schema[] requests = key.messageType.requestSchemas();
            for (short version = key.oldestVersion(); version <= key.latestVersion(); version++) {
                Schema schema = requests[version];
                if (schema == null)
                    throw new IllegalStateException("Unexpected null schema for " + key + " with version " + version);
                // Schema
                b.append("<div>");
                // Version header
                b.append("<pre>");
                b.append(key.name);
                b.append(" Request (Version: ");
                b.append(version);
                b.append(") => ");
                schemaToBnfHtml(schema, b, 2);
                b.append("</pre>");

                if (!key.isVersionEnabled(version, false)) {
                    b.append("<p>This version of the request is unstable.</p>");
                }

                b.append("<p><b>Request header version:</b> ");
                b.append(key.requestHeaderVersion(version));
                b.append("</p>\n");

                schemaToFieldTableHtml(schema, b);
                b.append("</div>\n");
            }

            // Responses
            b.append("<b>Responses:</b><br>\n");
            Schema[] responses = key.messageType.responseSchemas();
            for (int version = key.oldestVersion(); version <= key.latestVersion(); version++) {
                Schema schema = responses[version];
                if (schema == null)
                    throw new IllegalStateException("Unexpected null schema for " + key + " with version " + version);
                // Schema
                b.append("<div>");
                // Version header
                b.append("<pre>");
                b.append(key.name);
                b.append(" Response (Version: ");
                b.append(version);
                b.append(") => ");
                schemaToBnfHtml(responses[version], b, 2);
                b.append("</pre>");

                b.append("<p><b>Response header version:</b> ");
                b.append(key.responseHeaderVersion((short) version));
                b.append("</p>\n");

                schemaToFieldTableHtml(responses[version], b);
                b.append("</div>\n");
            }
        }

        return b.toString();
    }

    public static void main(String[] args) {
        System.out.println(toHtml());
    }

}
