/*
 * Licensed 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 com.qubole.presto.kinesis;

import com.facebook.presto.connector.ConnectorId;
import com.facebook.presto.spi.ConnectorHandleResolver;
import com.facebook.presto.spi.NodeManager;
import com.facebook.presto.spi.SchemaTableName;
import com.facebook.presto.spi.connector.Connector;
import com.facebook.presto.spi.connector.ConnectorContext;
import com.facebook.presto.spi.connector.ConnectorFactory;
import com.facebook.presto.spi.type.TypeManager;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import io.airlift.bootstrap.Bootstrap;
import io.airlift.json.JsonModule;
import io.airlift.log.Logger;

import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;

/**
 *
 * This factory class creates the KinesisConnector during server start and binds all the dependency
 * by calling create() method.
 */
public class KinesisConnectorFactory
        implements ConnectorFactory
{
    public static final String connectorName = "kinesis";
    private static final Logger log = Logger.get(KinesisConnectorFactory.class);

    private final ClassLoader classLoader;
    private TypeManager typeManager;
    private NodeManager nodeManager;
    private Optional<Supplier<Map<SchemaTableName, KinesisStreamDescription>>> tableDescriptionSupplier = Optional.empty();
    private Map<String, String> optionalConfig = ImmutableMap.of();
    private Optional<Class<? extends KinesisClientProvider>> altProviderClass = Optional.empty();

    private KinesisHandleResolver handleResolver;

    private Injector injector;

    KinesisConnectorFactory(ClassLoader classLoader, Optional<Supplier<Map<SchemaTableName, KinesisStreamDescription>>> tableDescriptionSupplier,
                            Map<String, String> optionalConfig,
                            Optional<Class<? extends KinesisClientProvider>> altProviderClass)
    {
        this.classLoader = classLoader;
        this.tableDescriptionSupplier = requireNonNull(tableDescriptionSupplier, "tableDescriptionSupplier is null");
        this.optionalConfig = requireNonNull(optionalConfig, "optionalConfig is null");
        this.altProviderClass = requireNonNull(altProviderClass, "altProviderClass is null");

        this.handleResolver = new KinesisHandleResolver(connectorName);

        // Explanation: AWS uses a newer version of jackson (2.6.6) than airlift (2.4.4).  In order to upgrade
        // to the latest version of the AWS API, we need to turn this feature off.  This can be set
        // in jvm.properties but trying to make this more foolproof.
        System.setProperty("com.amazonaws.sdk.disableCbor", "true");
    }

    @Override
    public String getName()
    {
        return connectorName;
    }

    @Override
    public ConnectorHandleResolver getHandleResolver()
    {
        return new KinesisHandleResolver(connectorName);
    }

    @Override
    public Connector create(String connectorId, Map<String, String> config, ConnectorContext context)
    {
        log.info("In connector factory create method.  Connector id: " + connectorId);
        requireNonNull(connectorId, "connectorId is null");
        requireNonNull(config, "config is null");

        try {
            Bootstrap app = new Bootstrap(
                    new JsonModule(),
                    new KinesisConnectorModule(),
                    binder -> {
                        binder.bindConstant().annotatedWith(Names.named("connectorId")).to(connectorId);
                        binder.bind(ConnectorId.class).toInstance(new ConnectorId(connectorId));
                        binder.bind(TypeManager.class).toInstance(context.getTypeManager());
                        binder.bind(NodeManager.class).toInstance(context.getNodeManager());
                        // Note: moved creation from KinesisConnectorModule because connector manager accesses it earlier!
                        binder.bind(KinesisHandleResolver.class).toInstance(new KinesisHandleResolver(connectorName));

                        // Moved creation here from KinesisConnectorModule to make it easier to parameterize
                        if (altProviderClass.isPresent()) {
                            binder.bind(KinesisClientProvider.class).to(altProviderClass.get()).in(Scopes.SINGLETON);
                        }
                        else {
                            binder.bind(KinesisClientProvider.class).to(KinesisClientManager.class).in(Scopes.SINGLETON);
                        }

                        if (tableDescriptionSupplier.isPresent()) {
                            binder.bind(new TypeLiteral<Supplier<Map<SchemaTableName, KinesisStreamDescription>>>() {}).toInstance(tableDescriptionSupplier.get());
                        }
                        else {
                            binder.bind(new TypeLiteral<Supplier<Map<SchemaTableName, KinesisStreamDescription>>>() {}).to(KinesisTableDescriptionSupplier.class).in(Scopes.SINGLETON);
                        }
                    }
            );

            this.injector = app.strictConfig()
                        .doNotInitializeLogging()
                        .setRequiredConfigurationProperties(config)
                        .setOptionalConfigurationProperties(optionalConfig)
                        .initialize();

            KinesisConnector connector = this.injector.getInstance(KinesisConnector.class);

            // Register objects for shutdown, at the moment only KinesisTableDescriptionSupplier
            if (!tableDescriptionSupplier.isPresent()) {
                // This will shutdown related dependent objects as well:
                KinesisTableDescriptionSupplier supp = getTableDescSupplier(this.injector);
                connector.registerShutdownObject(supp);
            }

            log.info("Done with injector.  Returning the connector itself.");
            return connector;
        }
        catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Convenience method to get the table description supplier.
     *
     * @param inj Injector for table description supplier
     * @return Returns Table Description Supplier for kinesis table
     */
    protected KinesisTableDescriptionSupplier getTableDescSupplier(Injector inj)
    {
        requireNonNull(inj, "Injector is missing in getTableDescSupplier");
        Supplier<Map<SchemaTableName, KinesisStreamDescription>> supplier =
                inj.getInstance(Key.get(new TypeLiteral<Supplier<Map<SchemaTableName, KinesisStreamDescription>>>() {}));
        requireNonNull(inj, "Injector cannot find any table description supplier");
        return (KinesisTableDescriptionSupplier) supplier;
    }

    protected Injector getInjector()
    {
        return this.injector;
    }
}