/*

SPDX-FileCopyrightText: Copyright 2021 Siemens
SPDX-License: GPL-3.0-only
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.

This program 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 for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.


 */


package javareporter;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import org.jd.core.v1.ClassFileToJavaSourceDecompiler;




public class JavaBomReporter {

	static private List<File> ProcessedExcludedFiles = new ArrayList<File>();
	static private List<File> FilesToDecompile = new ArrayList<File>();
	static public Arguments arguments = new Arguments();
	
    static public void Reset()
    {
    	arguments.Reset();
    	ProcessedExcludedFiles = new ArrayList<File>();
    	FilesToDecompile = new ArrayList<File>();
    }
    
    //getters for unittests
    static public List<File> GetProcessedFiles()
    {
    	return FilesToDecompile;
    }
    static public List<File> GetProcessedExcludeFiles()
    {
    	return ProcessedExcludedFiles;
    }
	
	public enum ClassOrJar
    {
        CLASS,
        JAR,
        //Default case, process both classes and jars
        BOTH
    }
    
    public static class Arguments
    {
    	public Arguments() { 
    		directories = new ArrayList<String>();
    		classOrJar = ClassOrJar.BOTH;
    		excludeDirectories = new ArrayList<String>();
    		outputFile = "";
    		recursive = false;
    		}
    	
    	public void Reset()
    	{

    		directories = new ArrayList<String>();
    		classOrJar = ClassOrJar.BOTH;
    		excludeDirectories = new ArrayList<String>();
    		outputFile = "";
    		recursive = false;
    	}
        public List<String> directories = new ArrayList<String>();
        public ClassOrJar classOrJar = ClassOrJar.BOTH;
        public List<String> excludeDirectories = new ArrayList<String>();
        public String outputFile = "";
        public boolean recursive = false;
    }
    
    
    private static void DisplayVersion()
    {
        System.out.println("JavaBOMReporter Version: " + Utils.GetToolVersion() + "   Build: " + Utils.GetBuildVersion());
    }
    
    private static void DisplayHelp()
    {
        DisplayVersion();
        System.out.println("Usage: java -jar JavaBomReporter.jar -d <paths and/or jar/class files> -o <output file name> "
                + "[-jar | -class] [-r] [-exclude <paths and/or files>]");
        
        System.out.println("Required Arguments: ");
        System.out.println("    -d <paths and/or files>       -  Full path of directory containing "
                + "jars and/or class files.  Both class and jar files will be processed, "
                + "unless the -class or -jar option is specified.  It is an error to specify both "
                + "-class and -jar.");
        
        System.out.println("    -o <output filename>          - This will control the output json file name. "
                + "Will accept a full path.  If only a filename is given, it will go to the current working directory.");
        
        System.out.println("Optional Arguments: ");
        System.out.println("    -class                        - Indicates to only process class files");
        System.out.println("    -jar                          - Indicates to only process jar files");
        System.out.println("    -exclude <paths and/or files> - Indicates to exclude given files/directories.  "
        		+ "Directories will be processed recursively");
        System.out.println("    -r                            - Recursively search directories");
        System.out.println("    -v                            - Display the version of JavaBOMReporter");
        System.out.println("    -h                            - Display help");
        
        
    }
    
    private static void AddToList(File file, List<File> allFiles)
    {
    	if ((arguments.classOrJar == ClassOrJar.BOTH && (file.getName().endsWith(".class") || file.getName().endsWith(".jar"))
    			|| (arguments.classOrJar == ClassOrJar.CLASS && file.getName().endsWith(".class"))
    			|| (arguments.classOrJar == ClassOrJar.JAR && file.getName().endsWith(".jar")))
    			&& !ProcessedExcludedFiles.contains(file)
    			)
    	{
    		allFiles.add(file);
    	}
    }
    
    private static List<File> ProcessDirectories(List<String> filesOrDirs, boolean recursive)
    {
    	List<File> allFiles = new ArrayList<File>();
    	
        for (String fileOrDir : filesOrDirs)
        {
            File file = new File(fileOrDir);
            if (file.exists())
            {
                if (file.isDirectory())
                {
                	if (recursive)
                    {
                		try 
                		{
							Files.walk(file.toPath()).forEach(path -> AddToList(path.toFile(), allFiles));
						} 
                		catch (IOException e) {
							e.printStackTrace();
						}
                    }
                	else
                	{
                		for (File directoryFiles : file.listFiles())
                		{
                			 AddToList(directoryFiles, allFiles);
                		}
                	}
                }
                else
                {
                	AddToList(file, allFiles);
                }
            }
            else
            {
                Utils.Error("Invalid Directory: " + fileOrDir);
            }
        }
        
        return allFiles;
    }
    
    
    public static void ProcessArguments(String[] args)
    {
    	for (int i = 0; i < args.length;)
        {
            if (args[i].equals("-h"))
            {
                DisplayHelp();
                System.exit(0);
            }
            else if (args[i].equals("-v"))
            {
                DisplayVersion();
                System.exit(0);
            }
            else if (args[i].equals("-d"))
            {
                i++;
                while (i < args.length && !args[i].startsWith("-"))
                {
                	arguments.directories.add(args[i]);
                    i++;
                }
            }
            else if (args[i].equals("-o"))
            {
            	i++;
            	if (i < args.length)
                {
            		arguments.outputFile = args[i];
                }
            	
            	i++;
            }
            else if (args[i].equals("-class"))
            {
                if (arguments.classOrJar != ClassOrJar.BOTH)
                {
                	Utils.Error("ERROR: only -class or -jar may be specified");
                }
                
                arguments.classOrJar = ClassOrJar.CLASS;
                i++;
            }
            else if (args[i].equals("-jar"))
            {
                if (arguments.classOrJar != ClassOrJar.BOTH)
                {
                	Utils.Error("ERROR: only -class or -jar may be specified");
                }
                
                arguments.classOrJar = ClassOrJar.JAR;
                i++;
            }
            else if (args[i].equals("-exclude"))
            {
                i++;
                while (i < args.length && !args[i].startsWith("-"))
                {
                	arguments.excludeDirectories.add(args[i]);
                    i++;
                }
            }
            else if (args[i].equals("-r"))
            {
            	arguments.recursive = true;
                i++;
            }
            else
            {
            	DisplayHelp();
            	Utils.Error("ERROR: Unrecognized argument " + args[i]);
            }
        }
    }
    
    private static void DecompileJar(File file) throws Exception
    {
    	NXOpenPrinter printer = new NXOpenPrinter();
        
        JarAndClassLoader jarLoader = new JarAndClassLoader(file, true);
        
        HashMap<String, byte[]> jarMap = jarLoader.getMap();
        
        
        ClassFileToJavaSourceDecompiler jardecompiler = new ClassFileToJavaSourceDecompiler();
        
        String filePath = file.getAbsolutePath();
        
        if (Utils.GetIsRunningUnittest())
        {
        	filePath = file.getName();
        }
        
        printer.SetCurrentClassorJarFile(filePath, true);
        
        for (Map.Entry<String, byte[]> entry : jarMap.entrySet())
        {
            if (entry.getKey().endsWith(".class"))
            {
            	printer.SetCurrentClassorJarFile(entry.getKey(), false);
                jardecompiler.decompile(jarLoader, printer, entry.getKey());
            }
        }
    }
    
    private static void DecompileClass(File file) throws Exception
    {
    	ClassFileToJavaSourceDecompiler decompiler = new ClassFileToJavaSourceDecompiler();
    	
    	
    	JarAndClassLoader classLoader = new JarAndClassLoader(file, false);
        NXOpenPrinter classPrinter = new NXOpenPrinter();
        classPrinter.SetCurrentClassorJarFile("", true);
        
        String filePath = file.getAbsolutePath();
        
        if (Utils.GetIsRunningUnittest())
        {
        	filePath = file.getName();
        }
        
        classPrinter.SetCurrentClassorJarFile(filePath, false);
        
        try
        {
        	decompiler.decompile(classLoader, classPrinter, file.getName());
        }
        catch (Exception e)
        {
        	System.out.println("Exception encountered while processing " + filePath + ".  Skipping file.  "
        			+ "If this is an enum class, this is a known limitation of jd-core.");
        }
    }
    
    public static void DecompileFiles() throws Exception
    {
    	for (File file : FilesToDecompile)
        {
        	if (file.getName().endsWith(".jar"))
        	{
        		
        		DecompileJar(file);
        	}
        	else if (file.getName().endsWith(".class"))
        	{
        		DecompileClass(file);
                
        	}
        }
     }
    
    
    public static void PrintToJson() throws IOException
    {

        NXOpenPrinter fullprinter = new NXOpenPrinter();
        String source = fullprinter.printAsJson();
        
        //Useful for debug, will print the decompiled file to console
        //System.out.println(fullprinter.toString());
        if (!arguments.outputFile.endsWith(".json"))
        {
        	arguments.outputFile += ".json";
        }
        File output = new File(arguments.outputFile);
        
        if (output.exists())
        {
        	boolean inputprocessed = false;
        	while (!inputprocessed)
        	{
        		System.out.println("Output file " + arguments.outputFile + " already exists");
	            System.out.println("Overwrite this file? (y/n)");
	        	Scanner myObj = new Scanner(System.in);
	        	
	        	String input = myObj.nextLine();
	        	
	        	if (input.toLowerCase().equals("n"))
	        	{
	        		myObj.close();
	        		Utils.Error("exiting");
	        	}
	        	else if (input.toLowerCase().equals("y"))
	        	{
	        		inputprocessed = true;
	        	}
	        	myObj.close();
        	}
        	
        	output.delete();
        	output = new File(arguments.outputFile);
        }

        if (output.getParentFile() != null && !output.getParentFile().exists())
        {
            output.getParentFile().mkdirs();
        }
        boolean success =output.createNewFile();
        if (success)
        {
        	
        	FileWriter fw = new FileWriter(output, false);
        	fw.write(source);
        	fw.close();
        }
        else
        {
        	System.out.println("Error creating output file");
        }
        
        
    }
    
    public static void PopulateDirectories()
    {
        //process exclude directories first
        if (arguments.excludeDirectories.size() != 0)
        {
        	ProcessedExcludedFiles = ProcessDirectories(arguments.excludeDirectories, true);
        }
        
        if (arguments.directories.size() != 0)
        {
        	FilesToDecompile = ProcessDirectories(arguments.directories, arguments.recursive);
        }
        else
        {
        	Utils.Error("ERROR: No directories specified.  See -h for usage.");
        }
        
        if (arguments.outputFile.equals(""))
        {
        	Utils.Error("ERROR: No output file given.  See -h for usage.");
        }
    }
    
    public static void main(String[] args) throws Exception {
        
    	ProcessArguments(args);
    	PopulateDirectories();
        DecompileFiles();
        PrintToJson();
    }
        
    
}
