package cz.senslog.connector.tools.json;

import org.everit.json.schema.Schema;
import org.everit.json.schema.ValidationException;
import org.everit.json.schema.loader.SchemaLoader;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;

import static java.lang.String.format;
import static java.nio.file.Files.readAllBytes;

/**
 * The class {@code JsonSchema} represents a basic wrapper for {@link Schema}.
 * Provides functionality of creating a new schema and validation an input json according the schema.
 * Input json can start as a list or object.
 *
 * @author Lukas Cerny
 * @version 1.0
 * @since 1.0
 */
public final class JsonSchema {

    /** Instance of loaded schema. */
    private final Schema schema;

    /**
     * Loads the schema from resources.
     * @param schemaName - name of the schema.
     * @return new instance of {@code JsonSchema}.
     * @throws IOException throws if the schema does not exist or can not be loaded.
     */
    public static JsonSchema loadAsResource(String schemaName) throws IOException {
        InputStream inputStream = ClassLoader.getSystemResourceAsStream(schemaName);

        if (inputStream == null) {
            throw new IOException(format("Resource file %s was not found.", schemaName));
        }

        BufferedReader streamReader = new BufferedReader(new InputStreamReader(inputStream));
        String schema = streamReader.lines().collect(Collectors.joining());

        return create(schema);
    }

    /**
     * Loads the schema from file system.
     * @param schemaPath - path of the schema.
     * @return new instance of {@code JsonSchema}.
     * @throws IOException throws if the schema does not exist or can not be loaded.
     */
    public static JsonSchema load(Path schemaPath) throws IOException {
        return create(new String(readAllBytes(schemaPath)));
    }

    /**
     *  Creates and build a new schema.
     * @param jsonSchema - string json schema.
     * @return new instance of {@code JsonSchema}.
     */
    public static JsonSchema create(String jsonSchema) {
        Schema schema = SchemaLoader.builder()
                .schemaJson(new JSONObject(jsonSchema)).build()
                .load().build();
        return new JsonSchema(schema);
    }

    /**
     * Private constructor of the class. Accessible via static method {@link JsonSchema#create(String)}.
     * @param schema - build schema.
     */
    private JsonSchema(Schema schema) {
        this.schema = schema;
    }

    /**
     * Validates input json which starts as an object.
     * @param json - input object json.
     * @param errors - list of errors if json is not valid.
     * @return true - valid, false - invalid.
     */
    public boolean validateJsonObject(String json, List<String> errors) {
        try {
            return validate(new JSONObject(json), errors);
        } catch (JSONException e) {
            if (errors != null) {
                errors.clear();
                errors.add(e.getMessage());
            }
            return false;
        }
    }

    /**
     * Validates input json which starts as an array.
     * @param json - input array json.
     * @param errors - list of errors if json is not valid.
     * @return true - valid, false - invalid.
     */
    public boolean validateJsonArray(String json, List<String> errors) {
        try {
            return validate(new JSONArray(json), errors);
        } catch (JSONException e) {
            if (errors != null) {
                errors.clear();
                errors.add(e.getMessage());
            }
            return false;
        }
    }

    /**
     * Validates input json according to build schema. If input json is not valid,
     * is thrown an exception with messages why json is not valid.
     * @param json - input json.
     * @param errors - list of errors if json is not valid.
     * @return true - valid, false - invalid.
     */
    private boolean validate(Object json, List<String> errors) {
        try {
            schema.validate(json);
            return true;
        } catch (ValidationException e) {
            if (errors != null) {
                errors.clear();
                errors.addAll(e.getAllMessages());
            }
            return false;
        }
    }
}
