<?php
require_once '../vendor/nikic/php-parser/lib/bootstrap.php';
require_once 'php_serializer.class.php';

function dbg($indent, $text) {
	for($i=0;$i<$indent;$i++) echo ".";
	echo "$text\n";
}

abstract class AbstractWeaver {
	protected $parser;
	protected $nodeDumper;
	protected $prettyPrinter;
	protected $serializer;
	protected $unserializer;
	protected $ast_dump_before_walk;
	protected $ast_dump_after_walk;

	public function __construct() {
		$this->parser = new PHPParser_Parser(new PHPParser_Lexer);
		$this->nodeDumper = new PHPParser_NodeDumper;
		$this->prettyPrinter = new PHPParser_PrettyPrinter_Zend;
		$this->serializer = new PHPParser_PHPSerializer;
		$this->unserializer = &$this->serializer;
		$this->ast_dump_before_walk="";
		$this->ast_dump_after_walk="";
	}

	public function setASTDumpFiles($ast_dump_before_walk, $ast_dump_after_walk) {
		// Set file to "" to prevent AST dumping
		$this->$ast_dump_before_walk=$ast_dump_before_walk;
		$this->$ast_dump_after_walk=$ast_dump_after_walk;
	}

	public function runAllSteps($sourcetree_rootpath, $cache_path, $result_path) {
		try {
			$step='detectFramework';
			$fw_props=$this->detectFramework($sourcetree_rootpath);
			$step='parseFrameworkConfig';
			$fw_conf=$this->parseFrameworkConfig($fw_props);
			$step='parseFrameworkCode';
			$fw_extra=$this->parseFrameworkCode($fw_props, $fw_conf, $cache_path);
			$step='parseApplicationCode';
			$app_extra=$this->parseApplicationCode($fw_props, $fw_conf, $fw_extra, $cache_path);
			$step='computePageList';
			$app_pages=$this->computePageList($fw_props, $fw_conf, $fw_extra, $app_extra);
			$step='outputMappingFile';
			$this->outputMappingFile($app_pages,  $result_path);
			foreach ($app_pages as $page=>$page_props) {
				$step="makePageFullAST of '$page'";
				$page_full_ast=$this->makePageFullAST($page, $page_props, $fw_props, $fw_conf, $fw_extra, $app_extra, $cache_path);
				$step="analysePageAST of '$page'";
				$page_annotated_ast=$this->analysePageAST($page_full_ast, $page, $page_props, $fw_props, $fw_conf, $fw_extra, $app_extra, $cache_path);
				$step="throwDeadCode of '$page'";
				$this->throwDeadCode($page_annotated_ast);
				$step="weaveCode of '$page'";
				$out_ast=$this->weaveCode($page_annotated_ast);

				$page_dest_path=$this->makeDestPath($page, $result_path);
				$step="prettyPrint to '$page_dest_path'";
				$this->prettyPrint($out_ast, $page_dest_path);
			}

		} catch (Exception $e) {
			echo "Step '$step' failure : ", $e->getMessage();
		}
	}

	public function makeDestPath($page, $result_path) {
		return $result_path . '/' . str_replace('/', '_', $page) . ".php";
	}

	public function parseAndWalk($src_filepath, $traverser, $env=array(), $level=0) {
		/* Parse a file et traverse the AST.
		 * Could be recursive if a traverser call again parseAndWalk
		 *  for example when finding a include() directive.
		 * The main call should be in this case :
		 * $w=new XXXWeaver;
                 * $nv=new NodeVisitor_YYY($w); // The node visitor 
		 * $t=new PHPParser_NodeTraverser; $t->addVisitor($nv);
		 * w->parseAndWalk($src_mainfile, $t, array('cwd'=>dirname($src_mainfile));
		*/
		try {
			dbg($level,"Parsing '$src_filepath'");
			$stmts = $this->parser->parse(file_get_contents($src_filepath));

			//TODO : using an attribute here is not very userfriendly
			$this->dumpAST($stmts, $src_filepath, $this->ast_dump_before_walk);

			if ($traverser) {
				dbg($level,"Transforming '$src_filepath'");
				$stmts = $traverser->traverse($stmts);
				$traverser=null; //Destroy
			}

			$this->dumpAST($stmts, $src_filepath, $this->ast_dump_after_walk);
		
		} catch (PHPParser_Error $e) {
		    echo 'Parse Error: ', $e->getMessage();
		}
		return $stmts;
	}

	public function staticEvalDefine($ast, $magics, $previous_constants=null) {
		if ($previous_constants==null) {
			$constants=array('DIRECTORY_SEPARATOR' => DIRECTORY_SEPARATOR);
		} else {
			$constants=$previous_constants;
		}
		while($stmt=array_shift($ast)) {
			if ($stmt instanceof PHPParser_Node_Stmt_If) {
				//TODO try to eval statically the cond
				if (1) {
					$constants += $this->staticEvalDefine($stmt->stmts, $magics, $constants);
				}
			} elseif ($stmt instanceof PHPParser_Node_Expr_FuncCall) {
				if ( $stmt->name->parts === array('define') ) {
					$k=$this->staticReduce($stmt->args[0], $constants, $magics);
					$v=$this->staticReduce($stmt->args[1], $constants, $magics);
					if ($k instanceof PHPParser_Node_Scalar_String && $v instanceof PHPParser_Node_Scalar_String) {
						$k=$k->value;
						$v=$v->value;
//						dbg(0,"Found '$k' => '$v'");
						$constants[$k]=$v;
					}
				} else {
//					dbg(0,"Skipped funcall: " . $this->prettyPrinter->prettyPrint(array($stmt)));
				}
			} else {
//				dbg(0,"Skipped : " . get_class($stmt));
			}
		}
		return $constants;
	}

	public function staticReduce($stmt, $constants, $magics, $level=0) {
		//dbg($level,'staticReduce(<'.get_class($stmt).'>, $constants, $magics)');

		// Condition de sortie
		if ($stmt instanceof PHPParser_Node_Scalar_String) return $stmt;
		// Propagation d'erreur
		if (is_integer($stmt)) return $stmt;

		// Cas nominal, action selon type de noeud
		if ($stmt instanceof PHPParser_Node_Expr_FuncCall) {
			// Support some string manipulation funcs
			if ( count($stmt->name->parts)===1 ) {
				$func=$stmt->name->parts[0];
				switch($func) {
					// Unary context insensitive funcs
					case 'dirname':
					case 'basename':
						$arg=$this->staticReduce($stmt->args[0], $constants, $magics, $level+1);
						if ($arg instanceof PHPParser_Node_Scalar_String) {
							$res=$func($arg->value); //Real call !!
							return new PHPParser_Node_Scalar_String($res);
						}
						break;
					default:
						dbg(0,"Unsupported func '$func'");
						return 1;
				}
			} else {
				dbg(0,"Unknown multipart funcname");
				return 2;
			}
		} elseif ($stmt instanceof PHPParser_Node_Arg) {
			// Unbox arguments, ignoring by ref things (static context)
			return $this->staticReduce($stmt->value, $constants, $magics, $level+1);
		} elseif ($stmt instanceof PHPParser_Node_Expr_Concat) {
			$l=$this->staticReduce($stmt->left, $constants, $magics, $level+1);
			if (! $l instanceof PHPParser_Node_Scalar_String) {
				return $l;
			}
			$r=$this->staticReduce($stmt->right, $constants, $magics, $level+1);
			if (! $r instanceof PHPParser_Node_Scalar_String) {
				return $r;
			}
			return new PHPParser_Node_Scalar_String($l->value . $r->value);
		} elseif ($stmt instanceof PHPParser_Node_Expr_ConstFetch) {
			//TODO : a ConstFetch could be have more than one part in his name ?
			// What glue between pieces ??
			//$k=$this->prettyPrinter->prettyPrint(array($stmt));
			$k=$stmt->name->parts[0];
			if (array_key_exists($k, $constants)) {
				return new PHPParser_Node_Scalar_String($constants[$k]);
			} else {
				dbg(0,"ConstFetch failed for '$k'");
				return 3;
			}
		} elseif ($stmt instanceof PHPParser_Node_Scalar_FileConst) {
			$k='__FILE__';
			if (array_key_exists($k, $magics)) {
				return new PHPParser_Node_Scalar_String($magics[$k]);
			} else {
				dbg(0,"Magic evaluation failed for '$k'");
				return 4;
			}
		}
	}

	public function prettyPrint($ast, $dest_filepath) {
		dbg(0,"Outputing '$dest_filepath'");
		file_put_contents($dest_filepath, "<?php\n" . $this->prettyPrinter->prettyPrint($ast) . "\n");
	}

	public function dumpAST($ast, $ast_title, $dest_filepath) {
		if (is_array($ast) && strlen($dest_filepath) > 0 ) {
			dbg(0,"Dumping '$ast_title,' AST to '$dest_filepath'");
			file_put_contents($dest_filepath, $this->nodeDumper->dump($ast));
		}
	}

	public function serializeAST($ast, $ast_title, $dest_filepath) {
//		if (is_array($ast) && strlen($dest_filepath) > 0 ) {
			file_put_contents($dest_filepath, $this->serializer->serialize($ast));
//		}
	}

	public function unserializeAST($src_filepath) {
		return $this->serializer->unserialize(file_get_contents($src_filepath));
	}

	public function findAllFiles($basepath, $regexMatch, $regexPrune, $already_found=array(), $level=0) {
		//dbg($level,"findAllFiles('$basepath', '$regex')");
		$found=$already_found;

		if ( $files=scandir($basepath) ) {
			foreach ($files as $f) {
				if (preg_match($regexPrune, $f)===1) continue;

				$f_path=$basepath . DIRECTORY_SEPARATOR . $f;
				if (is_dir($f_path)) {
					if ( !( $f=="." || $f==".." ) ) {
						$found += $this->findAllFiles($f_path, $regexMatch, $regexPrune, $found, $level+1);
					}
				} elseif ( is_readable($f_path) ) {
					//dbg($level, "preg_match('$regexMatch', '$f')" );
					if (preg_match($regexMatch, $f)===1) {
						// A matching file has found in the list
						$found[] = $f_path;
					}
				}
			}
		}

		return $found;
	}

	public function parseAndCacheSourceTree($sourcetree_path, $cache_path, $regexMatch, $regexPrune, $env, $traverser=null) {
		$filelist=$this->findAllFiles($sourcetree_path, $regexMatch, $regexPrune);
		foreach ($filelist as $f) {
			$cache_filepath=$cache_path . "/" . sha1($f) . "-" . substr(sha1(print_r($env,true)),-8,8) . ".ast";
			// If the cache is already up to date, skip parsing
			if ( $stat_cache=@stat($cache_filepath) ) {
				$stat_source=stat($f);
				if ($stat_cache['mtime'] >= $stat_source['mtime'] ) continue;
			}

			$ast=$this->parseAndWalk($f, $traverser, $env);
			$this->serializeAST($ast, $f, $cache_filepath);
		}
	}

	// Framework specific code
	abstract public function detectFramework($sourcetree_rootpath);
	abstract public function parseFrameworkConfig($fw_props);
	abstract public function parseFrameworkCode($fw_props, $fw_conf, $cache_path);
	abstract public function parseApplicationCode($fw_props, $fw_conf, $fw_extra, $cache_path);
	abstract public function computePageList($fw_props, $fw_conf, $fw_extra, $app_extra);
	abstract public function outputMappingFile($app_pages,  $result_path);
	abstract public function makePageFullAST($page, $page_props, $fw_props, $fw_conf, $fw_extra, $app_extra, $cache_path);
	abstract public function analysePageAST($page_full_ast, $page, $page_props, $fw_props, $fw_conf, $fw_extra, $app_extra, $cache_path);
	abstract public function throwDeadCode(&$page_annotated_ast);
	abstract public function weaveCode($page_annotated_ast);
}