/*
 * Copyright 2015 the original author or authors.
 *
 * 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 org.gradle.api.internal.artifacts.ivyservice.dependencysubstitution;

import org.gradle.api.Action;
import org.gradle.api.artifacts.DependencyResolveDetails;
import org.gradle.api.artifacts.DependencySubstitution;
import org.gradle.api.artifacts.DependencySubstitutions;
import org.gradle.api.artifacts.ModuleIdentifier;
import org.gradle.api.artifacts.component.ComponentSelector;
import org.gradle.api.artifacts.component.ModuleComponentSelector;
import org.gradle.api.artifacts.component.ProjectComponentSelector;
import org.gradle.api.artifacts.result.ComponentSelectionReason;
import org.gradle.api.initialization.IncludedBuild;
import org.gradle.api.internal.artifacts.DependencySubstitutionInternal;
import org.gradle.api.internal.artifacts.component.ComponentIdentifierFactory;
import org.gradle.api.internal.artifacts.configurations.MutationValidator;
import org.gradle.api.internal.artifacts.ivyservice.resolveengine.result.VersionSelectionReasons;
import org.gradle.internal.Actions;
import org.gradle.internal.component.local.model.DefaultProjectComponentSelector;
import org.gradle.internal.exceptions.DiagnosticsVisitor;
import org.gradle.internal.typeconversion.NotationConvertResult;
import org.gradle.internal.typeconversion.NotationConverter;
import org.gradle.internal.typeconversion.NotationParser;
import org.gradle.internal.typeconversion.NotationParserBuilder;
import org.gradle.internal.typeconversion.TypeConversionException;

import java.util.LinkedHashSet;
import java.util.Set;

public class DefaultDependencySubstitutions implements DependencySubstitutionsInternal {
    private static final NotationParser<Object, ComponentSelector> MODULE_SELECTOR_NOTATION_PARSER = NotationParserBuilder
        .toType(ComponentSelector.class)
        .converter(new ModuleSelectorStringNotationConverter())
        .toComposite();
    private final Set<Action<? super DependencySubstitution>> substitutionRules;
    private final NotationParser<Object, ComponentSelector> moduleSelectorNotationParser;
    private final NotationParser<Object, ComponentSelector> projectSelectorNotationParser;
    private final ComponentSelectionReason reason;

    private MutationValidator mutationValidator = MutationValidator.IGNORE;
    private boolean hasDependencySubstitutionRule;

    public static DefaultDependencySubstitutions forResolutionStrategy(ComponentIdentifierFactory componentIdentifierFactory) {
        NotationParser<Object, ComponentSelector> projectSelectorNotationParser = NotationParserBuilder
            .toType(ComponentSelector.class)
            .fromCharSequence(new ProjectPathConverter(componentIdentifierFactory))
            .toComposite();
        return new DefaultDependencySubstitutions(VersionSelectionReasons.SELECTED_BY_RULE, projectSelectorNotationParser);
    }

    public static DefaultDependencySubstitutions forIncludedBuild(IncludedBuild build) {
        NotationParser<Object, ComponentSelector> projectSelectorNotationParser = NotationParserBuilder
                .toType(ComponentSelector.class)
                .fromCharSequence(new CompositeProjectPathConverter(build))
                .toComposite();
        return new DefaultDependencySubstitutions(VersionSelectionReasons.COMPOSITE_BUILD, projectSelectorNotationParser);
    }

    private DefaultDependencySubstitutions(ComponentSelectionReason reason, NotationParser<Object, ComponentSelector> projectSelectorNotationParser) {
        this(reason, new LinkedHashSet<Action<? super DependencySubstitution>>(), MODULE_SELECTOR_NOTATION_PARSER, projectSelectorNotationParser);
    }

    private DefaultDependencySubstitutions(ComponentSelectionReason reason,
                                           Set<Action<? super DependencySubstitution>> substitutionRules,
                                           NotationParser<Object, ComponentSelector> moduleSelectorNotationParser,
                                           NotationParser<Object, ComponentSelector> projectSelectorNotationParser) {
        this.reason = reason;
        this.substitutionRules = substitutionRules;
        this.moduleSelectorNotationParser = moduleSelectorNotationParser;
        this.projectSelectorNotationParser = projectSelectorNotationParser;
    }

    @Override
    public boolean hasRules() {
        return hasDependencySubstitutionRule;
    }

    @Override
    public Action<DependencySubstitution> getRuleAction() {
        return Actions.composite(substitutionRules);
    }

    private void addRule(Action<? super DependencySubstitution> rule) {
        mutationValidator.validateMutation(MutationValidator.MutationType.STRATEGY);
        substitutionRules.add(rule);
    }

    @Override
    public DependencySubstitutions all(Action<? super DependencySubstitution> rule) {
        addRule(rule);
        hasDependencySubstitutionRule = true;
        return this;
    }

    @Override
    public DependencySubstitutions allWithDependencyResolveDetails(Action<? super DependencyResolveDetails> rule) {
        addRule(new DependencyResolveDetailsWrapperAction(rule));
        return this;
    }

    @Override
    public ComponentSelector module(String notation) {
        return moduleSelectorNotationParser.parseNotation(notation);
    }

    @Override
    public ComponentSelector project(final String path) {
        return projectSelectorNotationParser.parseNotation(path);
    }

    @Override
    public Substitution substitute(final ComponentSelector substituted) {
        return new Substitution() {
            @Override
            public void with(ComponentSelector substitute) {
                DefaultDependencySubstitution.validateTarget(substitute);

                if (substituted instanceof UnversionedModuleComponentSelector) {
                    final ModuleIdentifier moduleId = ((UnversionedModuleComponentSelector) substituted).getModuleIdentifier();
                    all(new ModuleMatchDependencySubstitutionAction(moduleId, substitute));
                } else {
                    all(new ExactMatchDependencySubstitutionAction(substituted, substitute));
                }
            }
        };
    }

    @Override
    public void setMutationValidator(MutationValidator validator) {
        mutationValidator = validator;
    }

    @Override
    public DependencySubstitutionsInternal copy() {
        return new DefaultDependencySubstitutions(
            reason,
            new LinkedHashSet<Action<? super DependencySubstitution>>(substitutionRules),
            moduleSelectorNotationParser,
            projectSelectorNotationParser);
    }

    private static class ProjectPathConverter implements NotationConverter<String, ProjectComponentSelector> {
        private final ComponentIdentifierFactory componentIdentifierFactory;

        private ProjectPathConverter(ComponentIdentifierFactory componentIdentifierFactory) {
            this.componentIdentifierFactory = componentIdentifierFactory;
        }

        @Override
        public void describe(DiagnosticsVisitor visitor) {
            visitor.example("Project paths, e.g. ':api'.");
        }

        @Override
        public void convert(String notation, NotationConvertResult<? super ProjectComponentSelector> result) throws TypeConversionException {
            result.converted(componentIdentifierFactory.createProjectComponentSelector(notation));
        }
    }

    private static class CompositeProjectPathConverter implements NotationConverter<String, ProjectComponentSelector> {
        private final IncludedBuild build;

        private CompositeProjectPathConverter(IncludedBuild build) {
            this.build = build;
        }

        @Override
        public void describe(DiagnosticsVisitor visitor) {
            visitor.example("Project paths, e.g. ':api'.");
        }

        @Override
        public void convert(String notation, NotationConvertResult<? super ProjectComponentSelector> result) throws TypeConversionException {
            result.converted(DefaultProjectComponentSelector.newSelector(build, notation));
        }
    }

    private class ExactMatchDependencySubstitutionAction implements Action<DependencySubstitution> {
        private final ComponentSelector substituted;
        private final ComponentSelector substitute;

        public ExactMatchDependencySubstitutionAction(ComponentSelector substituted, ComponentSelector substitute) {
            this.substituted = substituted;
            this.substitute = substitute;
        }

        @Override
        public void execute(DependencySubstitution dependencySubstitution) {
            if (substituted.equals(dependencySubstitution.getRequested())) {
                ((DependencySubstitutionInternal) dependencySubstitution).useTarget(substitute, reason);
            }
        }
    }

    private class ModuleMatchDependencySubstitutionAction implements Action<DependencySubstitution> {
        private final ModuleIdentifier moduleId;
        private final ComponentSelector substitute;

        public ModuleMatchDependencySubstitutionAction(ModuleIdentifier moduleId, ComponentSelector substitute) {
            this.moduleId = moduleId;
            this.substitute = substitute;
        }

        @Override
        public void execute(DependencySubstitution dependencySubstitution) {
            if (dependencySubstitution.getRequested() instanceof ModuleComponentSelector) {
                ModuleComponentSelector requested = (ModuleComponentSelector) dependencySubstitution.getRequested();
                if (moduleId.getGroup().equals(requested.getGroup()) && moduleId.getName().equals(requested.getModule())) {
                    ((DependencySubstitutionInternal) dependencySubstitution).useTarget(substitute, reason);
                }
            }
        }
    }

    private static class DependencyResolveDetailsWrapperAction implements Action<DependencySubstitution> {
        private final Action<? super DependencyResolveDetails> delegate;

        public DependencyResolveDetailsWrapperAction(Action<? super DependencyResolveDetails> delegate) {
            this.delegate = delegate;
        }

        @Override
        public void execute(DependencySubstitution substitution) {
            DefaultDependencyResolveDetails details = new DefaultDependencyResolveDetails((DependencySubstitutionInternal) substitution);
            delegate.execute(details);
        }
    }
}
