/*
 * Copyright 2013 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.nativeplatform.toolchain.internal.tools;

import org.gradle.api.GradleException;
import org.gradle.api.UncheckedIOException;
import org.gradle.internal.os.OperatingSystem;
import org.gradle.internal.text.TreeFormatter;
import org.gradle.nativeplatform.toolchain.internal.ToolType;
import org.gradle.util.TreeVisitor;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ToolSearchPath {
    private final Map<String, File> executables = new HashMap<String, File>();
    private final List<File> pathEntries = new ArrayList<File>();

    private final OperatingSystem operatingSystem;

    public ToolSearchPath(OperatingSystem operatingSystem) {
        this.operatingSystem = operatingSystem;
    }

    public List<File> getPath() {
        return pathEntries;
    }

    public void setPath(List<File> pathEntries) {
        this.pathEntries.clear();
        this.pathEntries.addAll(pathEntries);
        executables.clear();
    }

    public void path(File pathEntry) {
        pathEntries.add(pathEntry);
        executables.clear();
    }

    public CommandLineToolSearchResult locate(ToolType key, String exeName) {
        File executable = executables.get(exeName);
        if (executable == null) {
            executable = findExecutable(operatingSystem, exeName);
            if (executable != null) {
                executables.put(exeName, executable);
            }
        }
        return executable == null || !executable.isFile() ? new MissingTool(key, exeName, pathEntries) : new FoundTool(executable);
    }

    private File findExecutable(OperatingSystem operatingSystem, String name) {
        List<File> path = pathEntries.isEmpty() ? operatingSystem.getPath() : pathEntries;
        String exeName = operatingSystem.getExecutableName(name);
        try {
            if (name.contains(File.separator)) {
                return maybeResolveFile(operatingSystem, new File(name), new File(exeName));
            }
            for (File pathEntry : path) {
                File resolved = maybeResolveFile(operatingSystem, new File(pathEntry, name), new File(pathEntry, exeName));
                if (resolved != null) {
                    return resolved;
                }
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

        return null;
    }

    private File maybeResolveFile(OperatingSystem operatingSystem, File symlinkCandidate, File exeCandidate) throws IOException {
        if (exeCandidate.isFile()) {
            return exeCandidate;
        }
        if (operatingSystem.isWindows()) {
            File symlink = maybeResolveCygwinSymlink(symlinkCandidate);
            if (symlink != null) {
                return symlink;
            }
        }
        return null;
    }

    private File maybeResolveCygwinSymlink(File symlink) throws IOException {
        if (!symlink.isFile()) {
            return null;
        }
        if (symlink.length() <= 11) {
            return null;
        }

        String pathStr;
        DataInputStream instr = new DataInputStream(new BufferedInputStream(new FileInputStream(symlink)));
        try {
            byte[] header = new byte[10];
            instr.readFully(header);
            if (!new String(header, "utf-8").equals("!<symlink>")) {
                return null;
            }
            byte[] pathContent = new byte[(int) symlink.length() - 11];
            instr.readFully(pathContent);
            pathStr = new String(pathContent, "utf-8");
        } finally {
            instr.close();
        }

        symlink = new File(symlink.getParentFile(), pathStr);
        if (symlink.isFile()) {
            return symlink.getCanonicalFile();
        }
        return null;
    }

    private static class FoundTool implements CommandLineToolSearchResult {
        private final File tool;

        private FoundTool(File tool) {
            this.tool = tool;
        }

        @Override
        public boolean isAvailable() {
            return true;
        }

        @Override
        public File getTool() {
            return tool;
        }

        @Override
        public void explain(TreeVisitor<? super String> visitor) {
        }
    }

    private static class MissingTool implements CommandLineToolSearchResult {
        private final ToolType type;
        private final String exeName;
        private final List<File> path;

        private MissingTool(ToolType type, String exeName, List<File> path) {
            this.type = type;
            this.exeName = exeName;
            this.path = path;
        }

        @Override
        public void explain(TreeVisitor<? super String> visitor) {
            if (path.isEmpty()) {
                visitor.node(String.format("Could not find %s '%s' in system path.", type.getToolName(), exeName));
            } else {
                visitor.node(String.format("Could not find %s '%s'. Searched in", type.getToolName(), exeName));
                visitor.startChildren();
                for (File location : path) {
                    visitor.node(location.toString());
                }
                visitor.endChildren();
            }
        }

        @Override
        public File getTool() {
            TreeFormatter formatter = new TreeFormatter();
            explain(formatter);
            throw new GradleException(formatter.toString());
        }

        @Override
        public boolean isAvailable() {
            return false;
        }
    }
}
