/*
 *
 *  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 as;

import org.apache.royale.compiler.clients.MXMLC;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.swf.io.SWFDump;
import org.apache.royale.utils.*;
import utils.FlashplayerSecurityHandler;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;


/**
 * Base class for AS feature tests which compile AS code with MXMLC and run it in the standalone Flash Player.
 * Copied and modified from MXMLFeatureTestsBase.java
 */
@SuppressWarnings("deprecation")
public class ASFeatureTestsBase
{
	private static boolean generateResultFile = false;
	
	public ASFeatureTestsBase()
	{
        ITestAdapter testAdapter = TestAdapterFactory.getTestAdapter();
		File playerExecutable = testAdapter.getFlashplayerDebugger();
		if(playerExecutable == null || !playerExecutable.isFile() || !playerExecutable.exists()) {
			hasFlashPlayerExecutable = false;
		}
	    File playerGlobal = testAdapter.getPlayerglobal();
		if(playerGlobal == null || !playerGlobal.isFile() || !playerGlobal.exists()) {
			hasFlashPlayerGlobal = false;
		}

		if (hasFlashPlayerExecutable && !hasFlashPlayerGlobal)
		{
			String message = "Fatal Error: If FLASHPLAYER_DEBUGGER is defined, playerglobal must be available";
			System.err.println(message);
			fail(message);
		}

		if (!hasFlashPlayerExecutable && hasFlashPlayerGlobal)
		{
			String message = "Fatal Error: If playerglobal is available, FLASHPLAYER_DEBUGGER is required";
			System.err.println(message);
			fail(message);
		}
	}

	protected boolean hasFlashPlayerExecutable = true;
	protected boolean hasFlashPlayerGlobal = true;
	
	private static final String NAMESPACE_2009 = "http://ns.adobe.com/mxml/2009";

	protected File generateTempFile(String source)
	{
        // Write the MXML into a temp file.
        ITestAdapter testAdapter = TestAdapterFactory.getTestAdapter();
        String tempDir = testAdapter.getTempDir();
        File tempASFile = null;
        try
        {
            tempASFile = File.createTempFile(getClass().getSimpleName(), ".as", new File(tempDir));
            tempASFile.deleteOnExit();

            BufferedWriter out = new BufferedWriter(new FileWriter(tempASFile));
            String className = tempASFile.getName();
            // chop off .as
            className = className.substring(0, className.length() - 3);
            
            source = source.replaceAll("%0", className);
            out.write(source);
            out.close();
        }
        catch (IOException e1) 
        {
            e1.printStackTrace();
            fail("Error generating test code");
        }
        return tempASFile;
	}
	
	protected String compile(File tempASFile, String source, boolean withFramework, boolean withRPC, boolean withSpark, String[] otherOptions, boolean checkExitCode)
	{
        System.out.println("Generating test:");

        ITestAdapter testAdapter = TestAdapterFactory.getTestAdapter();
        // Build the list of SWCs to compile against on the library path.
        List<String> swcs = new ArrayList<String>();
        if (withFramework)
        {
            swcs.add(testAdapter.getFlexArtifact("framework").getPath());
            swcs.add(testAdapter.getFlexArtifactResourceBundle("framework").getPath());
        }
        if (withRPC)
        {
            swcs.add(testAdapter.getFlexArtifact("rpc").getPath());
            swcs.add(testAdapter.getFlexArtifactResourceBundle("rpc").getPath());
        }
        if (withSpark)
        {
            swcs.add(testAdapter.getFlexArtifact("spark").getPath());
            swcs.add(testAdapter.getFlexArtifactResourceBundle("spark").getPath());
        }

        List<String> args = new ArrayList<String>();
        //args.add("-debug");
        if (hasFlashPlayerGlobal)
        	args.add("-external-library-path=" + testAdapter.getPlayerglobal().getPath());
        else {
        	String jsSwcPath = FilenameNormalization.normalize("../compiler-externc/target/js.swc");
        	args.add("-external-library-path=" + jsSwcPath);
        }
        if(swcs.size() > 0) {
            String libraryPath = "-library-path=" + StringUtils.join(swcs.toArray(new String[swcs.size()]), ",");
            args.add(libraryPath);
        }
        if (withFramework || withRPC || withSpark)
            args.add("-namespace=" + NAMESPACE_2009 + "," + testAdapter.getFlexManifestPath("mxml-2009"));
        if (otherOptions != null)
        {
            Collections.addAll(args, otherOptions);
        }
        args.add(tempASFile.getAbsolutePath());

        // Use MXMLC to compile the MXML file against playerglobal.swc and possibly other SWCs.
        MXMLC mxmlc = new MXMLC();
        StringBuffer cmdLine = new StringBuffer();
        for(String arg : args) {
            cmdLine.append(arg).append(" ");
        }
	    
        System.out.println("Compiling test:\n" + cmdLine.toString());
        int exitCode = mxmlc.mainNoExit(args.toArray(new String[args.size()]));

        // Check that there were no compilation problems.
        List<ICompilerProblem> problems = mxmlc.getProblems().getProblems();
        StringBuilder sb = new StringBuilder(checkExitCode && problems.size() > 0 ? "Unexpected compilation problems:\n" : "");
        for (ICompilerProblem problem : problems)
        {
            sb.append(problem.toString());
            sb.append('\n');
        }
        
        System.out.println("After compile:\n" + sb.toString());
        if (checkExitCode)
            assertThat(sb.toString(), exitCode, is(0));
        return sb.toString();

	}
	
    protected void compileAndExpectErrors(String source, boolean withFramework, boolean withRPC, boolean withSpark, String[] otherOptions, String errors)
    {
        File tempASFile = generateTempFile(source);
        String results = compile(tempASFile, source, withFramework, withRPC, withSpark, otherOptions, false);
        assertThat(results, is(errors));
    }

	protected void compileAndRun(String source, boolean withFramework, boolean withRPC, boolean withSpark, String[] otherOptions)
	{
	    int exitCode = 0;
	    File tempASFile = generateTempFile(source);
	    compile(tempASFile, source, withFramework, withRPC, withSpark, otherOptions, true);
        ITestAdapter testAdapter = TestAdapterFactory.getTestAdapter();
		// Check the existence of the flashplayer executable
		// Run the SWF in the standalone player amd wait until the SWF calls System.exit().
		String swf = FilenameNormalization.normalize(tempASFile.getAbsolutePath());
		swf = swf.replace(".as", ".swf");
		if (hasFlashPlayerExecutable)
		{
			File playerExecutable = testAdapter.getFlashplayerDebugger();
			String[] runArgs = new String[] { playerExecutable.getPath(), swf };
			try
			{
				System.out.println("Executing test:\n" + Arrays.toString(runArgs));
	
				// TODO: Hack to add the directory containing the temp swf to the flashplayer trust.
				new FlashplayerSecurityHandler().trustFile(tempASFile.getParentFile());
	
				exitCode = executeCommandWithTimeout(runArgs, 20);
			}
			catch (Exception e)
			{
				e.printStackTrace();
				// If we just print the stacktrace the exitCode is still 0 and the test will pass.
				fail("Got exception");
			}
			
		    // Check that the runtime exit code was 0, meaning that no asserts failed.
			assertThat(exitCode, is(0));
		}
		else
		{
			StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
			boolean foundUs = false;
			for (StackTraceElement ste : stackTraceElements)
			{
				String className = ste.getClassName();
				String methodName = ste.getMethodName();
				if (className.equals("as.ASFeatureTestsBase") &&
					methodName.equals("compileAndRun"))
				{
					foundUs = true;
					continue;
				}
				if (foundUs)
				{
				    StringWriter out = new StringWriter();
				    PrintWriter writer = new PrintWriter(out);
					try {
						SWFDump.abcOption = true;
						SWFDump.dumpSwf(writer, SWFDump.toURL(new File(swf)), null);
					} catch (MalformedURLException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					String ours = out.toString();
					ours = ours.replaceAll("\r\n", "\n");
					className = className.replace(".", "_");
					String resultName = className + "_" + methodName + "_swfdump.xml";
					File resultFile = new File(testAdapter.getUnitTestBaseDir(), "swfdumps/" + resultName);
					if (!resultFile.exists())
					{
						if (generateResultFile)
							writeResultFile(resultFile, tempASFile.getName(), ours);
						System.out.println(resultName);
						System.out.println(ours);
						fail("result file " + resultFile.getAbsolutePath() + " does not exist");
					}
					String resultText = null;
					try {
						FileReader fileReader = new FileReader(resultFile);
						StringBuffer stringBuffer = new StringBuffer();
						int numCharsRead;
						char[] charArray = new char[1024];
						while ((numCharsRead = fileReader.read(charArray)) > 0) {
							stringBuffer.append(charArray, 0, numCharsRead);
						}
						fileReader.close();
						resultText = stringBuffer.toString();
						resultText = resultText.replaceAll("\r\n", "\n");
						String tempClassName = tempASFile.getName();
						tempClassName = tempClassName.replace(".as", "");
						tempClassName = tempClassName.replace(".mxml", "");
						resultText = resultText.replaceAll("%0", tempClassName);
						int c = resultText.indexOf("<!--");
						int c2 = resultText.indexOf("-->\n");
						if (c != -1)
						{
							resultText = resultText.substring(0, c) + resultText.substring(c2 + 4);
						}
					} catch (IOException e) {
						e.printStackTrace();
					}
					// remove file length because the generated classname could have a different
					// length and affect the swf length
					int c = resultText.indexOf("<!-- framecount");
					int c2 = resultText.indexOf("-->\n", c);
					resultText = resultText.substring(0, c) + resultText.substring(c2 + 4);
					c = ours.indexOf("<!-- framecount");
					c2 = ours.indexOf("-->\n", c);
					ours = ours.substring(0, c) + ours.substring(c2 + 4);
					// remove asc:compiler metadata as Eclipse builds don't get the right value
					c = ours.indexOf("<asc:compiler");
					c2 = ours.indexOf("/>\n", c);
					ours = ours.substring(0, c) + ours.substring(c2 + 4);
					c = resultText.indexOf("<asc:compiler");
					c2 = resultText.indexOf("/>\n", c);
					resultText = resultText.substring(0, c) + resultText.substring(c2 + 4);
                    // remove parsing swf comment as it contains local path to swf
                    c = ours.indexOf("<!-- Parsing swf");
                    c2 = ours.indexOf("-->\n", c);
                    ours = ours.substring(0, c) + ours.substring(c2 + 4);
                    c = resultText.indexOf("<!-- Parsing swf");
                    c2 = resultText.indexOf("-->\n", c);
                    resultText = resultText.substring(0, c) + resultText.substring(c2 + 4);
					// output contains 'succ[...]' which can be in different order
					ours = ours.replaceAll("succs=\\[.*\\]", "");
					resultText = resultText.replaceAll("succs=\\[.*\\]", "");
					// string padding can be different if classname is longer so collapse spaces
					ours = ours.replaceAll("[ ]+", " ");
					resultText = resultText.replaceAll("[ ]+", " ");
					// trim every line.  For some reason there are different numbers of trailing spaces on some lines.
					String[] ourlines = ours.split("\n");
					int n = ourlines.length;
					for (int i = 0; i < n; i++)
					{
						ourlines[i] = ourlines[i].trim();
					}
					ours = StringUtils.join(ourlines, "\n");
					String[] resultLines = resultText.split("\n");
					n = resultLines.length;
					for (int i = 0; i < n; i++)
					{
						resultLines[i] = resultLines[i].trim();
					}
					resultText = StringUtils.join(resultLines, "\n");
					assertThat(ours, is(resultText));
					break;
				}
			}
		}
	}
	
	protected void compileAndRun(String source)
	{
		compileAndRun(source, false, false, false, null);
	}

	public static int executeCommandWithTimeout(String[] args, long timeoutInSeconds) throws Exception {
		ExecutorService service = Executors.newSingleThreadExecutor();
		Process process = Runtime.getRuntime().exec(args);
		try {
			Callable<Integer> call = new CallableProcess(process);
			Future<Integer> future = service.submit(call);
			return future.get(timeoutInSeconds, TimeUnit.SECONDS);
		} catch (ExecutionException e) {
			throw new Exception("Process failed to execute", e);
		} catch (TimeoutException e) {
			process.destroy();
			throw new Exception("Process timed out", e);
		} finally {
			service.shutdown();
		}
	}

	private static class CallableProcess implements Callable<Integer> {
		private Process p;

		public CallableProcess(Process process) {
			p = process;
		}

		public Integer call() throws Exception {
			return p.waitFor();
		}
	}

	/**
	 * Combines various code snippets to make a complete one-file application.
	 */
    protected String getAS(String[] imports, String[] declarations, String[] testCode, String[] extraCode)
    {
    	String[] template;
    	
    	if (hasFlashPlayerGlobal)
    	{
	        template = new String[]
	        {
	            "package {",
	            "import flash.display.Sprite;",
	            "import flash.events.Event;",
	            "import flash.system.System;",
	            "%1",
	            "public class %0 extends flash.display.Sprite",
	            "{",
	            "    public function %0()",
	            "    {",
	            "        loaderInfo.addEventListener(flash.events.Event.INIT, initHandler);",
	            "    }",
	            "    %2",
	            "    private function initHandler(e:flash.events.Event):void",
	            "    {",
	            "        %3",
	            "        System.exit(0);",            
	            "    }",
	            "    private function assertEqual(message:String, actualValue:*, expectedValue:*):void",
	            "    {",
	            "        if (actualValue !== expectedValue)",
	            "        {",
	            "            trace(message, actualValue, expectedValue);",
	            "            System.exit(1);",
	            "        }",
	            "    }",
	            "}",
	            "}",
	            "%4"
	        };
    	}
    	else
    	{
	        template = new String[]
  	        {
  	            "package {",
  	            "%1",
  	            "public class %0",
  	            "{",
  	            "    public function %0()",
  	            "    {",
  	            "    }",
  	            "    %2",
  	            "    private function initHandler(e:Object):void",
  	            "    {",
  	            "        %3",
  	            "    }",
	            "    private function assertEqual(message:String, actualValue:*, expectedValue:*):void",
	            "    {",
	            "    }",
  	            "}",
  	            "}",
  	            "%4"
  	        };
    	}
        String source = StringUtils.join(template, "\n");
        source = source.replace("%1", StringUtils.join(imports, "\n"));
        source = source.replace("%2", StringUtils.join(declarations, "\n        "));
        source = source.replace("%3", StringUtils.join(testCode, "\n"));
        source = source.replace("%4", StringUtils.join(extraCode, "\n"));
        return source;
    }
    
	/**
	 * Combines various code snippets to make a complete one-file application.
	 */
    public static void writeResultFile(File file, String className, String results)
    {
		className = className.replace(".as", "");
		className = className.replace(".mxml", "");
    	results = results.replaceAll(className, "%0");
    	int firstLine = results.indexOf("\n");
    	results = results.substring(0, firstLine + 1) + ASF_XML_HEADER + results.substring(firstLine + 1);
    	BufferedWriter out = null;
    	try {
        	FileWriter fw = new FileWriter(file.getAbsolutePath());
    	    out = new BufferedWriter(fw);
    	    out.write(results);
    	}
    	catch (IOException e)
    	{
    	    System.out.println("Exception ");
    	}
    	finally
    	{
    		if (out != null)
    		{
				try {
					out.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
    		}
    	}
    }
    
    public static String ASF_XML_HEADER = "<!--\n" +
"\n" +
"  Licensed to the Apache Software Foundation (ASF) under one or more\n" +
"  contributor license agreements.  See the NOTICE file distributed with\n" +
"  this work for additional information regarding copyright ownership.\n" +
"  The ASF licenses this file to You under the Apache License, Version 2.0\n" +
"  (the \"License\"); you may not use this file except in compliance with\n" +
"  the License.  You may obtain a copy of the License at\n" +
"\n" +
"      http://www.apache.org/licenses/LICENSE-2.0\n" +
"\n" +
"  Unless required by applicable law or agreed to in writing, software\n" +
"  distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
"  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
"  See the License for the specific language governing permissions and\n" +
"  limitations under the License.\n" +
"\n" +
"-->\n";

}
