package org.codehaus.modello.plugin.jsonschema;

/*
 * Copyright (c) 2013, Codehaus.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import javax.inject.Named;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonGenerator.Feature;
import com.fasterxml.jackson.core.json.JsonWriteFeature;
import org.codehaus.modello.ModelloException;
import org.codehaus.modello.ModelloParameterConstants;
import org.codehaus.modello.model.Model;
import org.codehaus.modello.model.ModelAssociation;
import org.codehaus.modello.model.ModelClass;
import org.codehaus.modello.model.ModelDefault;
import org.codehaus.modello.model.ModelField;
import org.codehaus.modello.plugins.xml.AbstractXmlJavaGenerator;
import org.codehaus.modello.plugins.xml.metadata.XmlAssociationMetadata;
import org.codehaus.plexus.util.StringUtils;

/**
 * @author <a href="mailto:simonetripodi@apache.org">Simone Tripodi</a>
 * @since 1.8
 */
@Named("jsonschema")
public final class JsonSchemaGenerator extends AbstractXmlJavaGenerator {

    @Override
    public void generate(Model model, Map<String, Object> parameters) throws ModelloException {
        initialize(model, parameters);

        try {
            generateJsonSchema(parameters);
        } catch (IOException ioe) {
            throw new ModelloException("Exception while generating JSON Schema.", ioe);
        }
    }

    private void generateJsonSchema(Map<String, Object> parameters) throws IOException, ModelloException {
        Model objectModel = getModel();

        File directory = getOutputDirectory();

        if (isPackageWithVersion()) {
            directory = new File(directory, getGeneratedVersion().toString());
        }

        if (!directory.exists()) {
            directory.mkdirs();
        }

        // we assume parameters not null
        String schemaFileName = (String) parameters.get(ModelloParameterConstants.OUTPUT_JSONSCHEMA_FILE_NAME);

        File schemaFile;

        if (schemaFileName != null) {
            schemaFile = new File(directory, schemaFileName);
        } else {
            schemaFile = new File(directory, objectModel.getId() + "-" + getGeneratedVersion() + ".schema.json");
        }

        JsonGenerator generator = new JsonFactory()
                .enable(Feature.AUTO_CLOSE_JSON_CONTENT)
                .enable(Feature.AUTO_CLOSE_TARGET)
                .enable(Feature.FLUSH_PASSED_TO_STREAM)
                .enable(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature())
                .enable(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature())
                .enable(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature())
                .disable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS.mappedFeature())
                .createGenerator(newWriter(schemaFile.toPath(), StandardCharsets.UTF_8));

        generator.useDefaultPrettyPrinter();

        ModelClass root = objectModel.getClass(objectModel.getRoot(getGeneratedVersion()), getGeneratedVersion());

        try {
            generator.writeStartObject();
            generator.writeStringField("$schema", "http://json-schema.org/draft-04/schema#");

            writeClassDocumentation(generator, root, true);

            generator.writeObjectFieldStart("definitions");

            for (ModelClass current : objectModel.getClasses(getGeneratedVersion())) {
                if (!root.equals(current)) {
                    writeClassDocumentation(generator, current, false);
                }
            }

            // end "definitions"
            generator.writeEndObject();

            // end main object
            generator.writeEndObject();
        } finally {
            generator.close();
        }
    }

    private void writeClassDocumentation(JsonGenerator generator, ModelClass modelClass, boolean isRoot)
            throws IOException {
        if (!isRoot) {
            generator.writeObjectFieldStart(modelClass.getName());
        }

        generator.writeStringField("id", modelClass.getName() + '#');
        writeDescriptionField(generator, modelClass.getDescription());
        writeTypeField(generator, "object");

        generator.writeObjectFieldStart("properties");

        List<String> required = new LinkedList<>();

        ModelClass reference = modelClass;
        // traverse the whole modelClass hierarchy to create the nested Builder instance
        while (reference != null) {
            // collect parameters and set them in the instance object
            for (ModelField modelField : reference.getFields(getGeneratedVersion())) {
                if (modelField.isRequired()) {
                    required.add(modelField.getName());
                }

                // each field is represented as object
                generator.writeObjectFieldStart(modelField.getName());

                writeDescriptionField(generator, modelField.getDescription());

                if (modelField instanceof ModelAssociation) {
                    ModelAssociation modelAssociation = (ModelAssociation) modelField;

                    if (modelAssociation.isOneMultiplicity()) {
                        writeTypeField(generator, modelAssociation.getType());
                    } else {
                        // MANY_MULTIPLICITY
                        writeTypeField(generator, "array");

                        generator.writeObjectFieldStart("items");

                        String type = modelAssociation.getType();
                        String toType = modelAssociation.getTo();

                        if (ModelDefault.LIST.equals(type) || ModelDefault.SET.equals(type)) {
                            writeTypeField(generator, toType);
                        } else {
                            // Map or Properties

                            writeTypeField(generator, "object");

                            generator.writeObjectFieldStart("properties");

                            XmlAssociationMetadata xmlAssociationMetadata = (XmlAssociationMetadata)
                                    modelAssociation.getAssociationMetadata(XmlAssociationMetadata.ID);

                            if (xmlAssociationMetadata.isMapExplode()) {
                                // key
                                generator.writeObjectFieldStart("key");
                                writeTypeField(generator, "string");
                                generator.writeEndObject();

                                // value
                                generator.writeObjectFieldStart("value");
                                writeTypeField(generator, toType);
                                generator.writeEndObject();

                                // properties
                                generator.writeEndObject();

                                // required field
                                generator.writeArrayFieldStart("required");
                                generator.writeString("key");
                                generator.writeString("value");
                                generator.writeEndArray();
                            } else {
                                generator.writeObjectFieldStart("*");
                                writeTypeField(generator, toType);
                                generator.writeEndObject();
                            }
                        }

                        // items
                        generator.writeEndObject();
                    }
                } else {
                    writeTypeField(generator, modelField.getType());
                }

                generator.writeEndObject();
            }

            if (reference.hasSuperClass()) {
                reference = reference.getModel().getClass(reference.getSuperClass(), getGeneratedVersion());
            } else {
                reference = null;
            }
        }

        // end of `properties` element
        generator.writeEndObject();

        // write `required` sequence
        if (!required.isEmpty()) {
            generator.writeArrayFieldStart("required");

            for (String requiredField : required) {
                generator.writeString(requiredField);
            }

            generator.writeEndArray();
        }

        // end definition
        if (!isRoot) {
            generator.writeEndObject();
        }
    }

    private static void writeDescriptionField(JsonGenerator generator, String description) throws IOException {
        if (!StringUtils.isEmpty(description)) {
            generator.writeStringField("description", description);
        }
    }

    private void writeTypeField(JsonGenerator generator, String type) throws IOException {
        if (isClassInModel(type, getModel())) {
            generator.writeStringField("$ref", "#/definitions/" + type);
            return;
        }

        // try to make the input type compliant, as much as possible, to JSON Schema primitive types
        // see http://json-schema.org/latest/json-schema-core.html#anchor8
        if ("boolean".equals(type) || "Boolean".equals(type)) {
            type = "boolean";
        } else if ("int".equals(type) || "Integer".equals(type)) {
            type = "integer";
        } else if ("short".equals(type)
                || "Short".equals(type)
                || "long".equals(type)
                || "Long".equals(type)
                || "double".equals(type)
                || "Double".equals(type)
                || "float".equals(type)
                || "Float".equals(type)) {
            type = "number";
        } else if ("String".equals(type)) {
            type = "string";
        }

        // keep as it is otherwise

        generator.writeStringField("type", type);
    }
}
