/*
 * 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.jackrabbit.vault.fs.impl.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Value;

import org.apache.commons.io.output.CloseShieldOutputStream;
import org.apache.jackrabbit.vault.fs.api.Aggregate;
import org.apache.jackrabbit.vault.fs.api.SerializationType;
import org.apache.jackrabbit.vault.fs.io.Serializer;
import org.apache.jackrabbit.vault.util.JcrConstants;

/**
 * {@code DocViewSerializer}...
*
*/
public class CNDSerializer implements Serializer {

    /**
     * the indention string
     */
    private static final String INDENT = "  ";

    /**
     * the export context
     */
    private final Aggregate aggregate;

    /**
     * Creates a new doc view serializer
     * @param aggregate the export context
     */
    public CNDSerializer(Aggregate aggregate) {
        this.aggregate = aggregate;
    }

    /**
     * {@inheritDoc}
     */
    public void writeContent(OutputStream out) throws IOException, RepositoryException {
        try (Writer w = new OutputStreamWriter(new CloseShieldOutputStream(out), "utf-8")) {
            for (String prefix: aggregate.getNamespacePrefixes()) {
                w.write("<'");
                w.write(prefix);
                w.write("'='");
                w.write(escape(aggregate.getNamespaceURI(prefix)));
                w.write("'>\n");
            }
            w.write("\n");
            
            writeNodeTypeDef(w, aggregate.getNode());
        }
        out.flush();
    }

    private void writeNodeTypeDef(Writer out, Node node) throws IOException, RepositoryException {
        writeName(out, node);
        writeSupertypes(out, node);
        writeOptions(out, node);
        writeDefs(out, node);
        out.write("\n");
    }

    private void writeName(Writer out, Node node)
            throws IOException, RepositoryException {
        out.write("[");
        out.write(node.getProperty(JcrConstants.JCR_NODETYPENAME).getString());
        out.write("]");
    }

    private void writeSupertypes(Writer out, Node node) throws IOException, RepositoryException {
        Value[] types = node.getProperty(JcrConstants.JCR_SUPERTYPES).getValues();
        String delim = " > ";
        for (Value s: types) {
            out.write(delim);
            out.write(s.getString());
            delim = ", ";
        }
    }

    private void writeOptions(Writer out, Node node) throws IOException, RepositoryException {
        String delim = "\n " + INDENT;
        if (node.getProperty(JcrConstants.JCR_HASORDERABLECHILDNODES).getBoolean()) {
            out.write(delim);
            out.write("orderable");
            delim = " ";
        }
        if (node.getProperty(JcrConstants.JCR_ISMIXIN).getBoolean()) {
            out.write(delim);
            out.write("mixin");
        }
    }

    private void writeDefs(Writer out, Node node) throws IOException, RepositoryException {
        NodeIterator iter = node.getNodes();
        String primary = null;
        if (node.hasProperty(JcrConstants.JCR_PRIMARYITEMNAME)) {
            primary = node.getProperty(JcrConstants.JCR_PRIMARYITEMNAME).getString();
        }
        while (iter.hasNext()) {
            Node child = iter.nextNode();
            if (child.getPrimaryNodeType().getName().equals(JcrConstants.NT_PROPERTYDEFINITION)) {
                writePropDef(out, child, primary);
            }
        }
        iter = node.getNodes();
        while (iter.hasNext()) {
            Node child = iter.nextNode();
            if (child.getPrimaryNodeType().getName().equals(JcrConstants.NT_CHILDNODEDEFINITION)) {
                writeNodeDef(out, child, primary);
            }
        }
    }

    private void writePropDef(Writer out, Node node, String primary) throws IOException, RepositoryException {
        out.write("\n" + INDENT + "- ");
        String name = "*";
        if (node.hasProperty(JcrConstants.JCR_NAME)) {
            name = node.getProperty(JcrConstants.JCR_NAME).getString();
        }
        out.write(name);
        out.write(" (");
        out.write(node.getProperty(JcrConstants.JCR_REQUIREDTYPE).getString().toLowerCase());
        out.write(")");
        if (node.hasProperty(JcrConstants.JCR_DEFAULTVALUES)) {
            writeDefaultValues(out, node.getProperty(JcrConstants.JCR_DEFAULTVALUES).getValues());
        }
        if (primary != null && primary.equals(name)) {
            out.write(" primary");
        }
        if (node.getProperty(JcrConstants.JCR_MANDATORY).getBoolean()) {
            out.write(" mandatory");
        }
        if (node.getProperty(JcrConstants.JCR_AUTOCREATED).getBoolean()) {
            out.write(" autocreated");
        }
        if (node.getProperty(JcrConstants.JCR_PROTECTED).getBoolean()) {
            out.write(" protected");
        }
        if (node.getProperty(JcrConstants.JCR_MULTIPLE).getBoolean()) {
            out.write(" multiple");
        }
        String opv = node.getProperty(JcrConstants.JCR_ONPARENTVERSION).getString().toLowerCase();
        if (!opv.equals("copy")) {
            out.write(" ");
            out.write(opv);
        }
        if (node.hasProperty(JcrConstants.JCR_VALUECONSTRAINTS)) {
            writeValueConstraints(out, node.getProperty(JcrConstants.JCR_VALUECONSTRAINTS).getValues());
        }
    }

    private void writeDefaultValues(Writer out, Value[] dva) throws IOException, RepositoryException {
        if (dva != null && dva.length > 0) {
            String delim = " = '";
            for (Value value : dva) {
                out.write(delim);
                out.write(escape(value.getString()));
                out.write("'");
                delim = ", '";
            }
        }
    }

    private void writeValueConstraints(Writer out, Value[] vca) throws IOException, RepositoryException {
        String delim = "\n" + INDENT  + "  < ";
        for (Value v: vca) {
            out.write(delim);
            out.write("'");
            out.write(escape(v.getString()));
            out.write("'");
            delim = ", ";
        }
    }

    private void writeNodeDef(Writer out, Node node, String primary) throws IOException, RepositoryException {
        out.write("\n" + INDENT + "+ ");
        String name = "*";
        if (node.hasProperty(JcrConstants.JCR_NAME)) {
            name = node.getProperty(JcrConstants.JCR_NAME).getString();
        }
        out.write(name);

        writeRequiredTypes(out, node.getProperty(JcrConstants.JCR_REQUIREDPRIMARYTYPES).getValues());
        if (node.hasProperty(JcrConstants.JCR_DEFAULTPRIMARYTYPE)) {
            writeDefaultType(out, node.getProperty(JcrConstants.JCR_DEFAULTPRIMARYTYPE).getString());
        }
        if (primary != null && primary.equals(name)) {
            out.write(" primary");
        }
        if (node.getProperty(JcrConstants.JCR_MANDATORY).getBoolean()) {
            out.write(" mandatory");
        }
        if (node.getProperty(JcrConstants.JCR_AUTOCREATED).getBoolean()) {
            out.write(" autocreated");
        }
        if (node.getProperty(JcrConstants.JCR_PROTECTED).getBoolean()) {
            out.write(" protected");
        }
        if (node.getProperty(JcrConstants.JCR_SAMENAMESIBLINGS).getBoolean()) {
            out.write(" multiple");
        }
        String opv = node.getProperty(JcrConstants.JCR_ONPARENTVERSION).getString().toLowerCase();
        if (!opv.equals("copy")) {
            out.write(" ");
            out.write(opv);
        }
    }

    private void writeRequiredTypes(Writer out, Value[] reqTypes) throws IOException, RepositoryException {
        if (reqTypes.length > 0) {
            String delim = " (";
            for (Value value : reqTypes) {
                out.write(delim);
                out.write(value.getString());
                delim = ", ";
            }
            out.write(")");
        }
    }

    private void writeDefaultType(Writer out, String defType) throws IOException {
        if (!defType.equals("*")) {
            out.write(" = ");
            out.write(defType);
        }
    }

    /**
     * escape
     * @param s the string to escape
     * @return the escaped string
     */
    private String escape(String s) {
        StringBuffer sb = new StringBuffer(s);
        for (int i = 0; i < sb.length(); i++) {
            if (sb.charAt(i) == '\\') {
                sb.insert(i, '\\');
                i++;
            } else if (sb.charAt(i) == '\'') {
                sb.insert(i, '\'');
                i++;
            }
        }
        return sb.toString();
    }


    /**
     * {@inheritDoc}
     *
     * @return {@link SerializationType#CND}
     */
    public SerializationType getType() {
        return SerializationType.CND;
    }
}