/*
 * 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.awt.Dimension;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.FileSet;

/**
 * Task for copying out icons from NetBeans projects. It creates paralel directory 
 * structure in destdir with only copied icons. It creates index.html page that 
 * lists all found icons and group them according to nb modules. Icons are considered 
 * images with dimensions 8x8, 16x16, 24x24, 32x32 of type PNG or GIF. 
 * Inclusion filter for images in each module is given by attribute 'iconincludes', 
 * default inclusion filter is all gifs and pngs under src folder. Exclusion filter
 * is defined by attribute 'iconexcludes'. Filters are regular ant inclusion and 
 * exclusion filters. Task tries to find all nb modules up to depth defined by 
 * 'depth' attribute (default is 4) in the nbsrcroot. Modules without icons are
 * not listed by default, they can be shown by setting attribute 'showempty' to true.
 * 
 * Required attributes are:
 *   nbsrcroot ... root of NetBeans Hg checkout
 *   destdir   ... dir for copying icons
 * 
 * @author Milan Kubec
 */
public class CopyIcons extends MatchingTask {
    
    private int depth = 0;
    private List<File> projectDirList = new ArrayList<>();
    private List<ProjectIconInfo> prjIconInfoList = new ArrayList<>();
    
    File baseDir = null;
    public void setNbsrcroot(File f) {
        baseDir = f;
    }
    
    File destDir = null;
    public void setDestdir(File f) {
        destDir = f;
    }
    
    int userDepth = 2;
    public void setDepth(int n) {
        userDepth = n;
    }
    
    String iconIncludes = "src/**/*.png,src/**/*.gif";
    public void setIconincludes(String s) {
        iconIncludes = s;
    }
    
    String iconExcludes = "";
    public void setIconexcludes(String s) {
        iconExcludes = s;
    }
    
    String prjIncludes = "";
    public void setPrjincludes(String s) {
        prjIncludes = s;
    }
    
    String prjExcludes = "";
    public void setPrjExcludes(String s) {
        prjExcludes = s;
    }
    
    boolean showEmpty = false;
    public void setShowempty(boolean b) {
        showEmpty = b;
    }
    
    public void execute() throws BuildException {
        if (baseDir == null || destDir == null) {
            log("Nbsrcroot or destdir are not specified.");
            return;
        }
        scanForProjectDirs(baseDir);
        for (Iterator<File> iter = projectDirList.iterator(); iter.hasNext(); ) {
            File f = iter.next();
            processProjectDir(f);
        }
        copyToDestDir(prjIconInfoList);
        dumpListToHTML(prjIconInfoList);
    }
    
    private ImageInfo readImageInfo(File fl) throws IOException {
        Dimension dim = null;
        ImageInfo imageInfo = null;
        try (ByteArrayInputStream bais = readSomeBytes(fl)) {
            if (isGIF(bais)) {
                imageInfo = new ImageInfo(readGIFDimension(bais), ImageInfo.GIF);
            } else if (isPNG(bais)) {
                imageInfo = new ImageInfo(readPNGDimension(bais), ImageInfo.PNG);
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return imageInfo;
    }
    
    private ByteArrayInputStream readSomeBytes(File fl) throws IOException {
        byte buffer[] = new byte[30];
        FileInputStream fis = null;
        fis = new FileInputStream(fl);
        fis.read(buffer);
        fis.close();
        return new ByteArrayInputStream(buffer);
    }
    
    private boolean isGIF(InputStream is) throws IOException {
        is.reset();
        byte buf[];
        is.read(buf = new byte[3]);
        int signatureBuffer[] = parseUnsigned(buf);
        if (((char) signatureBuffer[0] == 'G') &&
            ((char) signatureBuffer[1] == 'I') &&
            ((char) signatureBuffer[2] == 'F')) {
            return true;
        }
        return false;
    }
    
    private boolean isPNG(InputStream is) throws IOException {
        is.reset();
        byte buf[];
        is.read(buf = new byte[8]);
        int signatureBuffer[] = parseUnsigned(buf);
        if ((signatureBuffer[0] == 137) && // PNG signature
            (signatureBuffer[1] == 80) && 
            (signatureBuffer[2] == 78) && 
            (signatureBuffer[3] == 71) && 
            (signatureBuffer[4] == 13) && 
            (signatureBuffer[5] == 10) && 
            (signatureBuffer[6] == 26) && 
            (signatureBuffer[7] == 10)) {
            return true;
        }
        return false;
    }
    
    private Dimension readGIFDimension(InputStream is) throws IOException {
        byte buf [];
        is.read(new byte[3]); // GIF version
        is.read(buf = new byte[2]); // width
        int widthBuf[] = parseUnsigned(buf);
        int width = (widthBuf[1] << 8) + widthBuf[0];
        is.read(buf = new byte[2]); // height
        int heightBuf[] = parseUnsigned(buf);
        int height = (heightBuf[1] << 8) + heightBuf[0];
        return new Dimension(width, height);
    }
    
    private Dimension readPNGDimension(InputStream is) throws IOException {
        byte buf[];
        is.read(new byte[4]); // length
        is.read(new byte[4]); // type
        is.read(buf = new byte[4]); // width
        int widthBuf[] = parseUnsigned(buf);
        int width = (widthBuf[0] << 24) + (widthBuf[1] << 16) + (widthBuf[2] << 8) + widthBuf[3];
        is.read(buf = new byte[4]); // height
        int heightBuf[] = parseUnsigned(buf);
        int height = (heightBuf[0] << 24) + (heightBuf[1] << 16) + (heightBuf[2] << 8) + heightBuf[3];
        return new Dimension(width, height);
    }
    
    private static int[] parseUnsigned(byte[] src) {
        int[] val = new int[src.length];
        for (int i = 0; i < src.length; i ++) {
            val[i] = (src[i] < 0) && (src[i] >= -128) ? 256 + src[i] : src[i];
        }
        return val;
    }
    
    // -------------------------------------------------------------------------
    
//    public static void main(String[] args) {
//        File buildFile = new File("e:/development/projects/CopyIcons/testscript.xml");
//        Project p = new Project();
//        p.setUserProperty("ant.file", buildFile.getAbsolutePath());
//        p.init();
//        ProjectHelper helper = ProjectHelper.getProjectHelper();
//        p.addReference("ant.projectHelper", helper);
//        helper.parse(p, buildFile);
//        p.executeTarget(p.getDefaultTarget());
//    }
    
    // -------------------------------------------------------------------------
    
    private static class ImageInfo {
        
        public static final int GIF = 1;
        public static final int PNG = 2;
        
        private Dimension dim;
        private int type;
        private String path;
        private String ext;
        
        public ImageInfo(Dimension dm, int tp) {
            this(null, dm, tp);
        }
        
        public ImageInfo(String pth, Dimension dm, int tp) {
            path = pth;
            dim = dm;
            type = tp;
        }
        
        public String getPath() {
            return path;
        }
        
        public void setPath(String pth) {
            path = pth;
        }
        
        public int getHeight() {
            return dim.height;
        }
        
        public int getWidth() {
            return dim.width;
        }
        
        public String getType() {
            if (type == GIF) {
                return "GIF";
            } else if (type == PNG) {
                return "PNG";
            }
            return "";
        }
        
        public String getExt() {
            return ext;
        }
        
        public String setExt(String ex) {
            return ext = ex;
        }
        
    }
    
    private static class ProjectIconInfo {
        
        public String prjPath;
        public List<ImageInfo> matchingIcons;
        public List<ImageInfo> notmatchingIcons;
        
        public ProjectIconInfo(String pth, List<ImageInfo> mi, List<ImageInfo> nmi) {
            prjPath = pth;
            matchingIcons = mi;
            notmatchingIcons = nmi;
        }
        
    }
    
    // -------------------------------------------------------------------------
        
    private void dumpListToHTML(List<ProjectIconInfo> lst) {
        File reportFile = new File(destDir, "index.html");
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(reportFile);
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        }
        try (PrintWriter pw = new PrintWriter(new BufferedOutputStream(fos))) {
            pw.println("<html>");
            pw.println("<head>");
            pw.println("<style type=\"text/css\">\nbody { font-family: Tahoma, Verdana, sans-serif }\n</style>");
            pw.println("</head>");
            pw.println("<body>");
            pw.println("<h2>List of icons in NetBeans projects</h2>");
            pw.println("<h3>NetBeans Source Root: " + baseDir + "<br/>");
            pw.println("Destination Directory: " + destDir + "</h3>");
            pw.println("<p style=\"width: 70%\"><b>Description:</b><br/> New Image is image in the Destination Directory, Orig. Image is image in NetBeans Source Root. " +
                    "By replacing the image in Destination Directory under coresponding module and path you can prepare rebranded " +
                    "icons in paralel directory structure and then copy them over to NetBeans Source Root. Orig. Image is " +
                    "in the table just for comparison and reference what image was already changed.</p>");
            for (Iterator<ProjectIconInfo> iter = prjIconInfoList.iterator(); iter.hasNext(); ) {
                ProjectIconInfo info = iter.next();
                if (!showEmpty && info.matchingIcons.size() == 0) {
                    continue;
                }
                pw.println("<p>Module name: <b>" + info.prjPath + "</b></p>");
                pw.println("<p style=\"margin-left: 20px\">");
                if (info.matchingIcons.size() == 0) {
                    pw.println("<i>--- No icons ---</i>");
                    pw.println("</p>");
                    continue;
                }
                pw.println("<table width=\"80%\"border=\"1\" cellpadding=\"3\" cellspacing=\"0\">");
                pw.println("<tr><td><b>Resource Path</b></td>" +
                        "<td align=\"center\"><b>&nbsp;New Image&nbsp;</b></td>" +
                        "<td align=\"center\"><b>&nbsp;Orig. image&nbsp;</b></td>" +
                        "<td align=\"center\"><b>&nbsp;W x H&nbsp;</b></td>" +
                        "<td align=\"center\"><b>&nbsp;Extension&nbsp;</b></td>" +
                        "<td align=\"center\"><b>&nbsp;Real Type&nbsp;</b></td></tr>");
                for (Iterator<ImageInfo> goodIter = info.matchingIcons.iterator(); goodIter.hasNext(); ) {
                    ImageInfo goodInfo = goodIter.next();
                    String iconPath = goodInfo.getPath();
                    String copiedIconPath = info.prjPath + File.separator + iconPath;
                    String originalIconPath = baseDir.getAbsolutePath() + File.separator + info.prjPath + File.separator + iconPath;
                    pw.println("<tr>");
                    pw.println("<td>");
                    pw.println("<a href=\"" + copiedIconPath + "\">" + iconPath + "</a>");
                    pw.println("</td>");
                    pw.println("<td align=\"center\">");
                    pw.println("<img src=\"" + copiedIconPath + "\"/>"); // copied image
                    pw.println("</td>");
                    pw.println("<td align=\"center\">");
                    pw.println("<img src=\"file://" + originalIconPath + "\"/>"); // original image
                    pw.println("</td>");
                    pw.println("<td align=\"center\">" + goodInfo.getWidth() + " x " + goodInfo.getHeight() + "</td>");
                    pw.println("<td align=\"center\">" + goodInfo.getExt().toUpperCase() + "</td>");
                    if (!goodInfo.getExt().equalsIgnoreCase(goodInfo.getType())) {
                        pw.println("<td align=\"center\"><font color=\"Orange\">" + goodInfo.getType() + "</font></td>");
                    } else {
                        pw.println("<td align=\"center\">" + goodInfo.getType() + "</td>");
                    }
                    pw.println("</tr>");
                }
                pw.println("</table>");
                pw.println("</p>");
            }
            pw.println("</body>");
            pw.println("</html>");
            pw.flush();
        }
        log("---> Report was written to file: " + reportFile.getAbsolutePath());
    }
        
    private void copyToDestDir(List<ProjectIconInfo> prjInfoList) {
        FileSet fs = null;
        for (Iterator<ProjectIconInfo> iter = prjInfoList.iterator(); iter.hasNext(); ) {
            fs = new FileSet();
            log("Setting basedir for fileset: " + baseDir, Project.MSG_VERBOSE);
            ProjectIconInfo prjIconInfo = iter.next();
            fs.setDir(new File(baseDir, prjIconInfo.prjPath));
            int numFilesToCopy = prjIconInfo.matchingIcons.size() + prjIconInfo.notmatchingIcons.size();
            for (Iterator<ImageInfo> matchInfoIter = prjIconInfo.matchingIcons.iterator(); matchInfoIter.hasNext(); ) {
                ImageInfo info = matchInfoIter.next();
                log("Adding file to matching fileset: " + info.getPath(), Project.MSG_VERBOSE);
                fs.setIncludes(info.getPath());
            }
            for (Iterator<ImageInfo> notmatchInfoIter = prjIconInfo.notmatchingIcons.iterator(); notmatchInfoIter.hasNext(); ) {
                ImageInfo info = notmatchInfoIter.next();
                log("Adding file to notmatching fileset: " + info.getPath(), Project.MSG_VERBOSE);
                fs.setIncludes(info.getPath());
            }
            if (numFilesToCopy > 0) {
                Copy copy = (Copy) getProject().createTask("copy");
                copy.addFileset(fs);
                File dest = new File(destDir, prjIconInfo.prjPath);
                dest.mkdir();
                copy.setTodir(dest);
                copy.init();
                copy.setLocation(getLocation());
                copy.execute();
            }
        }
    }    
    
    // -------------------------------------------------------------------------
    
    private void scanForProjectDirs(File fl) {
        if (depth > userDepth) return;
        //if (isProjectDir(fl)) {
        //    projectDirList.add(fl);
        //}
        File allFiles[] = fl.listFiles(new FileFilter() {
            public boolean accept(File pathname) {
                if (pathname.isDirectory()) {
                    return true;
                }
                return false;
            }
        });
        depth++;
        for (File f : allFiles) {
            if (isProjectDir(f)) {
                // here could be some project exclusion logic
                projectDirList.add(f);
                log(f.toString(), Project.MSG_VERBOSE);
                scanForProjectDirs(f);
            } else {
                scanForProjectDirs(f);
            }
        }
        depth--;
    }
    
    private boolean isProjectDir(File fl) {
        File prjDirs[] = fl.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                if ("nbproject".equals(name)) {
                    return true;
                }
                return false;
            }
        });
        if (prjDirs.length != 1) {
            return false;
        }
        String prjFiles[] = prjDirs[0].list(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                if ("project.xml".equals(name)) {
                    return true;
                }
                return false;
            }
        });
        if (prjFiles.length != 1) {
            return false;
        }
        return true;
    }

    private void processProjectDir(File f) {
        String prjPath = null;
        //if (f.getAbsolutePath().equals(baseDir.getAbsolutePath())) {
        //    prjPath = "";
        //} else {
            prjPath = f.getAbsolutePath().substring(baseDir.getAbsolutePath().length() + 1);
        //}
        log("Processing project dir: " + prjPath);
        DirectoryScanner ds = new DirectoryScanner();
        ds.setBasedir(f);
        ds.setIncludes(getAsArray(iconIncludes));
        ds.setExcludes(getAsArray(iconExcludes));
        ds.setCaseSensitive(false);
        ds.scan();
        String[] files = ds.getIncludedFiles();
        log("    Found " + files.length + " files in " + f);
        List<ImageInfo> goodIcons = new ArrayList<>();
        List<ImageInfo> badIcons = new ArrayList<>();
        for (String file : files) {
            String ext = file.substring(file.lastIndexOf('.') + 1);
            if (ext.equalsIgnoreCase("gif") || ext.equalsIgnoreCase("png")) {
                File iconFile = new File(f, file);
                try {
                    ImageInfo imageInfo = null;
                    imageInfo = readImageInfo(iconFile);
                    if (imageInfo != null) {
                        imageInfo.setPath(file);
                        imageInfo.setExt(ext);
                        int w = imageInfo.getWidth();
                        int h = imageInfo.getHeight();
                        if ((w == 8 && h == 8) || (w == 16 && h == 16) ||
                            (w == 24 && h == 24) || (w == 32 && h == 32)) {
                            goodIcons.add(imageInfo);
                        } else {
                            badIcons.add(imageInfo);
                        }
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
        ProjectIconInfo prjIconInfo = new ProjectIconInfo(prjPath, goodIcons, badIcons);
        prjIconInfoList.add(prjIconInfo);
    }
    
    private String[] getAsArray(String s) {
        List<String> list = new ArrayList<>();
        for (StringTokenizer stok = new StringTokenizer(s, ","); stok.hasMoreTokens(); ) {
          String token = stok.nextToken().trim();
          list.add(token);
        }
        return list.toArray(new String[list.size()]);
    }
    
}
