/*
 * 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.netbeans.nbbuild;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.tools.ant.*;
import org.apache.tools.ant.types.*;
import org.apache.tools.ant.util.FileUtils;

/**
 * Replace paths prefixes with variables. 
 * Designed for netbeans.dest dir and test.dist.dir variables 
 */
public class ShorterPaths extends Task {

    /** dir is prefix and name is name of variable
     * <shorterpaths in="inputpropname" out="outpropNames">
     *     <replacement name="property_name" dir="directory"/>
     * </shorterpaths>
     */
    public static class Replacement {

        String name;
        File dir;
        File excluded;

        public void setName(String name) {
            this.name = name;
        }

        public void setDir(File dir) {
            this.dir = dir;
        }

        public void setExcluded(File excluded) {
            this.excluded = excluded;
        }

        @Override
        public String toString() {
            return dir + (excluded != null ? " - " + excluded : "") + " => ${" + name + "}";
        }
    }
    private List<Replacement> replacements = new LinkedList<>(); // List<Nestme>

    public Replacement createReplacement() {
        Replacement r = new Replacement();
        replacements.add(r);
        return r;
    }

    public void addReplacement(Replacement r) {
        replacements.add(r);
    }
    private Path in;

    public void setIn(Path p) {
        if (in == null) {
            in = p.createPath();
        }
        in.append(p);
    }

    public Path createIn() {
        if (in == null) {
            in = new Path(getProject());
        }
        return in;
    }

    public void setinRef(Reference r) {
        createIn().setRefid(r);
    }
    // <customtask path="foo:bar"/>
    // <customtask>
    //     <path>
    //         <pathelement location="foo"/>
    //     </path>
    // </customtask>
    // Etc.
    String out;

    public void setOut(String out) {
        this.out = out;
    }
    String extraLibs;

    public void setExtraLibs(String extraLibs) {
        this.extraLibs = extraLibs;
    }
    File extraLibsDir;

    public void setExtraLibsDir(File extraLibsDir) {
        this.extraLibsDir = extraLibsDir;
    }
    File testProperties;

    public void setTestProperties(File testProperties) {
        this.testProperties = testProperties;
    }

    @Override
    public void execute() throws BuildException {
        // TODO code here what the task actually does:
        String paths[] = in.list();
        StringBuffer nbLibBuff = new StringBuffer();
//        Path nbLibPath = new Path(getProject());
        StringBuffer externalLibBuf = new StringBuffer();
        try {
            for (int i = 0; i < paths.length; i++) {
                String path = paths[i];
                File file = new File(path);
                // check if file exists
                if (file.exists()) {
                    // add it on classpath
                    path = file.getCanonicalPath();
                    simplyPath(path, externalLibBuf, nbLibBuff);
                } else {
                    log("Path element " + file + " doesn't exist.", Project.MSG_VERBOSE);
                }
            }
            if (out != null) {
                define(out, nbLibBuff.toString());
            }
            if (this.extraLibs != null) {
                define(extraLibs, externalLibBuf.toString());
            }

            if (testProperties != null) {
                // create properties file
                try (PrintWriter pw = new PrintWriter(testProperties)) {
                    // copy extra unit.test.properties
                    Map<String,Object> properties = getProject().getProperties();
                    StringBuffer outProp = new StringBuffer();
                    for (String name : properties.keySet()) {
                        if (name.matches("test-(unit|qa-functional)-sys-prop\\..+")) {
                            if (name.equals("test-unit-sys-prop.xtest.data")) {
                                // ignore overring xtest.data.dir, data.zip placed to standard location
                                continue;
                            }
                            //
                            outProp.setLength(0);
                            for (String path : tokenizePath(properties.get(name).toString())) {
                                replacePath(path, outProp);
                            }
                            pw.println(name.replaceFirst("^test-(unit|qa-functional)-sys-prop\\.", "test-sys-prop.") + "=" + outProp);
                        } else if (name.startsWith("test.config")) {
                            pw.println(name + "=" + properties.get(name));
                        } else if ("requires.nb.javac".equals(name)) {
                            pw.println(name + "=" + properties.get(name));
                        }
                    }
                    pw.println("extra.test.libs=" + externalLibBuf.toString());
                    pw.println("test.run.cp=" + nbLibBuff.toString());
                }
            }
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    /** Replace absolute path with ${a.prop}/relpath and concatenate resulting
     * path to nbLibBuff. If replacement does not exist copy file on the path
     * to extra lib folder and add the path to externalLibBuf.
     * @param path path to be replaced
     * @param externalLibBuf extra lib paths buffer
     * @param nbLibBuff nb lib paths buffer
     * @throws IOException 
     */
    private void simplyPath(String path, final StringBuffer externalLibBuf, final StringBuffer nbLibBuff) throws IOException {
        boolean bAppend = false;
        File file = new File(path);
        if (file.exists()) {
            // file exists, try to to replace the path with ${a.prop}/relpath
            //
            path = file.getAbsolutePath();
            for (Replacement repl : replacements) {
                String dirCan = repl.dir.getCanonicalPath();
                if (path.startsWith(dirCan) && (repl.excluded == null || !path.startsWith(repl.excluded.getCanonicalPath()))) {
                    if (nbLibBuff.length() > 0) {
                        nbLibBuff.append(":\\\n");
                    }

                    nbLibBuff.append("${").append(repl.name).append("}");
                    // postfix + unify file separators to '/'
                    nbLibBuff.append(path.substring(dirCan.length()).replace(File.separatorChar, '/'));
                    bAppend = true;
                    break;
                }
            }
            if (!bAppend) {
                String fName = copyExtraLib(file);
                if (fName != null) {
                    if (externalLibBuf.length() > 0) {
                        externalLibBuf.append(":\\\n");
                    }
                    externalLibBuf.append("${extra.test.libs.dir}/").append(fName);
                }
            }

        } else {
            if (nbLibBuff.length() > 0) {
                nbLibBuff.append(":\\\n");
            }
            nbLibBuff.append(path);
        }

    }

    /** Replace absolute path with ${a.prop}/relpath and concatenate resulting
     * path to pathsBuff.
     * @param path path to be replaced by one of registered replacements
     * @param pathsBuff buffer containing whole path
     */
    private void replacePath(String path, final StringBuffer pathsBuff) throws IOException {
        File file = new File(path);
        if (file.exists()) {
            path = file.getAbsolutePath();
        }
        boolean replacementFound = false;
        if (pathsBuff.length() > 0) {
            pathsBuff.append(":\\\n");
        }
        for (Replacement repl : replacements) {
            String dirCan = repl.dir.getCanonicalPath();
            if (path.startsWith(dirCan)) {
                pathsBuff.append("${").append(repl.name).append("}");
                // postfix + unify file separators to '/'
                pathsBuff.append(path.substring(dirCan.length()).replace(File.separatorChar, '/'));
                replacementFound = true;
                break;
            }
        }
        if (!replacementFound) {
            // append without property replacement
            pathsBuff.append(path.replace(File.separatorChar, '/'));
        }
    }

    private void define(String prop, String val) {
        log("Setting " + prop + "=" + val, Project.MSG_VERBOSE);
        String old = getProject().getProperty(prop);
        if (old != null && !old.equals(val)) {
            getProject().log("Warning: " + prop + " was already set to " + old, Project.MSG_WARN);
        }
        getProject().setNewProperty(prop, val);
    }

    private String copyExtraLib(File file) throws IOException {
        if (extraLibsDir == null || !extraLibsDir.isDirectory() || !file.isFile()) {
            return null;
        }
        File copy = new File(extraLibsDir, file.getName());
        boolean wasCopied = copyMissing(file, copy);
        // copy Class-Path extensions if available
        if (wasCopied && file.getName().endsWith(".jar")) {
            String cp;
            try {
                try (JarFile jf = new JarFile(file)) {
                    Manifest manifest = jf.getManifest();
                    cp = manifest != null ? manifest.getMainAttributes().getValue(Name.CLASS_PATH) : null;
                }
            } catch (IOException x) {
                log("Could not parse " + file + " for Class-Path", Project.MSG_WARN);
                cp = null;
            }
            if (cp != null) {
                for (String ext : cp.split(" ")) {
                    // copy CP extension with relative path to keep link dependency from manifest
                    copyMissing(new File(file.getParentFile(), ext), new File(extraLibsDir, ext));
                }
            }
        }
        return copy.getName();
    }
    
    /** Copies source file to target location only if it is missing there.
     * @param file source file
     * @param copy target file
     * @return true if file was successfully copied, false if source is the same
     * as target
     * @throws IOException if target file exists and it is not the same as 
     * source file
     */
    private boolean copyMissing(File file, File copy) throws IOException {
        if (FileUtils.getFileUtils().contentEquals(file, copy)) {
            return false;
        } else if (copy.isFile()) {
            // Could try to copy to a different name, but this is probably something that should be fixed anyway:
            throw new IOException(file + " is not the same as " + copy + "; will not overwrite");
        }
        log("Copying " + file + " to extralibs despite " + replacements);
        FileUtils.getFileUtils().copyFile(file, copy);
        return true;
    }

    /**
     * Split an Ant-style path specification into components.
     * Tokenizes on <code>:</code> and <code>;</code>, paying
     * attention to DOS-style components such as <samp>C:\FOO</samp>.
     * Also removes any empty components.
     * Copied from org.netbeans.spi.project.support.ant.PropertyUtils.
     * @param path an Ant-style path (elements arbitrary) using DOS or Unix separators
     * @return a tokenization of that path into components
     */
    public static String[] tokenizePath(String path) {
        List<String> l = new ArrayList<>();
        StringTokenizer tok = new StringTokenizer(path, ":;", true); // NOI18N
        char dosHack = '\0';
        char lastDelim = '\0';
        int delimCount = 0;
        while (tok.hasMoreTokens()) {
            String s = tok.nextToken();
            if (s.length() == 0) {
                // Strip empty components.
                continue;
            }
            if (s.length() == 1) {
                char c = s.charAt(0);
                if (c == ':' || c == ';') {
                    // Just a delimiter.
                    lastDelim = c;
                    delimCount++;
                    continue;
                }
            }
            if (dosHack != '\0') {
                // #50679 - "C:/something" is also accepted as DOS path
                if (lastDelim == ':' && delimCount == 1 && (s.charAt(0) == '\\' || s.charAt(0) == '/')) {
                    // We had a single letter followed by ':' now followed by \something or /something
                    s = "" + dosHack + ':' + s;
                    // and use the new token with the drive prefix...
                } else {
                    // Something else, leave alone.
                    l.add(Character.toString(dosHack));
                    // and continue with this token too...
                }
                dosHack = '\0';
            }
            // Reset count of # of delimiters in a row.
            delimCount = 0;
            if (s.length() == 1) {
                char c = s.charAt(0);
                if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
                    // Probably a DOS drive letter. Leave it with the next component.
                    dosHack = c;
                    continue;
                }
            }
            l.add(s);
        }
        if (dosHack != '\0') {
            //the dosHack was the last letter in the input string (not followed by the ':')
            //so obviously not a drive letter.
            //Fix for issue #57304
            l.add(Character.toString(dosHack));
        }
        return l.toArray(new String[l.size()]);
    }
}
