/*
 * 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.camel.groovy.xml;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;

import groovy.util.Node;
import groovy.xml.XmlNodePrinter;
import groovy.xml.XmlParser;
import groovy.xml.XmlUtil;
import groovy.xml.slurpersupport.GPathResult;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.DataFormatName;
import org.apache.camel.spi.annotations.Dataformat;
import org.apache.camel.support.service.ServiceSupport;
import org.apache.camel.util.StringHelper;

@Dataformat("groovyXml")
public class GroovyXmlDataFormat extends ServiceSupport implements DataFormat, DataFormatName {

    private static final int START_TAG = 1;
    private static final int VALUE = 2;
    private static final int END_TAG = 3;

    private boolean attributeMapping = true;

    public boolean isAttributeMapping() {
        return attributeMapping;
    }

    public void setAttributeMapping(boolean attributeMapping) {
        this.attributeMapping = attributeMapping;
    }

    @Override
    public void marshal(Exchange exchange, Object graph, OutputStream stream) throws Exception {
        if (graph instanceof GPathResult gp) {
            XmlUtil.serialize(gp, stream);
        } else if (graph instanceof Node n) {
            serialize(n, stream);
        } else if (graph instanceof Map map) {
            serialize(exchange, map, stream);
        } else {
            // optional jackson support (TODO: jackson3)
            if (graph.getClass().getName().startsWith("com.fasterxml.jackson.databind")) {
                var map = exchange.getContext().getTypeConverter().convertTo(Map.class, exchange, graph);
                serialize(exchange, map, stream);
            } else {
                byte[] arr = exchange.getContext().getTypeConverter().mandatoryConvertTo(byte[].class, exchange, graph);
                stream.write(arr);
            }
        }
    }

    @Override
    public Object unmarshal(Exchange exchange, InputStream stream) throws Exception {
        XmlParser parser = new XmlParser();
        return parser.parse(stream);
    }

    @Override
    public String getDataFormatName() {
        return "groovyXml";
    }

    private void serialize(Node node, OutputStream os) {
        PrintWriter pw = new PrintWriter(os);
        XmlNodePrinter nodePrinter = new XmlNodePrinter(pw);
        nodePrinter.setPreserveWhitespace(true);
        nodePrinter.print(node);
    }

    private void printLines(List<Line> lines, OutputStream os) throws Exception {
        // add missing root end tag
        lines.add(new Line(lines.get(0).key, null, END_TAG, null));
        int level = 0;
        for (Line line : lines) {
            int kind = line.kind;
            if (kind == START_TAG) {
                String pad = StringHelper.padString(level);
                os.write(pad.getBytes());
                os.write("<".getBytes());
                os.write(line.key.getBytes());
                if (line.attrs != null) {
                    StringJoiner sj = new StringJoiner(" ");
                    for (var a : line.attrs.entrySet()) {
                        sj.add(a.getKey() + "=\"" + a.getValue() + "\"");
                    }
                    if (sj.length() > 0) {
                        os.write(" ".getBytes());
                        os.write(sj.toString().getBytes());
                    }
                }
                os.write(">\n".getBytes());
                level++;
            } else if (kind == END_TAG) {
                level--;
                String pad = StringHelper.padString(level);
                os.write(pad.getBytes());
                os.write("</".getBytes());
                os.write(line.key.getBytes());
                os.write(">\n".getBytes());
            } else {
                String pad = StringHelper.padString(level);
                os.write(pad.getBytes());
                os.write("<".getBytes());
                os.write(line.key.getBytes());
                os.write(">".getBytes());
                os.write(line.value.getBytes());
                os.write("</".getBytes());
                os.write(line.key.getBytes());
                os.write(">\n".getBytes());
            }
        }
    }

    private void serialize(Exchange exchange, Map<String, Object> map, OutputStream os) throws Exception {
        List<Line> lines = new ArrayList<>();
        doSerialize(exchange.getContext(), map, lines);
        printLines(lines, os);
    }

    private void doSerialize(CamelContext context, Map<String, Object> map, List<Line> lines) {

        // attributes
        Map<String, String> attrs = new LinkedHashMap<>();
        if (attributeMapping) {
            for (String key : map.keySet()) {
                if (key.startsWith("_") || key.startsWith("@")) {
                    String val = context.getTypeConverter().convertTo(String.class, map.get(key));
                    if (val != null) {
                        val = val.trim();
                        if (!val.isBlank()) {
                            attrs.put(key.substring(1), val);
                        }
                    }
                }
            }
        }

        boolean root = false;
        for (var e : map.entrySet()) {
            String key = e.getKey();

            // attribute mappings are disabled
            if (key.startsWith("_") || key.startsWith("@")) {
                continue;
            }

            if (!attrs.isEmpty() && !lines.isEmpty()) {
                int pos = lines.size() - 1;
                Line prev = lines.get(pos);
                lines.remove(pos);
                Line updated = new Line(prev.key, prev.value, prev.kind, new LinkedHashMap<>(attrs));
                lines.add(updated);
                attrs.clear();
            }

            // root tag
            if (lines.isEmpty()) {
                root = true;
                lines.add(new Line(key, null, START_TAG, null));
            }

            if (e.getValue() != null) {
                // nested list or map
                if (e.getValue() instanceof Map cm) {
                    doSerialize(context, cm, lines);
                } else if (e.getValue() instanceof List cl) {
                    doSerialize(context, cl, key, attrs, lines, root);
                } else {
                    String val = context.getTypeConverter().convertTo(String.class, e.getValue());
                    if (val != null) {
                        val = val.trim();
                        if (!val.isBlank()) {
                            lines.add(new Line(key, val, VALUE, null));
                        }
                    }
                }
            }
        }
    }

    private void doSerialize(
            CamelContext context, List list, String key, Map<String, String> attrs, List<Line> lines, boolean root) {
        for (var e : list) {
            if (!root) {
                lines.add(new Line(key, null, START_TAG, attrs));
            }
            if (e instanceof Map map) {
                doSerialize(context, map, lines);
            }
            if (!root) {
                lines.add(new Line(key, null, END_TAG, null));
            }
        }
    }

    record Line(String key, String value, int kind, Map<String, String> attrs) {
    }

}
