/*
 * 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.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.FileScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;

/** Check source files for a license notice.
 * @author Jesse Glick
 */
public class CheckLicense extends Task {

    private final List<FileSet> filesets = new ArrayList<>(1);
    private String fragment;
    private List<Convert> fragments;
    private FailType fail;

    /** Add a file set of source files to check.
     * @param fs set of files to check licenses of
     */
    public void addFileSet (FileSet fs) {
        filesets.add (fs);
    }
    
    /** Set the fragment of license notice which is expected
     * to be found in each source file.
     * @param f the fragment
     */
    public void setFragment (String f) {
        fragment = f;
    }
    
    public void setFail(FailType t) {
        fail = t;
    }
    
    public Convert createConvert() {
        Convert f = new Convert();
        if (fragments == null) {
            fragments = new ArrayList<>();
        }
        
        fragments.add(f);
        return f;
    }

    public @Override void execute() throws BuildException {
        if (fragment == null) {
            if (fragments == null) {
                throw new BuildException("You must supply a fragment", getLocation());
            }
            
            executeReplace();
            return;
        }
        if (filesets.isEmpty()) {
            throw new BuildException("You must supply at least one fileset", getLocation());
        }
        String failMsg = null;
        try {
            for (FileSet fileset : filesets) {
                FileScanner scanner = fileset.getDirectoryScanner(getProject());
                File baseDir = scanner.getBasedir ();
                String[] files = scanner.getIncludedFiles ();
                log ("Looking for " + fragment + " in " + files.length + " files in " + baseDir.getAbsolutePath ());
                for (int i = 0; i < files.length; i++) {
                    File f = new File (baseDir, files[i]);
                    //log("Scanning " + f, Project.MSG_VERBOSE);
                    try (BufferedReader br = new BufferedReader (new FileReader (f))) {
                        String line;
                        while ((line = br.readLine ()) != null) {
                            if (line.indexOf (fragment) != -1) {
                                // Found it.
                                if (fail != null && "whenpresent".equals(fail.getValue())) {
                                    if (failMsg != null) {
                                        log(failMsg, Project.MSG_ERR);
                                    }
                                    failMsg = "License found in " + f;
                                }
                                break;
                            }
                        }
                        if (line == null) {
                            String msg = f.getAbsolutePath () + ":1: no license notice found";
                            if (fail != null && "whenmissing".equals(fail.getValue())) {
                                throw new BuildException(msg);
                            }
                            if (fail == null) {
                                // Scanned whole file without finding it.
                                log (msg, Project.MSG_ERR);
                            }
                        }
                    }
                }
            }
            
            if (failMsg != null) {
                throw new BuildException(failMsg);
            }
            
        } catch (IOException ioe) {
            throw new BuildException("Could not open files to check licenses", ioe, getLocation());
        }
    }
    
    private void executeReplace() throws BuildException {
        try {
            byte[] workingArray = new byte[1024];
            for (FileSet fileset : filesets) {
                FileScanner scanner = fileset.getDirectoryScanner(getProject());
                File baseDir = scanner.getBasedir ();
                String[] files = scanner.getIncludedFiles ();
                log ("Replacing code in " + files.length + " files in " + baseDir.getAbsolutePath ());
                for (int i = 0; i < files.length; i++) {
                    File file = new File (baseDir, files[i]);
                    log("Processing " + file, Project.MSG_VERBOSE);
                    FileInputStream is = new FileInputStream(file);
                    int workingLength = is.read(workingArray);
                    if (workingLength == -1) {
                        continue;
                    }
                    String workingString = new String(workingArray, 0, workingLength);
                    boolean changed = false;
                    String prefix = null;

                    for (Convert f : fragments) {
                        Matcher matcher = f.orig.matcher(workingString);

                        while (matcher.find()) {
                            if (f.prefix) {
                                if (prefix != null) {
                                    throw new BuildException("Only one convert element can be prefix!");
                                }
                                if (matcher.groupCount() != 1) {
                                    throw new BuildException("There should be one group for the prefix element. Was: " + matcher.groupCount());
                                }
                                prefix = matcher.group(1);
                            }

                            String before = workingString.substring(0, matcher.start());
                            String after = workingString.substring(matcher.end());
                            String middle = wrapWithPrefix(f.repl, prefix, before.length() == 0 || before.endsWith("\n"));

                            if (!middle.equals(matcher.group(0))) {
                                workingString = before + middle + after;
                                log("Matched " + middle, Project.MSG_VERBOSE);
                                changed = true;
                            } else {
                                log("Matched, but no change: " + middle, Project.MSG_VERBOSE);
                            }

                            if (!f.all) {
                                break;
                            } else {
                                matcher = f.orig.matcher(workingString);
                            }
                        }
                    }

                    byte[] rest = null;
                    if (is.available() > 0 && changed) {
                        rest = new byte[is.available()];
                        int read = is.read(rest);
                        assert read == rest.length;
                    }

                    is.close();

                    if (changed) {
                        log ("Rewriting " + file);
                        FileOutputStream os = new FileOutputStream(file);
                        workingString = Pattern.compile(" +$", Pattern.MULTILINE).matcher(workingString+"X").replaceAll("");
                        os.write(workingString.substring(0, workingString.length() - 1).getBytes());
                        if (rest != null) {
                            os.write(rest);
                        }
                        os.close();
                    }
                }
            }
        } catch (IOException ioe) {
            throw new BuildException("Could not open files to check licenses", ioe, getLocation());
        }
    }

    private String wrapWithPrefix(String repl, String prefix, boolean startWithPrefix) {
        if (prefix == null) {
            return repl;
        }
        
        String[] all = repl.split("\n");
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < all.length; i++) {
            if (startWithPrefix) {
                sb.append(prefix);
            }
            sb.append(all[i]);
            if (i < all.length - 1) {
                sb.append('\n');
            }
            startWithPrefix = true;
        }
        
        return sb.toString();
    }

    
    public static final class Convert {
        Pattern orig;
        String repl;
        boolean prefix;
        boolean all;
        
        public void setToken(String orig) {
            this.orig = Pattern.compile(orig, Pattern.DOTALL | Pattern.MULTILINE);
        }
        public void setReplace(String repl) {
            this.repl = repl.replace("\\n", "\n").replace("\\t", "\t");
        }
        public void setPrefix(boolean b) {
            prefix = b;
        }
        public void setReplaceAll(boolean b) {
            all = b;
        }
        public Line createLine() {
            return new Line();
        }
        
        public final class Line {
            public void setText(String t) {
                if (repl == null) {
                    repl = t;
                } else {
                    repl = repl + "\n" + t;
                }
            }
        }
    }
    public static final class FailType extends org.apache.tools.ant.types.EnumeratedAttribute {
        public String[] getValues () {
            return new String[] { 
                "whenmissing",
                "whenpresent",
            };
        }
    }
}
