/*
 *
 *  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.apache.flex.compiler.internal.graph;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.flex.compiler.clients.problems.ProblemQuery;
import org.apache.flex.compiler.internal.codegen.js.goog.JSGoogEmitterTokens;
import org.apache.flex.compiler.internal.driver.js.goog.JSGoogConfiguration;
import org.apache.flex.compiler.problems.FileNotFoundProblem;
import org.apache.flex.swc.ISWC;
import org.apache.flex.swc.ISWCFileEntry;

import com.google.common.io.Files;

public class GoogDepsWriter {

    public GoogDepsWriter(File outputFolder, String mainClassName, JSGoogConfiguration config, List<ISWC> swcs)
	{
		this.outputFolderPath = outputFolder.getAbsolutePath();
		this.mainName = mainClassName;
		removeCirculars = config.getRemoveCirculars();
		otherPaths = config.getSDKJSLib();
		otherPaths.add(new File(outputFolder.getParent(), "flexjs/FlexJS/src").getPath());
		this.swcs = swcs;
		for (ISWC swc : swcs)
		{
			System.out.println("using SWC: " + swc.getSWCFile().getAbsolutePath());
		}
	}
	
	private ProblemQuery problems;
	private String outputFolderPath;
	private String mainName;
	private List<String> otherPaths;
	private List<ISWC> swcs;
	private boolean removeCirculars = false;
	private boolean problemsFound = false;
	private ArrayList<GoogDep> dps;
	
	private HashMap<String,GoogDep> depMap = new HashMap<String,GoogDep>();
	private HashMap<String, String> requireMap = new HashMap<String, String>();
	
	public ArrayList<String> getListOfFiles(ProblemQuery problems) throws InterruptedException
	{
		problemsFound = false;
		this.problems = problems;

		if (dps == null)
		{
			buildDB();
			dps = sort(mainName);
		}
		ArrayList<String> files = new ArrayList<String>();
		for (GoogDep gd : dps)
		{
			files.add(gd.filePath);
		}
		return files;
	}
	
	public boolean generateDeps(ProblemQuery problems, StringBuilder depsFileData) throws InterruptedException, FileNotFoundException
	{
	    problemsFound = false;
	    this.problems = problems;
	    if (dps == null)
	    {
	    	buildDB();
	    	dps = sort(mainName);
	    }
		String outString = "// generated by FalconJX" + "\n";
		int n = dps.size();
		for (int i = n - 1; i >= 0; i--)
		{
			GoogDep gd = dps.get(i);
			if (!isGoogClass(gd.className)) 
			{
			    String s = "goog.addDependency('";
	            s += relativePath(gd.filePath);
	            s += "', ['";
	            s += gd.className;
	            s += "'], [";
	            s += getDependencies(gd.deps);
	            s += "]);\n";
	            outString += s;
			}
		}
		depsFileData.append(outString);
		return !problemsFound; 
	}
	
	private boolean isGoogClass(String className)
	{
	    return className.startsWith("goog.");
	}
	
	private void buildDB()
	{
		addDeps(mainName);
	}
	
    public ArrayList<String> filePathsInOrder = new ArrayList<String>();
    
    public ArrayList<String> additionalHTML = new ArrayList<String>();
    
    private HashMap<String, GoogDep> visited = new HashMap<String, GoogDep>();
    
	private ArrayList<GoogDep> sort(String rootClassName)
	{
		ArrayList<GoogDep> arr = new ArrayList<GoogDep>();
		GoogDep current = depMap.get(rootClassName);
		sortFunction(current, arr);
		return arr;
	}
	
	private void sortFunction(GoogDep current, ArrayList<GoogDep> arr)
	{
		visited.put(current.className, current);
		
		filePathsInOrder.add(current.filePath);
		if (removeCirculars)
			removeCirculars(current);
        System.out.println("Dependencies calculated for '" + current.filePath + "'");

		ArrayList<String> deps = current.deps;
		for (String className : deps)
		{
			if (!visited.containsKey(className) && !isGoogClass(className))
			{
				GoogDep gd = depMap.get(className);
				sortFunction(gd, arr);
			}
		}
		arr.add(current);
	}
	
	private void addDeps(String className)
	{
		if (depMap.containsKey(className) || isGoogClass(className))
			return;
		
		// build goog dependency list
		GoogDep gd = new GoogDep();
		gd.className = className;
		gd.filePath = getFilePath(className);
		if(gd.filePath.isEmpty()) {
			// TODO Send a ICompilerProblem instead.
			throw new RuntimeException("Unable to find JavaScript filePath for class: " + className);
		}
		depMap.put(gd.className, gd);
        List<String> fileLines;
		try {
			fileLines = Files.readLines(new File(gd.filePath), Charset.defaultCharset());
            FileInfo fi = getFileInfo(fileLines, className);
			gd.fileInfo = fi;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		ArrayList<String> deps = getDirectDependencies(gd.filePath);
		
		gd.deps = new ArrayList<String>();
		for (String dep : deps)
		{
			if (gd.fileInfo.impls != null && gd.fileInfo.impls.contains(dep))
			{
				if (!requireMap.containsKey(dep))
				{
					// we are first class that needs this dependency
					// at prototype initialization time
					requireMap.put(dep, className);
				}
			}
			else if (depMap.containsKey(dep) && !isGoogClass(dep))
		    {
		        continue;
		    }
			gd.deps.add(dep);
		}
        for (String dep : deps)
        {
            addDeps(dep);
        }
	}
	
	void removeCirculars(GoogDep gd)
	{
		String className = gd.className;
		
	    // remove requires that would cause circularity
	    try
        {
            List<String> fileLines = Files.readLines(new File(gd.filePath), Charset.defaultCharset());
            ArrayList<String> finalLines = new ArrayList<String>();
            
            FileInfo fi = gd.fileInfo;
            int suppressCount = 0;
            int i = 0;
            for (String line : fileLines)
            {
            	if (i < fi.constructorLine)
            	{
                    int c = line.indexOf(JSGoogEmitterTokens.GOOG_REQUIRE.getToken());
                    if (c > -1)
                    {
                        int c2 = line.indexOf(")");
                        String s = line.substring(c + 14, c2 - 1);
                        if (gd.fileInfo.impls == null || !gd.fileInfo.impls.contains(s))
                        {
	                        if (requireMap.containsKey(s) && requireMap.get(s) != className)
	                        {
	                        	// don't add the require if some class needs it at static initialization
	                        	// time and that class is not this class
	                        	suppressCount++;
	                        	System.out.println(gd.filePath + " removing circular (static): " + s);
	                        	continue;
	                        }
	                        else if (!gd.deps.contains(s))
	                        {
	                        	// someone require'd this class
	                        	suppressCount++;
	                        	System.out.println(gd.filePath + " removing circular: " + s);
	                        	continue;
	                        }
                        }
                    }
            	}
                finalLines.add(line);
                i++;
            }
            if (suppressCount > 0)
            {
            	if (fi.suppressLine > 0)
            	{
            		if (fi.suppressLine < fi.constructorLine) 
            		{
                		String line = finalLines.get(fi.suppressLine);
                		int c = line.indexOf("@suppress {");
                		if (c > -1)
                		{
                			if (!line.contains("missingRequire"))
                			{
                				line = line.substring(0, c) + "@suppress {missingRequire|" + line.substring(c + 11);
                				finalLines.remove(fi.suppressLine);
                				finalLines.add(fi.suppressLine, line);
                			}
                		}
                		else
                			System.out.println("Confused by @suppress in " + className);
                	}
                	else                		
                	{
                		// the @suppress was for the constructor or some other thing so add a top-level
                		// @suppress
                		if (fi.fileoverviewLine > -1)
                		{
                			// there is already a fileOverview but no @suppress
                			finalLines.add(fi.fileoverviewLine + 1, " *  @suppress {missingRequire}");
                		}
                		else if (fi.googProvideLine > -1)
                		{
                			finalLines.add(fi.googProvideLine, " */");
                			finalLines.add(fi.googProvideLine, " *  @suppress {missingRequire}");
                			finalLines.add(fi.googProvideLine, " *  @fileoverview");
                			finalLines.add(fi.googProvideLine, "/**");
                		}
                		else
                		{
                			System.out.println("Confused by @suppress in " + className);
                		}
                	}
            	}
            	else
            	{
            		if (fi.fileoverviewLine > -1)
            		{
            			// there is already a fileoverview but no @suppress
            			finalLines.add(fi.fileoverviewLine + 1, " *  @suppress {missingRequire}");
            		}
            		else if (fi.googProvideLine > -1)
            		{
            			finalLines.add(fi.googProvideLine, " */");
            			finalLines.add(fi.googProvideLine, " *  @suppress {missingRequire}");
            			finalLines.add(fi.googProvideLine, " *  @fileoverview");
            			finalLines.add(fi.googProvideLine, "/**");
            		}
            		else
            		{
            			System.out.println("Confused by @suppress in " + className);
            		}                		
            	}
            }
            File file = new File(gd.filePath);  
            PrintWriter out = new PrintWriter(new FileWriter(file));  
            for (String s : finalLines)
            {
                out.println(s);
            }
            out.close();
                
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }		
	}
	
	FileInfo getFileInfo(List<String> lines, String className)
	{
		FileInfo fi = new FileInfo();
		
	    int n = lines.size();
	    fi.constructorLine = n;
	    fi.suppressLine = -1;
	    fi.fileoverviewLine = -1;
	    fi.googProvideLine = -1;
	    for (int i = 0; i < n; i++)
	    {
	        String line = lines.get(i);
	        int c2;
	        int c = line.indexOf("goog.inherits");
	        if (c > -1)
	        {
	            String inheritLine = ""; 
                while (true)
                {
                    inheritLine += line;
                    c2 = line.indexOf(")");
                    if (c2 > -1)
                        break;
                    else
                    {
                        i++;
                        line = lines.get(i);
                    }
                }
	            c = inheritLine.indexOf(",");
                c2 = inheritLine.indexOf(")");
                fi.inherits = inheritLine.substring(c + 1, c2).trim();
                return fi;
	        }
	        else
	        {
		        c = line.indexOf("@constructor");
		        if (c > -1)
		        	fi.constructorLine = i;
		        else
		        {
			        c = line.indexOf("@interface");
			        if (c > -1)
			        	fi.constructorLine = i;
			        else
			        {
			        	c = line.indexOf("@suppress");
			        	if (c > -1)
			        		fi.suppressLine = i;
			        	else
			        	{
				        	c = line.indexOf("@fileoverview");
				        	if (c > -1)
				        		fi.fileoverviewLine = i;
				        	else
				        	{
					        	c = line.indexOf("goog.provide");
					        	if (c > -1)
					        		fi.googProvideLine = i;
					        	else
					        	{
					        		c = line.indexOf("@implements");
					        		if (c > -1)
					        		{
					        			if (fi.impls == null)
					        				fi.impls = new ArrayList<String>();
					        			c2 = line.indexOf("}", c);
					        			String impl = line.substring(c + 13, c2);
					        			fi.impls.add(impl);
					        		}
					        		else
					        		{
						        		c = line.indexOf("@extends");
						        		if (c > -1)
						        		{
						        			if (fi.impls == null)
						        				fi.impls = new ArrayList<String>();
						        			c2 = line.indexOf("}", c);
						        			String impl = line.substring(c + 10, c2);
						        			fi.impls.add(impl);
						        		}					        			
					        		}
					        	}
				        	}
			        	}
			        }
		        }
	        }
	    }
	    return fi;
	}
	
	String getFilePath(String className)
	{
	    String fn;
	    File destFile;
	    File f;
	    
		String classPath = className.replace(".", File.separator);
		// special case app names with underscores, but hope that
		// no other class names have underscores in them
        if (className.equals(mainName))
        	classPath = className;
        
        fn = outputFolderPath + File.separator + classPath + ".js";
        f = new File(fn);
        if (f.exists())
        {
            return fn;
        }
        
        for (String otherPath : otherPaths)
        {
    		fn = otherPath + File.separator + classPath + ".js";
    		f = new File(fn);
    		if (f.exists())
    		{
    			fn = outputFolderPath + File.separator + classPath + ".js";
    			destFile = new File(fn);
    			// copy source to output
    			try {
    				FileUtils.copyFile(f, destFile);
    				
    				// (erikdebruin) copy class assets files
    				if (className.contains("org.apache.flex"))
    				{
    				    File assetsDir = new File(f.getParentFile(), "assets");
    				    if (assetsDir.exists())
    				    {
    				        String nameOfClass = className.substring(className.lastIndexOf('_') + 1);
    				        
    				        File[] assetsList = assetsDir.listFiles();
					        assert assetsList != null;
					        for (File assetFile : assetsList) {
						        String assetFileName = assetFile.getName();

						        if (assetFile.isFile() && assetFileName.indexOf(nameOfClass) == 0) {
							        String pathOfClass;
							        pathOfClass = className.substring(0, className.lastIndexOf('_'));
							        pathOfClass = pathOfClass.replace(".", File.separator);

							        destFile = new File(outputFolderPath +
									        File.separator + pathOfClass +
									        File.separator + "assets" +
									        File.separator + assetFileName);
							        FileUtils.copyFile(assetFile, destFile);

							        destFile = new File(outputFolderPath.replace("js-debug", "js-release") +
									        File.separator + pathOfClass +
									        File.separator + "assets" +
									        File.separator + assetFileName);
							        FileUtils.copyFile(assetFile, destFile);

							        System.out.println("Copied assets of the '" + nameOfClass + "' class");
						        }
					        }
    				    }
    				}
    			} catch (IOException e) {
    				System.out.println("Error copying file for class: " + className);
    			}
    			return fn;
    		}
        }

		String fwdClassPath = className.replace(".", "/");
		String bckClassPath = className.replace(".", "\\");
        for (ISWC swc : swcs)
        {
        	ISWCFileEntry fileEntry =  swc.getFile("js/src/" + fwdClassPath + ".js");
        	if (fileEntry == null)
        		fileEntry = swc.getFile("js/out/" + fwdClassPath + ".js");
        	if (fileEntry == null)
        		fileEntry = swc.getFile("js/src/" + bckClassPath + ".js");
        	if (fileEntry == null)
        		fileEntry = swc.getFile("js/out/" + bckClassPath + ".js");
            if (fileEntry == null)
                fileEntry = swc.getFile("js\\src\\" + bckClassPath + ".js");
            if (fileEntry == null)
                fileEntry = swc.getFile("js\\out\\" + bckClassPath + ".js");
    		if (fileEntry != null)
    		{
    			fn = outputFolderPath + File.separator + classPath + ".js";
    			destFile = new File(fn);
    			// copy source to output
    			try {
    				InputStream inStream = fileEntry.createInputStream();
    				OutputStream outStream = FileUtils.openOutputStream(destFile);
    				byte[] b = new byte[1024 * 1024];
    				int bytes_read;
    				while ((bytes_read = inStream.read(b)) != -1)
    				{
        				outStream.write(b, 0, bytes_read);
    				}
    				outStream.flush();
    				outStream.close();    					
    				inStream.close();

    				// (erikdebruin) copy class assets files
    				if (className.contains("org.apache.flex"))
    				{
    					Map<String, ISWCFileEntry> includedfiles = swc.getFiles();
    					Set<String> includedList = includedfiles.keySet();
    					for (String included : includedList)
    					{
    						if (included.contains(".png") ||
    							included.contains(".gif") ||
    							included.contains(".jpg") ||
    							included.contains(".json"))
    						{
    							fileEntry = includedfiles.get(included);
    			    			String assetName = outputFolderPath + File.separator + included;
    			    			File assetFile = new File(assetName);
    		    				inStream = fileEntry.createInputStream();
    		    				outStream = FileUtils.openOutputStream(assetFile);
    		    				b = new byte[inStream.available()];
    		    				inStream.read(b);
    		    				outStream.write(b);
    		    				inStream.close();
    		    				outStream.flush();
    		    				outStream.close();
						        System.out.println("Copied asset " + assetName);
    						}
    					}
    				}
    			} catch (IOException e) {
    				System.out.println("Error copying file for class: " + className);
    			}
    			return fn;
    		}
        }
        
		System.out.println("Could not find file for class: " + className);
		problems.add(new FileNotFoundProblem(className));
		problemsFound = true;
		return "";
	}
	
	private ArrayList<String> getDirectDependencies(String fn)
	{
		ArrayList<String> deps = new ArrayList<String>();
		
		FileInputStream fis;
		try {
			fis = new FileInputStream(fn);
			Scanner scanner = new Scanner(fis, "UTF-8");
			boolean inInjectHTML = false;
			while (scanner.hasNextLine())
			{
				String s = scanner.nextLine();
				if (s.contains("goog.inherits"))
					break;
                if (inInjectHTML)
                {
                    int c = s.indexOf("</inject_html>");
                    if (c > -1)
                    {
                        inInjectHTML = false;
                        continue;
                    }
                }    
                if (inInjectHTML)
                {
                	s = s.trim();
                	if (s.startsWith("*"))
                		s = s.substring(1);
				    additionalHTML.add(s);
				    continue;
                }
				int c = s.indexOf(JSGoogEmitterTokens.GOOG_REQUIRE.getToken());
				if (c > -1)
				{
					int c2 = s.indexOf(")");
					s = s.substring(c + 14, c2 - 1);
					deps.add(s);
				}
                c = s.indexOf("<inject_html>");
                if (c > -1)
                {
                    inInjectHTML = true;
                }
			}
			scanner.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		return deps;
	}
	
	private String getDependencies(ArrayList<String> deps)
	{
		String s = "";
		for (String dep : deps)
		{
			if (s.length() > 0)
			{
				s += ", ";
			}
			s += "'" + dep + "'";			
		}
		return s;
	}

	String relativePath(String path)
	{
        if (path.indexOf(outputFolderPath) == 0)
        {
            path = path.replace(outputFolderPath, "../../..");
        }
        else
        {
    	    for (String otherPath : otherPaths)
    	    {
        		if (path.indexOf(otherPath) == 0)
        		{
        			path = path.replace(otherPath, "../../..");
        			
        		}
    	    }
        }
		// paths are actually URIs and always have forward slashes
		path = path.replace('\\', '/');
		return path;
	}
	private class GoogDep
	{
		public String filePath;
		public String className;
		public ArrayList<String> deps;
		public FileInfo fileInfo;
		
	}
	
	@SuppressWarnings( "unused" )
	private class FileInfo
	{
		public String inherits;
		public ArrayList<String> impls;
		public int constructorLine;
		public int suppressLine;
		public int fileoverviewLine;
		public int googProvideLine;
	}
}
