package io.quarkus.bootstrap.resolver.maven.workspace;

import io.quarkus.bootstrap.BootstrapConstants;
import io.quarkus.bootstrap.BootstrapException;
import io.quarkus.bootstrap.model.AppArtifact;
import io.quarkus.bootstrap.model.AppArtifactKey;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.Resource;

/**
 *
 * @author Alexey Loubyansky
 */
public class LocalProject {

    public static final String PROJECT_GROUPID = "${project.groupId}";

    private static final String PROJECT_BASEDIR = "${project.basedir}";
    private static final String POM_XML = "pom.xml";
    private static final String REVISION = "revision";
    static final String REVISION_EXPR = "${revision}";

    public static LocalProject load(Path path) throws BootstrapException {
        return load(path, true);
    }

    public static LocalProject load(Path path, boolean required) throws BootstrapException {
        final Path cpd = locateCurrentProjectDir(path, required);
        if (cpd == null) {
            return null;
        }
        try {
            return new LocalProject(readModel(cpd.resolve(POM_XML)), null);
        } catch (UnresolvedRevisionException e) {
            // if the revision couldn't be resolved, we are trying to resolve it from the workspace
            return loadWorkspace(cpd);
        }
    }

    public static LocalProject loadWorkspace(Path path) throws BootstrapException {
        return loadWorkspace(path, true);
    }

    public static LocalProject loadWorkspace(Path path, boolean required) throws BootstrapException {
        path = path.toAbsolutePath().normalize();
        final Path currentProjectDir = locateCurrentProjectDir(path, required);
        if (currentProjectDir == null) {
            return null;
        }
        final LocalWorkspace ws = new LocalWorkspace();
        final LocalProject project = load(ws, null, loadRootModel(currentProjectDir), currentProjectDir);
        return project == null ? load(ws, null, readModel(currentProjectDir.resolve(POM_XML)), currentProjectDir) : project;
    }

    private static LocalProject load(LocalWorkspace workspace, LocalProject parent, Model model, Path currentProjectDir)
            throws BootstrapException {
        final LocalProject project = new LocalProject(model, workspace);
        if (parent != null) {
            parent.modules.add(project);
        }
        LocalProject result = currentProjectDir == null || !currentProjectDir.equals(project.getDir()) ? null : project;
        final List<String> modules = project.getRawModel().getModules();
        if (!modules.isEmpty()) {
            Path dirArg = result == null ? currentProjectDir : null;
            for (String module : modules) {
                final LocalProject loaded = load(workspace, project,
                        readModel(project.getDir().resolve(module).resolve(POM_XML)), dirArg);
                if (loaded != null && result == null) {
                    result = loaded;
                    dirArg = null;
                }
            }
        }
        return result;
    }

    private static Model loadRootModel(Path currentProjectDir) throws BootstrapException {
        Path pomXml = currentProjectDir.resolve(POM_XML);
        Model model = null;
        while (Files.exists(pomXml)) {
            model = readModel(pomXml);
            final Parent parent = model.getParent();
            if (parent != null
                    && parent.getRelativePath() != null
                    && !parent.getRelativePath().isEmpty()) {
                pomXml = pomXml.getParent().resolve(parent.getRelativePath()).normalize();
                if (Files.isDirectory(pomXml)) {
                    pomXml = pomXml.resolve(POM_XML);
                }
            } else {
                pomXml = pomXml.getParent().getParent().resolve(POM_XML);
            }
        }
        return model;
    }

    private static final Model readModel(Path pom) throws BootstrapException {
        try {
            final Model model = ModelUtils.readModel(pom);
            model.setPomFile(pom.toFile());
            return model;
        } catch (IOException e) {
            throw new BootstrapException("Failed to read " + pom, e);
        }
    }

    private static Path locateCurrentProjectDir(Path path, boolean required) throws BootstrapException {
        Path p = path;
        while (p != null) {
            if (Files.exists(p.resolve(POM_XML))) {
                return p;
            }
            p = p.getParent();
        }
        if (required) {
            throw new BootstrapException("Failed to locate project pom.xml for " + path);
        }
        return null;
    }

    private final Model rawModel;
    private final String groupId;
    private final String artifactId;
    private final String version;
    private final Path dir;
    private final LocalWorkspace workspace;
    private final List<LocalProject> modules = new ArrayList<>(0);

    private LocalProject(Model rawModel, LocalWorkspace workspace) throws BootstrapException {
        this.rawModel = rawModel;
        this.dir = rawModel.getProjectDirectory().toPath();
        this.workspace = workspace;
        this.groupId = ModelUtils.getGroupId(rawModel);
        this.artifactId = rawModel.getArtifactId();

        String version = ModelUtils.getVersion(rawModel);
        final boolean revisionVersion;
        if (revisionVersion = REVISION_EXPR.equals(version)) {
            version = resolveRevision(rawModel);
        }

        if (workspace != null) {
            workspace.addProject(this, rawModel.getPomFile().lastModified());
            if (revisionVersion) {
                if (version == null) {
                    version = workspace.getRevision();
                    if (version == null) {
                        throw UnresolvedRevisionException.forGa(groupId, artifactId);
                    }
                } else {
                    workspace.setRevision(version);
                }
            }
        } else if (version == null) {
            throw UnresolvedRevisionException.forGa(groupId, artifactId);
        }

        this.version = version;
    }

    private static String resolveRevision(Model rawModel) {
        final Map<String, String> props = new HashMap<>();
        rawModel.getProperties().entrySet().forEach(e -> props.put(e.getKey().toString(), e.getValue().toString()));
        System.getProperties().entrySet().forEach(e -> props.put(e.getKey().toString(), e.getValue().toString()));
        return props.get(REVISION);
    }

    public String getGroupId() {
        return groupId;
    }

    public String getArtifactId() {
        return artifactId;
    }

    public String getVersion() {
        return version;
    }

    public Path getDir() {
        return dir;
    }

    public Path getOutputDir() {
        return dir.resolve("target");
    }

    public Path getClassesDir() {
        return getOutputDir().resolve("classes");
    }

    public Path getTestClassesDir() {
        return getOutputDir().resolve("test-classes");
    }

    public Path getSourcesSourcesDir() {
        if (getRawModel().getBuild() != null && getRawModel().getBuild().getSourceDirectory() != null) {
            String originalValue = getRawModel().getBuild().getSourceDirectory();
            return Paths
                    .get(originalValue.startsWith(PROJECT_BASEDIR) ? originalValue.replace(PROJECT_BASEDIR, this.dir.toString())
                            : originalValue);
        }
        return dir.resolve("src/main/java");
    }

    public Path getResourcesSourcesDir() {
        if (getRawModel().getBuild() != null && getRawModel().getBuild().getResources() != null) {
            for (Resource i : getRawModel().getBuild().getResources()) {
                //todo: support multiple resources dirs for config hot deployment
                return Paths.get(i.getDirectory());
            }
        }
        return dir.resolve("src/main/resources");
    }

    public Model getRawModel() {
        return rawModel;
    }

    public LocalWorkspace getWorkspace() {
        return workspace;
    }

    public AppArtifactKey getKey() {
        return new AppArtifactKey(groupId, artifactId);
    }

    public AppArtifact getAppArtifact() {
        final AppArtifact appArtifact = new AppArtifact(groupId, artifactId, BootstrapConstants.EMPTY, rawModel.getPackaging(),
                version);
        appArtifact.setPath(getClassesDir());
        return appArtifact;
    }

    public List<LocalProject> getSelfWithLocalDeps() {
        if (workspace == null) {
            return Collections.singletonList(this);
        }
        final List<LocalProject> ordered = new ArrayList<>();
        collectSelfWithLocalDeps(this, new HashSet<>(), ordered);
        return ordered;
    }

    private static void collectSelfWithLocalDeps(LocalProject project, Set<AppArtifactKey> addedDeps,
            List<LocalProject> ordered) {
        if (!project.modules.isEmpty()) {
            for (LocalProject module : project.modules) {
                collectSelfWithLocalDeps(module, addedDeps, ordered);
            }
        }
        for (Dependency dep : project.getRawModel().getDependencies()) {
            final AppArtifactKey depKey = project.getKey(dep);
            final LocalProject localDep = project.workspace.getProject(depKey);
            if (localDep == null || addedDeps.contains(depKey)) {
                continue;
            }
            collectSelfWithLocalDeps(localDep, addedDeps, ordered);
        }
        if (addedDeps.add(project.getKey())) {
            ordered.add(project);
        }
    }

    private AppArtifactKey getKey(Dependency dep) {
        return new AppArtifactKey(PROJECT_GROUPID.equals(dep.getGroupId()) ? getGroupId() : dep.getGroupId(),
                dep.getArtifactId());
    }
}
