package graphql;

import graphql.execution.ResponseMapFactory;
import graphql.introspection.GoodFaithIntrospection;
import graphql.parser.ParserOptions;
import graphql.schema.PropertyDataFetcherHelper;

import static graphql.Assert.assertNotNull;
import static graphql.execution.instrumentation.dataloader.DataLoaderDispatchingContextKeys.ENABLE_DATA_LOADER_CHAINING;
import static graphql.execution.instrumentation.dataloader.DataLoaderDispatchingContextKeys.ENABLE_DATA_LOADER_EXHAUSTED_DISPATCHING;

/**
 * This allows you to control "unusual" aspects of the GraphQL system
 * including some JVM wide settings and some per execution settings
 * as well as experimental ones.
 * <p>
 * This is named unusual because in general we don't expect you to
 * have to make ths configuration by default, but you can opt into certain features
 * or disable them if you want to.
 */
public class GraphQLUnusualConfiguration {
    GraphQLUnusualConfiguration() {
    }

    /**
     * @return an element that allows you to control JVM wide parsing configuration
     */
    public ParserConfig parsing() {
        return new ParserConfig(this);
    }

    /**
     * @return an element that allows you to control JVM wide {@link graphql.schema.PropertyDataFetcher} configuration
     */
    public PropertyDataFetcherConfig propertyDataFetching() {
        return new PropertyDataFetcherConfig(this);
    }

    /**
     * @return an element that allows you to control JVM wide configuration
     * of {@link graphql.introspection.GoodFaithIntrospection}
     */
    public GoodFaithIntrospectionConfig goodFaithIntrospection() {
        return new GoodFaithIntrospectionConfig(this);
    }

    private static class BaseConfig {
        protected final GraphQLUnusualConfiguration configuration;

        private BaseConfig(GraphQLUnusualConfiguration configuration) {
            this.configuration = configuration;
        }

        /**
         * @return an element that allows you to chain multiple configuration elements
         */
        public GraphQLUnusualConfiguration then() {
            return configuration;
        }
    }

    public static class ParserConfig extends BaseConfig {

        private ParserConfig(GraphQLUnusualConfiguration configuration) {
            super(configuration);
        }

        /**
         * By default, the Parser will not capture ignored characters.  A static holds this default
         * value in a JVM wide basis options object.
         * <p>
         * Significant memory savings can be made if we do NOT capture ignored characters,
         * especially in SDL parsing.
         *
         * @return the static default JVM value
         *
         * @see graphql.language.IgnoredChar
         * @see graphql.language.SourceLocation
         */
        public ParserOptions getDefaultParserOptions() {
            return ParserOptions.getDefaultParserOptions();
        }

        /**
         * By default, the Parser will not capture ignored characters.  A static holds this default
         * value in a JVM wide basis options object.
         * <p>
         * Significant memory savings can be made if we do NOT capture ignored characters,
         * especially in SDL parsing.  So we have set this to false by default.
         * <p>
         * This static can be set to true to allow the behavior of version 16.x or before.
         *
         * @param options - the new default JVM parser options
         *
         * @see graphql.language.IgnoredChar
         * @see graphql.language.SourceLocation
         */
        public ParserConfig setDefaultParserOptions(ParserOptions options) {
            ParserOptions.setDefaultParserOptions(options);
            return this;
        }


        /**
         * By default, for operation parsing, the Parser will not capture ignored characters, and it will not capture line comments into AST
         * elements .  A static holds this default value for operation parsing in a JVM wide basis options object.
         *
         * @return the static default JVM value for operation parsing
         *
         * @see graphql.language.IgnoredChar
         * @see graphql.language.SourceLocation
         */
        public ParserOptions getDefaultOperationParserOptions() {
            return ParserOptions.getDefaultOperationParserOptions();
        }

        /**
         * By default, the Parser will not capture ignored characters or line comments.  A static holds this default
         * value in a JVM wide basis options object for operation parsing.
         * <p>
         * This static can be set to true to allow the behavior of version 16.x or before.
         *
         * @param options - the new default JVM parser options for operation parsing
         *
         * @see graphql.language.IgnoredChar
         * @see graphql.language.SourceLocation
         */
        public ParserConfig setDefaultOperationParserOptions(ParserOptions options) {
            ParserOptions.setDefaultOperationParserOptions(options);
            return this;
        }

        /**
         * By default, for SDL parsing, the Parser will not capture ignored characters, but it will capture line comments into AST
         * elements.  The SDL default options allow unlimited tokens and whitespace, since a DOS attack vector is
         * not commonly available via schema SDL parsing.
         * <p>
         * A static holds this default value for SDL parsing in a JVM wide basis options object.
         *
         * @return the static default JVM value for SDL parsing
         *
         * @see graphql.language.IgnoredChar
         * @see graphql.language.SourceLocation
         * @see graphql.schema.idl.SchemaParser
         */
        public ParserOptions getDefaultSdlParserOptions() {
            return ParserOptions.getDefaultSdlParserOptions();
        }

        /**
         * By default, for SDL parsing, the Parser will not capture ignored characters, but it will capture line comments into AST
         * elements .  A static holds this default value for operation parsing in a JVM wide basis options object.
         * <p>
         * This static can be set to true to allow the behavior of version 16.x or before.
         *
         * @param options - the new default JVM parser options for SDL parsing
         *
         * @see graphql.language.IgnoredChar
         * @see graphql.language.SourceLocation
         */
        public ParserConfig setDefaultSdlParserOptions(ParserOptions options) {
            ParserOptions.setDefaultSdlParserOptions(options);
            return this;
        }
    }

    public static class PropertyDataFetcherConfig extends BaseConfig {
        private PropertyDataFetcherConfig(GraphQLUnusualConfiguration configuration) {
            super(configuration);
        }

        /**
         * PropertyDataFetcher caches the methods and fields that map from a class to a property for runtime performance reasons
         * as well as negative misses.
         * <p>
         * However during development you might be using an assistance tool like JRebel to allow you to tweak your code base and this
         * caching may interfere with this.  So you can call this method to clear the cache.  A JRebel plugin could
         * be developed to do just that.
         */
        @SuppressWarnings("unused")
        public PropertyDataFetcherConfig clearReflectionCache() {
            PropertyDataFetcherHelper.clearReflectionCache();
            return this;
        }

        /**
         * This can be used to control whether PropertyDataFetcher will use {@link java.lang.reflect.Method#setAccessible(boolean)} to gain access to property
         * values.  By default, it PropertyDataFetcher WILL use setAccessible.
         *
         * @param flag whether to use setAccessible
         *
         * @return the previous value of the flag
         */
        public boolean setUseSetAccessible(boolean flag) {
            return PropertyDataFetcherHelper.setUseSetAccessible(flag);
        }

        /**
         * This can be used to control whether PropertyDataFetcher will cache negative lookups for a property for performance reasons.  By default it PropertyDataFetcher WILL cache misses.
         *
         * @param flag whether to cache misses
         *
         * @return the previous value of the flag
         */
        public boolean setUseNegativeCache(boolean flag) {
            return PropertyDataFetcherHelper.setUseNegativeCache(flag);
        }
    }

    public static class GoodFaithIntrospectionConfig extends BaseConfig {
        private GoodFaithIntrospectionConfig(GraphQLUnusualConfiguration configuration) {
            super(configuration);
        }

        /**
         * @return true if good faith introspection is enabled
         */
        public boolean isEnabledJvmWide() {
            return GoodFaithIntrospection.isEnabledJvmWide();
        }

        /**
         * This allows you to disable good faith introspection, which is on by default.
         *
         * @param enabled the desired state
         *
         * @return the previous state
         */
        public GoodFaithIntrospectionConfig enabledJvmWide(boolean enabled) {
            GoodFaithIntrospection.enabledJvmWide(enabled);
            return this;
        }
    }

    /*
     * ===============================================
     * Per GraphqlContext code down here
     * ===============================================
     */

    @SuppressWarnings("DataFlowIssue")
    public static class GraphQLContextConfiguration {
        // it will be one or the other types of GraphQLContext
        private final GraphQLContext graphQLContext;
        private final GraphQLContext.Builder graphQLContextBuilder;

        GraphQLContextConfiguration(GraphQLContext graphQLContext) {
            this.graphQLContext = graphQLContext;
            this.graphQLContextBuilder = null;
        }

        GraphQLContextConfiguration(GraphQLContext.Builder graphQLContextBuilder) {
            this.graphQLContextBuilder = graphQLContextBuilder;
            this.graphQLContext = null;
        }

        /**
         * @return an element that allows you to control incremental support, that is @defer configuration
         */
        public IncrementalSupportConfig incrementalSupport() {
            return new IncrementalSupportConfig(this);
        }

        /**
         * @return an element that allows you to precisely control {@link org.dataloader.DataLoader} behavior
         * in graphql-java.
         */
        public DataloaderConfig dataloaderConfig() {
            return new DataloaderConfig(this);
        }

        /**
         * @return an element that allows you to control the {@link ResponseMapFactory} used
         */
        public ResponseMapFactoryConfig responseMapFactory() {
            return new ResponseMapFactoryConfig(this);
        }

        private void put(String named, Object value) {
            if (graphQLContext != null) {
                graphQLContext.put(named, value);
            } else {
                assertNotNull(graphQLContextBuilder).put(named, value);
            }
        }

        private boolean getBoolean(String named) {
            if (graphQLContext != null) {
                return graphQLContext.getBoolean(named);
            } else {
                return assertNotNull(graphQLContextBuilder).getBoolean(named);
            }
        }

        private <T> T get(String named) {
            if (graphQLContext != null) {
                return graphQLContext.get(named);
            } else {
                //noinspection unchecked
                return (T) assertNotNull(graphQLContextBuilder).get(named);
            }
        }
    }

    private static class BaseContextConfig {
        protected final GraphQLContextConfiguration contextConfig;

        private BaseContextConfig(GraphQLContextConfiguration contextConfig) {
            this.contextConfig = contextConfig;
        }

        /**
         * @return an element that allows you to chain multiple configuration elements
         */
        public GraphQLContextConfiguration then() {
            return contextConfig;
        }
    }

    public static class IncrementalSupportConfig extends BaseContextConfig {
        private IncrementalSupportConfig(GraphQLContextConfiguration contextConfig) {
            super(contextConfig);
        }

        /**
         * @return true if @defer and @stream behaviour is enabled for this execution.
         */
        public boolean isIncrementalSupportEnabled() {
            return contextConfig.getBoolean(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT);
        }

        /**
         * This controls whether @defer and @stream behaviour is enabled for this execution.
         */
        @ExperimentalApi
        public IncrementalSupportConfig enableIncrementalSupport(boolean enable) {
            contextConfig.put(ExperimentalApi.ENABLE_INCREMENTAL_SUPPORT, enable);
            return this;
        }
    }

    public static class DataloaderConfig extends BaseContextConfig {
        private DataloaderConfig(GraphQLContextConfiguration contextConfig) {
            super(contextConfig);
        }

        /**
         * returns true if chained data loader dispatching is enabled
         */
        public boolean isDataLoaderChainingEnabled() {
            return contextConfig.getBoolean(ENABLE_DATA_LOADER_CHAINING);
        }

        public boolean isDataLoaderExhaustedDispatchingEnabled() {
            return contextConfig.get(ENABLE_DATA_LOADER_EXHAUSTED_DISPATCHING);
        }

        /**
         * Enables the ability that chained DataLoaders are dispatched automatically.
         */
        @ExperimentalApi
        public DataloaderConfig enableDataLoaderChaining(boolean enable) {
            contextConfig.put(ENABLE_DATA_LOADER_CHAINING, enable);
            return this;
        }

        /**
         * Enables a dispatching strategy that will dispatch as long as there is no
         * other data fetcher or batch loader running.
         */
        @ExperimentalApi
        public DataloaderConfig enableDataLoaderExhaustedDispatching(boolean enable) {
            contextConfig.put(ENABLE_DATA_LOADER_EXHAUSTED_DISPATCHING, enable);
            return this;
        }


    }

    public static class ResponseMapFactoryConfig extends BaseContextConfig {
        private ResponseMapFactoryConfig(GraphQLContextConfiguration contextConfig) {
            super(contextConfig);
        }

        /**
         * @return the {@link ResponseMapFactory} in play - this can be null
         */
        @ExperimentalApi
        public ResponseMapFactory getOr(ResponseMapFactory defaultFactory) {
            ResponseMapFactory responseMapFactory = contextConfig.get(ResponseMapFactory.class.getCanonicalName());
            return responseMapFactory != null ? responseMapFactory : defaultFactory;
        }

        /**
         * This controls the {@link ResponseMapFactory} to use for this request
         */
        @ExperimentalApi
        public ResponseMapFactoryConfig setFactory(ResponseMapFactory factory) {
            contextConfig.put(ResponseMapFactory.class.getCanonicalName(), factory);
            return this;
        }
    }
}
