/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.maven.model.building;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.maven.model.Model;
import org.apache.maven.model.building.DefaultTransformerContext.GAKey;
import org.apache.maven.model.building.DefaultTransformerContext.Holder;

/**
 * Builds up the transformer context.
 * After the buildplan is ready, the build()-method returns the immutable context useful during distribution.
 * This is an inner class, as it must be able to call readRawModel()
 *
 * @since 4.0.0
 */
class DefaultTransformerContextBuilder implements TransformerContextBuilder {
    private final Graph dag = new Graph();
    private final DefaultModelBuilder defaultModelBuilder;
    private final DefaultTransformerContext context;

    private final Map<String, Set<FileModelSource>> mappedSources = new ConcurrentHashMap<>(64);

    DefaultTransformerContextBuilder(DefaultModelBuilder defaultModelBuilder) {
        this.defaultModelBuilder = defaultModelBuilder;
        this.context = new DefaultTransformerContext(defaultModelBuilder.getModelProcessor());
    }

    /**
     * If an interface could be extracted, DefaultModelProblemCollector should be ModelProblemCollectorExt
     */
    @Override
    public TransformerContext initialize(ModelBuildingRequest request, ModelProblemCollector collector) {
        // We must assume the TransformerContext was created using this.newTransformerContextBuilder()
        DefaultModelProblemCollector problems = (DefaultModelProblemCollector) collector;
        return new TransformerContext() {

            private volatile boolean fullReactorLoaded;

            @Override
            public Path locate(Path path) {
                return context.locate(path);
            }

            @Override
            public String getUserProperty(String key) {
                return context.userProperties.computeIfAbsent(
                        key, k -> request.getUserProperties().getProperty(key));
            }

            @Override
            public Model getRawModel(Path from, String gId, String aId) {
                Model model = findRawModel(from, gId, aId);
                if (model != null) {
                    context.modelByGA.put(new GAKey(gId, aId), new Holder(model));
                    context.modelByPath.put(model.getPomFile().toPath(), new Holder(model));
                }
                return model;
            }

            @Override
            public Model getRawModel(Path from, Path path) {
                Model model = findRawModel(from, path);
                if (model != null) {
                    String groupId = defaultModelBuilder.getGroupId(model);
                    context.modelByGA.put(
                            new DefaultTransformerContext.GAKey(groupId, model.getArtifactId()), new Holder(model));
                    context.modelByPath.put(path, new Holder(model));
                }
                return model;
            }

            private Model findRawModel(Path from, String groupId, String artifactId) {
                FileModelSource source = getSource(groupId, artifactId);
                if (source == null) {
                    // we need to check the whole reactor in case it's a dependency
                    loadFullReactor();
                    source = getSource(groupId, artifactId);
                }
                if (source != null) {
                    if (!addEdge(from, source.getFile().toPath(), problems)) {
                        return null;
                    }
                    try {
                        ModelBuildingRequest gaBuildingRequest =
                                new DefaultModelBuildingRequest(request).setModelSource(source);
                        return defaultModelBuilder.readRawModel(gaBuildingRequest, problems);
                    } catch (ModelBuildingException e) {
                        // gathered with problem collector
                    }
                }
                return null;
            }

            private void loadFullReactor() {
                if (!fullReactorLoaded) {
                    synchronized (this) {
                        if (!fullReactorLoaded) {
                            doLoadFullReactor();
                            fullReactorLoaded = true;
                        }
                    }
                }
            }

            private void doLoadFullReactor() {
                Path rootDirectory = request.getRootDirectory();
                if (rootDirectory == null) {
                    return;
                }
                List<File> toLoad = new ArrayList<>();
                File root = defaultModelBuilder.getModelProcessor().locateExistingPom(rootDirectory.toFile());
                toLoad.add(root);
                while (!toLoad.isEmpty()) {
                    File pom = toLoad.remove(0);
                    try {
                        ModelBuildingRequest gaBuildingRequest =
                                new DefaultModelBuildingRequest(request).setModelSource(new FileModelSource(pom));
                        Model rawModel = defaultModelBuilder.readFileModel(gaBuildingRequest, problems);
                        for (String module : rawModel.getModules()) {
                            File moduleFile = defaultModelBuilder
                                    .getModelProcessor()
                                    .locateExistingPom(new File(pom.getParent(), module));
                            if (moduleFile != null) {
                                toLoad.add(moduleFile);
                            }
                        }
                    } catch (ModelBuildingException e) {
                        // gathered with problem collector
                    }
                }
            }

            private Model findRawModel(Path from, Path p) {
                if (!Files.isRegularFile(p)) {
                    throw new IllegalArgumentException("Not a regular file: " + p);
                }

                if (!addEdge(from, p, problems)) {
                    return null;
                }

                DefaultModelBuildingRequest req = new DefaultModelBuildingRequest(request)
                        .setPomFile(p.toFile())
                        .setModelSource(new FileModelSource(p.toFile()));

                try {
                    return defaultModelBuilder.readRawModel(req, problems);
                } catch (ModelBuildingException e) {
                    // gathered with problem collector
                }
                return null;
            }
        };
    }

    private boolean addEdge(Path from, Path p, DefaultModelProblemCollector problems) {
        try {
            synchronized (dag) {
                dag.addEdge(dag.addVertex(from.toString()), dag.addVertex(p.toString()));
            }
            return true;
        } catch (Graph.CycleDetectedException e) {
            problems.add(new DefaultModelProblem(
                    "Cycle detected between models at " + from + " and " + p,
                    ModelProblem.Severity.FATAL,
                    null,
                    null,
                    0,
                    0,
                    null,
                    e));
            return false;
        }
    }

    @Override
    public TransformerContext build() {
        return context;
    }

    public FileModelSource getSource(String groupId, String artifactId) {
        Set<FileModelSource> sources = mappedSources.get(groupId + ":" + artifactId);
        if (sources == null) {
            return null;
        }
        return sources.stream()
                .reduce((a, b) -> {
                    throw new IllegalStateException(String.format(
                            "No unique Source for %s:%s: %s and %s",
                            groupId, artifactId, a.getLocation(), b.getLocation()));
                })
                .orElse(null);
    }

    public void putSource(String groupId, String artifactId, FileModelSource source) {
        mappedSources
                .computeIfAbsent(groupId + ":" + artifactId, k -> new HashSet<>())
                .add(source);
    }
}
