
    //--------------------------------------------------------------
    //---------QueryBuilder Inner classes---------------------------
    //--------------------------------------------------------------

    /**
     * GraphQL Query builder.
     * Generates a GraphQL query string from a <@code>Function</@code>
     */
    public static class GQLQuery {

        private final ObjectMapper mapper = new ObjectMapper();
        private GQLFunction function;

        private GQLQuery() {
            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        }

        public static GQLQuery from(GQLFunction function) {
            GQLQuery query = new GQLQuery();
            query.function = function;
            return query;
        }

        public String toString() {
            String query =
                    "\"" + function.getType().getName() + "(" + function.getArguments().toQueryArgumentsString() + ")" +
                            "{ " + function.getName() + "( " + function.getArguments().toMethodArgumentsString() + ")" +
                            "{ " + function.getFragment().toString() + "} }\"";

            ObjectNode rootNode = mapper.createObjectNode();
            rootNode.putRawValue("operationName", null);
            rootNode.putRawValue("query", new RawValue(query));
            rootNode.putPOJO("variables", function.getArguments().toVariables());

            return rootNode.toPrettyString();
        }

        public TypeReference<?> getReturnType() {
            return function.getRturnType();
        }

        public String getName() {
            return function.getName();
        }

    }


    /**
     * Represents a function argument, and it's details.
     */
    public static class Argument {
        private String name;
        private String type;
        private Object value;
        private boolean optional;
        private boolean ignore;

        private Argument() {
        }

        private Argument(String name, Object value) {
            if (value == null) {//mark argument as ignorable
                ignore = true;
                return;

            } else if ((value instanceof Optional)) {
                if (!((Optional<?>) value).isPresent()) {
                    ignore = true;
                    return;
                }
                this.optional = true;
                value = ((Optional<?>) value).get();
            }

            this.name = name;
            this.value = value;
            this.type = this.value.getClass().getSimpleName();
        }

        public static Argument of(String name, Object value) {
            return new Argument(name, value);
        }

        public String getName() {
            return name;
        }

        public Object getValue() {
            return value;
        }

        public String getType() {
            return type;
        }

        public boolean isOptional() {
            return optional;
        }

        public boolean isIgnore() {
            return ignore;
        }
    }


    /**
     * A wrapper for the function arguments.
     */
    public static class Arguments {
        private LinkedHashSet<Argument> arguments = new LinkedHashSet<>();

        private Arguments() {
        }

        private void add(Argument argument) {
            this.arguments.add(argument);
        }

        /**
         * Builds the arguments for graphql FUNCTION.
         * Ex: getUser(firstName: $firstName, lastName: $lastName)...
         *
         * @return graphql function arguments with parameters as variables.
         */
        public String toMethodArgumentsString() {
            StringBuilder sb = new StringBuilder();
            arguments.forEach(arg -> {
                if (!arg.isIgnore()) { //do not build arg if marked as ignorable
                    sb.append(arg.getName()).append(": $").append(arg.getName()).append(", ");
                }
            });
            sb.deleteCharAt(sb.lastIndexOf(","));
            return sb.toString();
        }

        /**
         * Builds the arguments for graphql QUERY type.
         * Ex: mutation($firstName: String, $lastName: String)...
         *
         * @return graphql query arguments with type.
         */
        public String toQueryArgumentsString() {
            StringBuilder sb = new StringBuilder();

            arguments.forEach(arg -> {
                if (!arg.isIgnore()) {
                    sb.append("$")
                            .append(arg.getName())
                            .append(": ")
                            .append(arg.getType())
                            .append(arg.isOptional() ? "" : "!")
                            .append(", ");
                }
            });
            sb.deleteCharAt(sb.lastIndexOf(","));
            return sb.toString();
        }

        /**
         * Builds a map of variables that can be passed to GQL query in the variables JSON element.
         *
         * @return a map of variables.
         */
        public HashMap<String, Object> toVariables() {
            HashMap<String, Object> variables = new HashMap<>();
            arguments.forEach(arg -> {
                if (!arg.isIgnore()) {
                    variables.put(arg.getName(), arg.getValue());
                }
            });

            return variables;
        }
    }


    /**
     * Represents a filed information from a GraphQL fragment.
     */
    public static class FragmentField {
        private String name;
        private LinkedHashSet<FragmentField> fieldList = new LinkedHashSet<>();

        private FragmentField() {
        }

        public static FragmentField of(String name) {
            FragmentField fragmentField = new FragmentField();
            fragmentField.name = name;
            return fragmentField;
        }

        public static FragmentField of(String name, FragmentField... fields) {
            FragmentField fragmentField = new FragmentField();
            fragmentField.name = name;
            fragmentField.fieldList.addAll(Arrays.asList(fields));
            return fragmentField;
        }

        public String getName() {
            return name;
        }

        public LinkedHashSet<FragmentField> getFieldList() {
            return fieldList;
        }
    }


    /**
     * GraphQL function types.
     */
    public enum GQLFunctionType {
        Query("query"),
        Mutation("mutation");

        private final String name;

        GQLFunctionType(final String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }


    /**
     * This is a wrapper that represent a GraphQL function.
     * It contains all the information in order to build a query.
     */
    public static class GQLFunction {
        private TypeReference<?> resultType;
        private GQLFunctionType type;
        private String name;
        private Arguments arguments;
        private ResultFragment resultFragment;

        private GQLFunction() {
        }

        public GQLFunction(GQLFunctionType type, String name) {
            this.name = name;
            this.type = type;
        }

        public GQLFunction returnType(TypeReference<?> resultType) {
            this.resultType = resultType;
            return this;
        }

        public GQLFunction arguments(Arguments arguments) {
            this.arguments = arguments;
            return this;
        }

        public GQLFunction resultFragment(ResultFragment fragment) {
            this.resultFragment = fragment;
            return this;
        }


        public TypeReference<?> getRturnType() {
            return resultType;
        }

        public String getName() {
            return name;
        }

        public Arguments getArguments() {
            return arguments;
        }

        public ResultFragment getFragment() {
            return resultFragment;
        }

        public GQLFunctionType getType() {
            return type;
        }
    }


    /**
     * GraphQL expected result type after the query is executed.
     * Note: This is pure informatory - as this type may or not be used by the user after the HTTP call.
     * However, it is useful to know the expected result type of <@code>Function</@code> that we want to execute.
     */
    public static class ResultFragment {
        private LinkedHashSet<FragmentField> fields = new LinkedHashSet<>();

        private ResultFragment() {
        }

        private ResultFragment add(FragmentField field) {
            this.fields.add(field);
            return this;
        }

        public String toString() {
            return getFieldString(fields);
        }

        private String getFieldString(LinkedHashSet<FragmentField> fields) {
            StringBuilder sb = new StringBuilder();
            fields.forEach(field -> {
                sb.append(field.getName()).append(" ");
                if (field.getFieldList() != null && !field.getFieldList().isEmpty()) {
                    sb.append("{ ").append(getFieldString(field.getFieldList())).append(" } ");
                }
            });
            return sb.toString();
        }
    }
