package org.echocat.gradle.plugins.golang.model;

import groovy.lang.Closure;
import groovy.lang.MissingMethodException;
import org.echocat.gradle.plugins.golang.vcs.VcsType;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.util.CollectionUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.echocat.gradle.plugins.golang.Constants.VENDOR_DIRECTORY_NAME;
import static org.echocat.gradle.plugins.golang.vcs.VcsType.git;
import static org.gradle.util.ConfigureUtil.configure;

public class DependenciesSettings {

    protected static final Pattern NOTATION_PATTERN = Pattern.compile("(?<group>[a-zA-Z0-9.\\-_/]+)(?::(?<version>[a-zA-Z0-9.\\-_/]+))?");

    private final boolean _root;

    @Nonnull
    private final Project _project;

    private Boolean _forceUpdate;
    private Boolean _deleteUnknownDependencies;
    private Boolean _deleteAllCachedDependenciesOnClean;
    private Path _dependencyCache;

    private Collection<VcsRepositoryProvider> _vcsRepositoryProviders;

    @Inject
    public DependenciesSettings(boolean root, @Nonnull Project project) {
        _root = root;
        _project = project;
        if (root) {
            _dependencyCache = project.getProjectDir().toPath().resolve(VENDOR_DIRECTORY_NAME);
            _deleteUnknownDependencies = true;
        }
    }

    public Boolean getForceUpdate() {
        return _forceUpdate;
    }

    public void setForceUpdate(Boolean forceUpdate) {
        _forceUpdate = forceUpdate;
    }

    public Boolean getDeleteUnknownDependencies() {
        return _deleteUnknownDependencies;
    }

    public void setDeleteUnknownDependencies(Boolean deleteUnknownDependencies) {
        _deleteUnknownDependencies = deleteUnknownDependencies;
    }

    public Boolean getDeleteAllCachedDependenciesOnClean() {
        return _deleteAllCachedDependenciesOnClean;
    }

    public void setDeleteAllCachedDependenciesOnClean(Boolean deleteAllCachedDependenciesOnClean) {
        _deleteAllCachedDependenciesOnClean = deleteAllCachedDependenciesOnClean;
    }

    public Path getDependencyCache() {
        return _dependencyCache;
    }

    public void setDependencyCache(Path dependencyCache) {
        _dependencyCache = dependencyCache;
    }

    public Collection<VcsRepositoryProvider> getVcsRepositoryProviders() {
        return _vcsRepositoryProviders;
    }

    public void setVcsRepositoryProviders(Collection<VcsRepositoryProvider> vcsRepositoryProviders) {
        _vcsRepositoryProviders = vcsRepositoryProviders;
    }

    @Nonnull
    public VcsRepositoryProvider vcsRepositoryProvider(@Nonnull String prefix, @Nonnull String name, @Nonnull String dependencyPattern) {
        return vcsRepositoryProvider(prefix, name, Pattern.compile(dependencyPattern));
    }

    @Nonnull
    public VcsRepositoryProvider vcsRepositoryProvider(@Nonnull String prefix, @Nonnull String name, @Nonnull Pattern dependencyPattern) {
        return vcsRepositoryProvider(git, prefix, name, dependencyPattern);
    }

    @Nonnull
    public VcsRepositoryProvider vcsRepositoryProvider(@Nonnull String type, @Nonnull String prefix, @Nonnull String name, @Nonnull String dependencyPattern) {
        return vcsRepositoryProvider(VcsType.valueOf(type), prefix, name, Pattern.compile(dependencyPattern));
    }

    @Nonnull
    public VcsRepositoryProvider vcsRepositoryProvider(@Nonnull VcsType type, @Nonnull String prefix, @Nonnull String name, @Nonnull Pattern dependencyPattern) {
        final VcsRepositoryProvider result = new VcsRepositoryProvider(type, prefix, name, dependencyPattern);
        add(result);
        return result;
    }

    public void add(@Nonnull VcsRepositoryProvider vcsRepositoryProvider) {
        if (_vcsRepositoryProviders == null) {
            _vcsRepositoryProviders = new ArrayList<>();
        }
        _vcsRepositoryProviders.add(vcsRepositoryProvider);
    }

    @Nonnull
    public Dependency add(@Nonnull String configurationName, @Nonnull Object notation) {
        return add(configurationName, notation, null);
    }

    @Nonnull
    public Dependency add(@Nonnull String configurationName, @Nonnull Object notation, @Nullable Closure<?> configureWith) {
        return doAdd(configurationName, notation, configureWith);
    }

    @Nonnull
    public Dependency create(@Nonnull String configurationName, @Nonnull Object notation) {
        return create(configurationName, notation, null);
    }

    @Nonnull
    public Dependency create(@Nonnull String configurationName, @Nonnull Object notation, @Nullable Closure<?> configureWith) {
        return doAdd(configurationName, notation, configureWith);
    }

    @Nonnull
    protected Dependency doAdd(@Nonnull String configurationName, @Nonnull Object notation, @Nullable Closure<?> configureWith) {
        if (notation instanceof String) {
            return doAdd(configurationName, (String) notation, configureWith);
        }
        if (notation instanceof Map) {
            //noinspection unchecked,rawtypes
            return doAdd(configurationName, (Map) notation, configureWith);
        }
        throw new IllegalArgumentException("Could not handle notation of type: " + notation.getClass().getName());
    }

    @Nonnull
    protected Dependency doAdd(@Nonnull String configurationName, @Nonnull String notation, @Nullable Closure<?> configureWith) {
        final Matcher matcher = NOTATION_PATTERN.matcher(notation);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("Illegal dependency notation provided: " + notation);
        }
        final String group = matcher.group("group");
        final String version = matcher.group("version");
        return doAdd(configurationName, new GolangDependency()
                .setGroup(group)
                .setVersion(version)
            , configureWith);
    }

    @Nonnull
    protected Dependency doAdd(@Nonnull String configurationName, @Nonnull Map<String, Object> arguments, @Nullable Closure<?> configureWith) {
        final String group = requiredArgument(arguments, "group");
        final String version = argument(arguments, "version");
        final URI repositoryUri = uriArgument(arguments, "repositoryUri");
        final VcsType repositoryType = vcsTypeArgument(arguments, "repositoryType");
        final UpdatePolicy updatePolicy = updatePolicyArgument(arguments, "updatePolicy");
        return doAdd(configurationName, new GolangDependency()
                .setGroup(group)
                .setVersion(version)
                .setRepositoryUri(repositoryUri)
                .setRepositoryType(repositoryType)
                .setUpdatePolicy(updatePolicy)
            , configureWith);
    }

    @Nonnull
    protected Dependency doAdd(@Nonnull String configurationName, @Nonnull Dependency dependency, @Nullable Closure<?> configureWith) {
        if (!_root) {
            throw new IllegalStateException("Adding of dependencies for golang is currently not support at task level.");
        }
        final Configuration configuration = _project.getConfigurations().getByName(configurationName);
        configuration.getDependencies().add(dependency);
        if (configureWith != null) {
            return configure(configureWith, dependency);
        }
        return dependency;
    }

    @Nullable
    protected String argument(@Nonnull Map<String, Object> arguments, @Nonnull String name) {
        final Object plain = arguments.get(name);
        if (plain == null) {
            return null;
        }
        return plain.toString();
    }

    @Nullable
    protected URI uriArgument(@Nonnull Map<String, Object> arguments, @Nonnull String name) {
        final Object plain = arguments.get(name);
        if (plain == null) {
            return null;
        }
        if (plain instanceof URI) {
            return (URI) plain;
        }
        return URI.create(plain.toString());
    }

    @Nullable
    protected VcsType vcsTypeArgument(@Nonnull Map<String, Object> arguments, @Nonnull String name) {
        final Object plain = arguments.get(name);
        if (plain == null) {
            return null;
        }
        if (plain instanceof VcsType) {
            return (VcsType) plain;
        }
        return VcsType.valueOf(plain.toString());
    }

    @Nonnull
    protected UpdatePolicy updatePolicyArgument(@Nonnull Map<String, Object> arguments, @Nonnull String name) {
        final Object plain = arguments.get(name);
        if (plain instanceof UpdatePolicy) {
            return (UpdatePolicy) plain;
        }
        return UpdatePolicy.valueOf(plain.toString());
    }

    @Nonnull
    protected String requiredArgument(@Nonnull Map<String, Object> arguments, @Nonnull String name) {
        final String result = argument(arguments, name);
        if (result == null) {
            throw new IllegalArgumentException("Required argument '" + name + "' not provided. But got: " + arguments);
        }
        return result;
    }

    @Nullable
    public Object methodMissing(@Nonnull String name, @Nullable Object args) {
        final Object[] argsArray = (Object[]) args;
        final Configuration configuration = _project.getConfigurations().findByName(name);
        if (configuration == null) {
            throw new MissingMethodException(name, this.getClass(), argsArray);
        }
        final List<?> normalizedArgs = CollectionUtils.flattenCollections(argsArray);
        if (normalizedArgs.size() == 2 && normalizedArgs.get(1) instanceof Closure) {
            //noinspection rawtypes
            return doAdd(name, normalizedArgs.get(0), (Closure) normalizedArgs.get(1));
        } else if (normalizedArgs.size() == 1) {
            return doAdd(name, normalizedArgs.get(0), null);
        } else {
            for (final Object arg : normalizedArgs) {
                doAdd(name, arg, null);
            }
            return null;
        }
    }
}