/*
 * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.jpackage.internal;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.text.MessageFormat;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.WixToolset.WixToolsetType;
import jdk.jpackage.internal.util.PathUtils;

/**
 * WiX tool.
 */
public enum WixTool {
    Candle3("candle", DottedVersion.lazy("3.0")),
    Light3("light", DottedVersion.lazy("3.0")),
    Wix4("wix", DottedVersion.lazy("4.0.4"));

    WixTool(String commandName, DottedVersion minimalVersion) {
        this.toolFileName = PathUtils.addSuffix(Path.of(commandName), ".exe");
        this.minimalVersion = minimalVersion;
    }

    static final class ToolInfo {

        ToolInfo(Path path, String version) {
            this.path = path;
            this.version = DottedVersion.lazy(version);
        }

        final Path path;
        final DottedVersion version;
    }

    static WixToolset createToolset() throws ConfigException {
        Function<List<ToolLookupResult>, Map<WixTool, ToolInfo>> conv = lookupResults -> {
            return lookupResults.stream().filter(ToolLookupResult::isValid).collect(Collectors.
                    groupingBy(lookupResult -> {
                return lookupResult.getInfo().version.toString();
            })).values().stream().filter(sameVersionLookupResults -> {
                Set<WixTool> sameVersionTools = sameVersionLookupResults.stream().map(
                        ToolLookupResult::getTool).collect(Collectors.toSet());
                if (sameVersionTools.equals(Set.of(Candle3)) || sameVersionTools.equals(Set.of(
                        Light3))) {
                    // There is only one tool from WiX v3 toolset of some version available. Discard it.
                    return false;
                } else {
                    return true;
                }
            }).flatMap(List::stream).collect(Collectors.toMap(ToolLookupResult::getTool,
                    ToolLookupResult::getInfo, (ToolInfo x, ToolInfo y) -> {
                        return Stream.of(x, y).sorted(Comparator.comparing((ToolInfo toolInfo) -> {
                            return toolInfo.version.toComponentsString();
                        }).reversed()).findFirst().get();
                    }));
        };

        Function<List<ToolLookupResult>, Optional<WixToolset>> createToolset = lookupResults -> {
            var tools = conv.apply(lookupResults);
            // Try to build a toolset found in the PATH and in known locations.
            return Stream.of(WixToolsetType.values()).map(toolsetType -> {
                return WixToolset.create(toolsetType.getTools(), tools);
            }).filter(Objects::nonNull).findFirst();
        };

        var toolsInPath = Stream.of(values()).map(tool -> {
            return new ToolLookupResult(tool, null);
        }).toList();

        // Try to build a toolset from tools in the PATH first.
        var toolset = createToolset.apply(toolsInPath);
        if (toolset.isPresent()) {
            return toolset.get();
        }

        // Look up for WiX tools in known locations.
        var toolsInKnownWiXDirs = findWixInstallDirs().stream().map(dir -> {
            return Stream.of(values()).map(tool -> {
                return new ToolLookupResult(tool, dir);
            });
        }).flatMap(Function.identity()).toList();

        // Build a toolset found in the PATH and in known locations.
        var allFoundTools = Stream.of(toolsInPath, toolsInKnownWiXDirs).flatMap(List::stream).filter(
                ToolLookupResult::isValid).toList();
        toolset = createToolset.apply(allFoundTools);
        if (toolset.isPresent()) {
            return toolset.get();
        } else if (allFoundTools.isEmpty()) {
            throw new ConfigException(I18N.getString("error.no-wix-tools"), I18N.getString(
                    "error.no-wix-tools.advice"));
        } else {
            var toolOldVerErr = allFoundTools.stream().map(lookupResult -> {
                if (lookupResult.versionTooOld) {
                    return new ConfigException(MessageFormat.format(I18N.getString(
                            "message.wrong-tool-version"), lookupResult.getInfo().path,
                            lookupResult.getInfo().version, lookupResult.getTool().minimalVersion),
                            I18N.getString("error.no-wix-tools.advice"));
                } else {
                    return null;
                }
            }).filter(Objects::nonNull).findAny();
            if (toolOldVerErr.isPresent()) {
                throw toolOldVerErr.get();
            } else {
                throw new ConfigException(I18N.getString("error.no-wix-tools"), I18N.getString(
                        "error.no-wix-tools.advice"));
            }
        }
    }

    private static class ToolLookupResult {

        ToolLookupResult(WixTool tool, Path lookupDir) {

            final Path toolPath = Optional.ofNullable(lookupDir).map(p -> p.resolve(
                    tool.toolFileName)).orElse(tool.toolFileName);

            final boolean[] tooOld = new boolean[1];
            final String[] parsedVersion = new String[1];

            final var validator = new ToolValidator(toolPath).setMinimalVersion(tool.minimalVersion).
                    setToolNotFoundErrorHandler((name, ex) -> {
                        return new ConfigException("", "");
                    }).setToolOldVersionErrorHandler((name, version) -> {
                tooOld[0] = true;
                return null;
            });

            final Function<Stream<String>, String> versionParser;

            if (Set.of(Candle3, Light3).contains(tool)) {
                validator.setCommandLine("/?");
                versionParser = output -> {
                    String firstLineOfOutput = output.findFirst().orElse("");
                    int separatorIdx = firstLineOfOutput.lastIndexOf(' ');
                    if (separatorIdx == -1) {
                        return null;
                    }
                    return firstLineOfOutput.substring(separatorIdx + 1);
                };
            } else {
                validator.setCommandLine("--version");
                versionParser = output -> {
                    return output.findFirst().orElse("");
                };
            }

            validator.setVersionParser(output -> {
                parsedVersion[0] = versionParser.apply(output);
                return parsedVersion[0];
            });

            this.tool = tool;
            if (validator.validate() == null) {
                // Tool found
                this.versionTooOld = tooOld[0];
                this.info = new ToolInfo(toolPath, parsedVersion[0]);
            } else {
                this.versionTooOld = false;
                this.info = null;
            }
        }

        WixTool getTool() {
            return tool;
        }

        ToolInfo getInfo() {
            return info;
        }

        boolean isValid() {
            return info != null && !versionTooOld;
        }

        boolean isVersionTooOld() {
            return versionTooOld;
        }

        private final WixTool tool;
        private final ToolInfo info;
        private final boolean versionTooOld;
    }

    private static Path getSystemDir(String envVar, String knownDir) {
        return Optional
                .ofNullable(getEnvVariableAsPath(envVar))
                .orElseGet(() -> Optional
                .ofNullable(getEnvVariableAsPath("SystemDrive"))
                .orElseGet(() -> Path.of("C:")).resolve(knownDir));
    }

    private static Path getEnvVariableAsPath(String envVar) {
        String path = System.getenv(envVar);
        if (path != null) {
            try {
                return Path.of(path);
            } catch (InvalidPathException ex) {
                Log.error(MessageFormat.format(I18N.getString(
                        "error.invalid-envvar"), envVar));
            }
        }
        return null;
    }

    private static List<Path> findWixInstallDirs() {
        return Stream.of(findWixCurrentInstallDirs(), findWix3InstallDirs()).
                flatMap(List::stream).toList();
    }

    private static List<Path> findWixCurrentInstallDirs() {
        return Stream.of(getEnvVariableAsPath("USERPROFILE"), Optional.ofNullable(System.
                getProperty("user.home")).map(Path::of).orElse(null)).filter(Objects::nonNull).map(
                path -> {
                    return path.resolve(".dotnet/tools");
                }).filter(Files::isDirectory).distinct().toList();
    }

    private static List<Path> findWix3InstallDirs() {
        PathMatcher wixInstallDirMatcher = FileSystems.getDefault().
                getPathMatcher(
                        "glob:WiX Toolset v*");

        Path programFiles = getSystemDir("ProgramFiles", "\\Program Files");
        Path programFilesX86 = getSystemDir("ProgramFiles(x86)",
                "\\Program Files (x86)");

        // Returns list of WiX install directories ordered by WiX version number.
        // Newer versions go first.
        return Stream.of(programFiles, programFilesX86).map(path -> {
            try (var paths = Files.walk(path, 1)) {
                return paths.toList();
            } catch (IOException ex) {
                Log.verbose(ex);
                List<Path> empty = List.of();
                return empty;
            }
        }).flatMap(List::stream)
                .filter(path -> wixInstallDirMatcher.matches(path.getFileName())).
                sorted(Comparator.comparing(Path::getFileName).reversed())
                .map(path -> path.resolve("bin"))
                .toList();
    }

    private final Path toolFileName;
    private final DottedVersion minimalVersion;
}
