package org.projectodd.nodyn.modules;

import static org.apache.commons.lang3.SystemUtils.FILE_SEPARATOR;
import static org.apache.commons.lang3.SystemUtils.USER_DIR;
import static org.apache.commons.lang3.SystemUtils.USER_HOME;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;

import org.dynjs.runtime.DynJS;
import org.dynjs.runtime.ExecutionContext;
import org.dynjs.runtime.GlobalObject;
import org.dynjs.runtime.builtins.Require;
import org.dynjs.runtime.modules.FilesystemModuleProvider;
import org.vertx.java.core.json.JsonObject;
import org.projectodd.nodyn.process.Process;

/**
 * Extends DynJS FilesystemModuleProvider to support Node.js Modules API
 * This module provider adds support specifically for loading from
 * node_modules folders and modules as folders.
 *
 * @see http://nodejs.org/api/modules.html
 * @see http://nodejs.org/api/modules.html#modules_loading_from_node_modules_folders
 * @see http://nodejs.org/api/modules.html#modules_folders_as_modules
 */
public class NpmModuleProvider extends FilesystemModuleProvider {

	private static final String NODE_MODULES = "node_modules";
	
    private Require require;

    public NpmModuleProvider(GlobalObject globalObject) {
        require = (Require) globalObject.get("require");
        if (require != null) {
            // global npm modules

        	// Ideally the Process instance should be injected, easier testing
			if (new Process().isWindows()) {
				String appdata = System.getenv("APPDATA");
				if (isNotBlank(appdata)) {
					require.pushLoadPath(appdata + FILE_SEPARATOR + "npm"
							+ FILE_SEPARATOR + NODE_MODULES);
				}
			} else {
				require.pushLoadPath("/usr/local/lib/" + NODE_MODULES);
			}

            // npm modules in $HOME
            require.pushLoadPath(USER_HOME + FILE_SEPARATOR + "." + NODE_MODULES);
            require.pushLoadPath(USER_HOME + FILE_SEPARATOR + NODE_MODULES);

            // npm modules in cwd
            require.pushLoadPath(USER_DIR + FILE_SEPARATOR + NODE_MODULES);

            // Inform dynjs that we exist
            require.addModuleProvider(this);
        } else {
            System.err.println("Can't find require() function");
        }
    }

    @Override
    protected boolean load(DynJS runtime, ExecutionContext context, String moduleID) {
        File file = new File(moduleID);
        if (file.exists()) {
            List<String> pathsToRoot = getLoadPathsToRoot(file.getParent());
            for (String path: pathsToRoot) {
                runtime.evaluate("require.pushLoadPath('" + path + "')");
            }
            runtime.evaluate("require.pushLoadPath('" + file.getParent() + "')");
            try {
//                System.err.println("Loading: " + file.getAbsolutePath());
                runtime.newRunner().withContext(context).withSource(file).execute();
                return true;
            } catch (IOException e) {
                System.err.println("There was an error loading the module " + moduleID + ". Error message: " + e.getMessage());
            } finally {
                runtime.evaluate("require.removeLoadPath('" + file.getParent() + "')");
                for(String path: pathsToRoot) {
                    runtime.evaluate("require.removeLoadPath('" + path + "')");
                }
            }
        }
        return false;
    }


    @Override
    protected File findFile(List<String> loadPaths, String moduleName) {
        String fileName = normalizeName(moduleName);
        File file = null;

        for (String loadPath: loadPaths) {
//            System.err.println("Looking in " + loadPath + " for " + moduleName);
            file = new File(loadPath, fileName);
            if (file.exists()) { return file; }

            // moduleName.js wasn't found as a file. Look for a directory instead.
            // http://nodejs.org/api/modules.html#modules_folders_as_modules
            // first check to see if there is a package.json in the directory
            File pkg = new File(loadPath + "/" + moduleName, "package.json");
            if (pkg.exists()) {
                // load the JSON and find the main module file to consider
                try {
                    String jsonString = new Scanner(pkg).useDelimiter("\\A").next();
                    JsonObject jsonObject = new JsonObject(jsonString);

                    // If there is no main in package.json, default to index.js
                    String moduleMain = jsonObject.getString("main", "index.js");

                    return new File(loadPath + "/" + moduleName, normalizeName(moduleMain));
                } catch (FileNotFoundException e) {
                    System.err.println("Error loading " + pkg.getAbsolutePath());
                }
            } else {
                // The last thing we look for is index.js in the module path
                file = new File(loadPath + "/" + moduleName, "index.js");
                if (file.exists()) return file;
            }
        }
        return file;
    }

    // npm modules in CWD up to root
    // http://nodejs.org/api/modules.html#modules_loading_from_node_modules_folders
    private List<String> getLoadPathsToRoot(String currentDir) {
        LinkedList<String> list = new LinkedList<>();
        File parent = new File(currentDir);
        while (parent != null) {
            list.push(parent.getAbsolutePath() + "/node_modules");
            parent = parent.getParentFile();
        }
        return list;
    }
}


@@ -1222,17 +1222,20 @@
 ider %7B%0A%0A
-%09
+    
 private 
@@ -1285,17 +1285,16 @@
 dules%22;%0A
-%09
 %0A    pri
@@ -1505,17 +1505,20 @@
         
-%09
+    
 // Ideal
@@ -1576,19 +1576,28 @@
 testing%0A
-%09%09%09
+            
 if (new 
@@ -1621,20 +1621,32 @@
 ws()) %7B%0A
-%09%09%09%09
+                
 String a
@@ -1680,20 +1680,32 @@
 DATA%22);%0A
-%09%09%09%09
+                
 if (isNo
@@ -1723,21 +1723,36 @@
 ata)) %7B%0A
-%09%09%09%09%09
+                    
 require.
@@ -1801,15 +1801,36 @@
 pm%22%0A
-%09%09%09%09%09%09%09
+                            
 + FI
@@ -1863,30 +1863,63 @@
 S);%0A
-%09%09%09%09%7D%0A%09%09%09%7D else %7B%0A%09%09%09%09
+                %7D%0A            %7D else %7B%0A                
 requ
@@ -1974,11 +1974,20 @@
 S);%0A
-%09%09%09
+            
 %7D%0A%0A 
@@ -2746,32 +2746,33 @@
 for (String path
+ 
 : pathsToRoot) %7B
@@ -3447,16 +3447,17 @@
      for
+ 
 (String 
@@ -3460,16 +3460,17 @@
 ing path
+ 
 : pathsT
@@ -3825,16 +3825,17 @@
 loadPath
+ 
 : loadPa
@@ -4005,16 +4005,32 @@
 sts()) %7B
+%0A               
  return 
@@ -4034,16 +4034,28 @@
 rn file;
+%0A           
  %7D%0A%0A    

Reformat code