/**
 * Copyright 2017 Goldman Sachs.
 * 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.gs.obevo.impl.context;

import java.io.File;
import java.util.Collection;

import com.gs.obevo.api.appdata.ChangeInput;
import com.gs.obevo.api.appdata.ChangeKey;
import com.gs.obevo.api.appdata.Environment;
import com.gs.obevo.api.platform.ChangeAuditDao;
import com.gs.obevo.api.platform.ChangeTypeSemantic;
import com.gs.obevo.api.platform.DeployExecutionDao;
import com.gs.obevo.api.platform.DeployMetrics;
import com.gs.obevo.api.platform.DeployerAppContext;
import com.gs.obevo.api.platform.FileSourceContext;
import com.gs.obevo.api.platform.FileSourceParams;
import com.gs.obevo.api.platform.MainDeployerArgs;
import com.gs.obevo.api.platform.Platform;
import com.gs.obevo.impl.ChangeTypeBehaviorRegistry;
import com.gs.obevo.impl.ChangeTypeBehaviorRegistry.ChangeTypeBehaviorRegistryBuilder;
import com.gs.obevo.impl.ChangesetCreator;
import com.gs.obevo.impl.DefaultDeployerPlugin;
import com.gs.obevo.impl.DeployMetricsCollector;
import com.gs.obevo.impl.DeployMetricsCollectorImpl;
import com.gs.obevo.impl.DeployerPlugin;
import com.gs.obevo.impl.FileSourceReaderStrategy;
import com.gs.obevo.impl.InputSourceReaderStrategy;
import com.gs.obevo.impl.MainDeployer;
import com.gs.obevo.impl.MainInputReader;
import com.gs.obevo.impl.NoOpPostDeployAction;
import com.gs.obevo.impl.PostDeployAction;
import com.gs.obevo.impl.PrepareDbChange;
import com.gs.obevo.impl.changecalc.ChangesetCreatorImpl;
import com.gs.obevo.impl.changesorter.ChangeCommandSorter;
import com.gs.obevo.impl.changesorter.ChangeCommandSorterImpl;
import com.gs.obevo.impl.changetypes.GroupChangeTypeSemantic;
import com.gs.obevo.impl.changetypes.IncrementalChangeTypeSemantic;
import com.gs.obevo.impl.changetypes.RerunnableChangeTypeSemantic;
import com.gs.obevo.impl.graph.GraphEnricher;
import com.gs.obevo.impl.graph.GraphEnricherImpl;
import com.gs.obevo.impl.reader.CachedDbChangeReader;
import com.gs.obevo.impl.reader.DbChangeFileParser;
import com.gs.obevo.impl.reader.DbDirectoryChangesetReader;
import com.gs.obevo.impl.reader.GetChangeType;
import com.gs.obevo.impl.reader.TextMarkupDocumentReader;
import com.gs.obevo.impl.text.TextDependencyExtractor;
import com.gs.obevo.impl.text.TextDependencyExtractorImpl;
import com.gs.obevo.util.inputreader.Credential;
import org.eclipse.collections.api.block.function.Function0;
import org.eclipse.collections.api.block.predicate.Predicate;
import org.eclipse.collections.api.list.ImmutableList;
import org.eclipse.collections.api.map.MutableMap;
import org.eclipse.collections.impl.block.factory.Predicates;
import org.eclipse.collections.impl.collection.mutable.CollectionAdapter;
import org.eclipse.collections.impl.factory.Lists;
import org.eclipse.collections.impl.factory.Maps;

/**
 * Base implementation class for a DeployerAppContext to facilitate creating other new {@link Platform} implementations.
 */
public abstract class AbstractDeployerAppContext<E extends Environment, Self extends DeployerAppContext<E, Self>> implements DeployerAppContext<E, Self> {
    private final MutableMap<String, Object> singletonBeans = Maps.mutable.empty();

    protected Credential credential;
    private File workDir;
    protected E env;
    private FileSourceContext fileSourceContext;

    @Override
    public final Self build() {
        buildDbContext();
        buildFileContext();

        // then everything else, which is already setup by default

        return (Self) this;
    }

    @Override
    public E getEnvironment() {
        return this.env;
    }

    @Override
    public Self setEnvironment(E env) {
        this.env = env;
        return (Self) this;
    }

    private Credential getCredential() {
        return this.credential;
    }

    @Override
    public Self setCredential(Credential credential) {
        this.credential = credential;
        return (Self) this;
    }

    @Override
    public File getWorkDir() {
        return this.workDir;
    }

    @Override
    public Self setWorkDir(File workDir) {
        this.workDir = workDir;
        return (Self) this;
    }

    @Override
    public Self setFileSourceContext(FileSourceContext fileSourceContext) {
        this.fileSourceContext = fileSourceContext;
        return (Self) this;
    }

    @Override
    public Self deploy() {
        return this.deploy(new MainDeployerArgs());
    }

    @Override
    public Self deploy(MainDeployerArgs deployerArgs) {
        FileSourceParams params = getFileSourceParams(deployerArgs.isUseBaseline());
        getDeployer().execute(env, getSourceReaderStrategy(params), deployerArgs);
        return (Self) this;
    }

    protected FileSourceParams getFileSourceParams(boolean baseline) {
        return FileSourceParams.newBuilder()
                    .setSchemaNames(env.getSchemaNames())
                    .setFiles(env.getSourceDirs())
                    .setBaseline(baseline)
                    .setAcceptedExtensions(env.getAcceptedExtensions())
                    .setChangeTypes(env.getPlatform().getChangeTypes())
                    .setDefaultSourceEncoding(env.getSourceEncoding())
                    .setLegacyDirectoryStructureEnabled(env.isLegacyDirectoryStructureEnabled())
                    .build();
    }

    protected FileSourceReaderStrategy getSourceReaderStrategy(FileSourceParams params) {
        FileSourceContext source = fileSourceContext != null ? fileSourceContext : getDefaultFileSourceContext();
        return new FileSourceReaderStrategy(source, params);
    }

    @Override
    public Self deploy(Collection<ChangeInput> changes) {
        return this.deploy(changes, new MainDeployerArgs());
    }

    @Override
    public Self deploy(Collection<ChangeInput> changes, MainDeployerArgs deployerArgs) {
        getDeployer().execute(env, new InputSourceReaderStrategy(CollectionAdapter.wrapList(changes).toImmutable()), deployerArgs);
        return (Self) this;
    }

    protected <T> T singleton(String beanName, Function0<T> func) {
        Object bean = this.singletonBeans.get(beanName);
        if (bean == null) {
            bean = func.value();
            this.singletonBeans.put(beanName, bean);
        }
        return (T) bean;
    }

    protected PostDeployAction getPostDeployAction() {
        return new NoOpPostDeployAction();
    }

    private ChangeCommandSorter changeCommandSorter() {
        return new ChangeCommandSorterImpl(env.getPlatform());
    }

    protected ChangesetCreator getChangesetCreator() {
        return this.singleton("getChangesetCreator", new Function0<ChangesetCreatorImpl>() {
            @Override
            public ChangesetCreatorImpl value() {
                return new ChangesetCreatorImpl(AbstractDeployerAppContext.this.changeCommandSorter(), AbstractDeployerAppContext.this.getChangeTypeBehaviorRegistry());
            }
        });
    }

    protected ChangeTypeBehaviorRegistry getChangeTypeBehaviorRegistry() {
        return this.singleton("getDbChangeReader", new Function0<ChangeTypeBehaviorRegistry>() {
            @Override
            public ChangeTypeBehaviorRegistry value() {
                return AbstractDeployerAppContext.this.getChangeTypeBehaviors().build();
            }
        });
    }

    protected DeployMetricsCollector deployStatsTracker() {
        return this.singleton("deployStatsTracker", new Function0<DeployMetricsCollectorImpl>() {
            @Override
            public DeployMetricsCollectorImpl value() {
                return new DeployMetricsCollectorImpl();
            }
        });
    }

    @Override
    public DeployMetrics getDeployMetrics() {
        return deployStatsTracker().getMetrics();
    }

    protected abstract ChangeTypeBehaviorRegistryBuilder getChangeTypeBehaviors();

    private MainDeployer getDeployer() {
        return this.singleton("deployer", new Function0<MainDeployer>() {
            @Override
            public MainDeployer value() {
                return new MainDeployer(
                        AbstractDeployerAppContext.this.getArtifactDeployerDao()
                        , AbstractDeployerAppContext.this.getInputReader()
                        , AbstractDeployerAppContext.this.getChangeTypeBehaviorRegistry()
                        , AbstractDeployerAppContext.this.getChangesetCreator()
                        , AbstractDeployerAppContext.this.getPostDeployAction()
                        , AbstractDeployerAppContext.this.deployStatsTracker()
                        , AbstractDeployerAppContext.this.getDeployExecutionDao()
                        , AbstractDeployerAppContext.this.getCredential()
                        , AbstractDeployerAppContext.this.getTextDependencyExtractor()
                        , AbstractDeployerAppContext.this.getDeployerPlugin()
                        , AbstractDeployerAppContext.this.graphEnricher()
                );
            }
        });
    }

    private TextDependencyExtractor getTextDependencyExtractor() {
        return new TextDependencyExtractorImpl(env.getPlatform().convertDbObjectName());
    }

    protected abstract FileSourceContext getDefaultFileSourceContext();

    protected DeployerPlugin<?> getDeployerPlugin() {
        return new DefaultDeployerPlugin();
    }

    protected abstract ChangeAuditDao getArtifactDeployerDao();

    protected abstract DeployExecutionDao getDeployExecutionDao();

    protected Predicate<? super ChangeKey> getDbChangeFilter() {
        return Predicates.alwaysTrue();
    }

    protected ChangeTypeSemantic rerunnableSemantic() {
        return new RerunnableChangeTypeSemantic(graphEnricher());
    }

    protected ChangeTypeSemantic incrementalSemantic() {
        return new IncrementalChangeTypeSemantic(getNumThreads());
    }

    protected ChangeTypeSemantic groupSemantic() {
        return new GroupChangeTypeSemantic(graphEnricher());
    }

    protected int getNumThreads() {
        return 5;
    }

    protected GraphEnricher graphEnricher() {
        return new GraphEnricherImpl(env.getPlatform().convertDbObjectName()::valueOf);
    }

    public static class ReaderContext {
        private final Environment env;
        private final DeployMetricsCollector deployStatsTracker;
        private final GetChangeType getChangeType;
        private final DbChangeFileParser baselineTableChangeParser;

        public ReaderContext(Environment env, DeployMetricsCollector deployStatsTracker, GetChangeType getChangeType, DbChangeFileParser baselineTableChangeParser) {
            this.env = env;
            this.deployStatsTracker = deployStatsTracker;
            this.getChangeType = getChangeType;
            this.baselineTableChangeParser = baselineTableChangeParser;
        }

        public FileSourceContext getDefaultFileSourceContext() {
            int metadataLineReaderVersion = env.getMetadataLineReaderVersion();
            TextMarkupDocumentReader textMarkupDocumentReader = new TextMarkupDocumentReader(metadataLineReaderVersion < 3);  // legacy mode is 2 and below

            FileSourceContext underlyingChangesetReader = new DbDirectoryChangesetReader(env.getPlatform().convertDbObjectName(), deployStatsTracker, true, textMarkupDocumentReader, baselineTableChangeParser, getChangeType);

            return new CachedDbChangeReader(underlyingChangesetReader);
        }
    }

    protected ImmutableList<PrepareDbChange<? super E>> getArtifactTranslators() {
        return Lists.immutable.empty();
    }

    protected final MainInputReader getInputReader() {
        return new MainInputReader(env, getDbChangeFilter(), getArtifactTranslators(), deployStatsTracker());
    }
}