<?php
/**
 * @author Olexandr Zanichkovsky <olexandr.zanichkovsky@zophiatech.com>
 * @package General
 */

require_once dirname(__FILE__) . '/XmlImportConfig.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstSequence.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstText.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstPrint.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstInteger.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstFloat.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstString.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstXPath.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstFunction.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstWith.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstForeach.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstIf.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstMath.php';
require_once dirname(__FILE__) . '/ast/XmlImportAstSpintax.php';

/**
 * Is used to generate a PHP code from AST (Abstract Syntax Tree)
 */
class XmlImportTemplateCodeGenerator
{

  /**
   * Top level statement sequence
   *
   * @var XmlImportAstSequence
   */
  private $sequence;

  /**
   * statement sequence stack
   *
   * @var array
   */
  private $sequenceStack = array();

  /**
   * SimpleXmlElement variable number
   *
   * @var int
   */
  private $xmlVariableNumber = 0;

  /**
   * Previous statement
   *
   * @var XmlImportAstStatement
   */
  private $previousStatement = null;

  /**
   * Stack of SimpleXmlElement instances
   *
   * @var array
   */
  private $xmlStack = array();

  /**
   * Whether PHP tag is open
   *
   * @var bool
   */
  private $isPhpTagOpen = false;

  /**
   * Creates new instance
   *
   * @param XmlImportAstSequence $sequence
   */
  public function __construct(XmlImportAstSequence $sequence)
  {
    $this->sequence = $sequence;
  }

  /**
   * Generates the code and returns the filename under which it was stored in
   * cache directory. If $filename is null then it is generated automatically
   *
   * @param string $filename
   * @return string
   */
  public function generate($filename = null)
  {
  	$result = '';
    if (count($this->sequence->getVariables()))
    {
      $var = '$x' . $this->xmlVariableNumber++;
      array_push($this->xmlStack, $var);
      $result .= $this->openPhpTag() . $var . ' = $this->xml;' ;
    }    
    $result .= $this->generateForSequence($this->sequence);

    if (count($this->sequence->getVariables()))
    {      
      array_pop($this->xmlStack);
    }
    // grab import ID for manual and cron import runs
    $input = new PMXI_Input();
    $import_id = $input->get('id');
    if (empty($import_id)){
        $import_id = $input->get('import_id');
    }
    if ( ! $filename or ! @is_writable($filename)) {
	    // Ensure wp_tempnam is available before calling it.
	    // It's only loaded in wp-admin by default.
	    if ( ! function_exists( 'wp_tempnam' ) ) {
		    require_once( ABSPATH . 'wp-admin/includes/file.php' );
	    }

		$filename = wp_tempnam('xim_id_'. $import_id);
    }
	if ( ! $filename or ! @is_writable($filename) ){
      $uploads  = wp_upload_dir();
      $targetDir = $uploads['basedir'] . DIRECTORY_SEPARATOR . PMXI_Plugin::TEMP_DIRECTORY;
      $filename = $targetDir . DIRECTORY_SEPARATOR . wp_unique_filename($targetDir, 'tmpfile_id_'. $import_id);
    }

    file_put_contents($filename, $result);
    $sleep = apply_filters( 'wp_all_import_shard_delay', 0 );
    usleep($sleep);

    //@chmod($filename, 0666);
    return $filename;
  }

  /**
   * Generates code for a statement sequence
   *
   * @param XmlImportAstSequence $sequence
   * @return string
   */
  private function generateForSequence(XmlImportAstSequence $sequence)
  {
    array_push($this->sequenceStack, $sequence);
    $result = '';

    if (count($sequence->getVariableDefinitions()) > 0)
    {
      $result .= $this->openPhpTag();
      foreach ($sequence->getVariableDefinitions() as $xpath)
      {        
        $result .= PHP_EOL . str_replace('{{XML}}', $this->xmlStack[count($this->xmlStack) - 1], $xpath);
      }
      $result .= PHP_EOL;
    }
    foreach ($sequence->getStatements() as $statement)
    {            
      $result .= $this->generateForStatement($statement);
    }
    array_pop($this->sequenceStack);

    return $result;
  }

  /**
   * Generates code for a statement
   *
   * @param XmlImportAstStatement $statement
   * @return string
   */
  private function generateForStatement(XmlImportAstStatement $statement)
  {
    $result = '';
    if ($statement instanceof XmlImportAstText)
    {
      $result .= $this->closePhpTag();
      $text = preg_replace('%<\?|\?>%', '<?php echo "$0"; ?>', $statement->getValue()); // escape php tags
      if ($this->previousStatement instanceof XmlImportAstPrint && (strpos($text, "\n") === 0 || strpos($text, "\r\n") === 0))
      {
        $result .= PHP_EOL;
      }
      $result .= $text;
    }
    else
    {
      $result .= $this->openPhpTag();
      if ($statement instanceof XmlImportAstPrint)
      {		
        $result .= 'echo ';
        $result .= $this->generateForExpression($statement->getValue(), true) . ';';
      }
      elseif ($statement instanceof XmlImportAstWith)
      {
        $var = '$x' . $this->xmlVariableNumber++;
        $result .= PHP_EOL . $var . ' = ' ;
        $result .= $this->generateForExpression($statement->getXpath()) . ';' . PHP_EOL;

        array_push($this->xmlStack, $var);
        $result .= 'if (' . $var . ' !== false && count(' . $var . ') > 0) :' . PHP_EOL . $var .
          ' = ' . $var . '[0];' . PHP_EOL;
        $result .= $this->generateForSequence($statement->getBody());
        array_pop($this->xmlStack);
        $result .= $this->openPhpTag() . PHP_EOL . 'endif;' . PHP_EOL;
      }
      elseif ($statement instanceof XmlImportAstForeach)
      {
        $var = '$x' . $this->xmlVariableNumber++;                    
        $result .= PHP_EOL . 'foreach (' . $this->generateForExpression($statement->getXPath(), false) .
          ' as ' . $var . ') :' . PHP_EOL;       
        array_push($this->xmlStack, $var);
        $result .= $this->generateForSequence($statement->getBody());
        $result .= $this->openPhpTag() . PHP_EOL . 'endforeach;' . PHP_EOL;        
        array_pop($this->xmlStack);
      }
      elseif ($statement instanceof XmlImportAstIf)
      {                
        $result .= PHP_EOL . 'if (' . $this->generateForExpression($statement->getCondition()) .  ') :' . PHP_EOL;
        $result .= $this->generateForSequence($statement->getIfBody());

        foreach ($statement->getElseIfs() as $elseif)
        {
          $result .= $this->openPhpTag() . PHP_EOL . 'elseif (' . $this->generateForExpression($elseif->getCondition()) . ') :' . PHP_EOL;
          $result .= $this->generateForSequence($elseif->getBody());
        }
        if (!is_null($body = $statement->getElseBody()))
        {          
          $result .= $this->openPhpTag() . PHP_EOL . 'else :' . PHP_EOL;
          $result .= $this->generateForSequence($body);
        }
        $result .= $this->openPhpTag() . PHP_EOL . 'endif;' . PHP_EOL;        
      }
	  
    }
    $this->previousStatement = $statement;
    return $result;
  }

  /**
   * Generates code for expression
   *
   * @param XmlImportAstExpression $expression
   * @param bool $inPrint whether in print or in clause or function argument
   * @return string
   */
  private function generateForExpression(XmlImportAstExpression $expression, $inPrint = false)
  {
	
    switch (get_class($expression))
    {
      case 'XmlImportAstString':        
        $result = '"' . $this->getEscapedValue($expression->getValue()) . '"';
        break;

      case 'XmlImportAstInteger':
      case 'XmlImportAstFloat':
        $result = $expression->getValue();
        break;

      case 'XmlImportAstXPath':              
        if ($inPrint)
        {          
          $variables = $this->sequenceStack[count($this->sequenceStack) - 1]->getVariables();          
          $result = '$this->getValue(' . $variables[$expression->getValue()] . ')';                    
        }
        else
        {          
          $variables = $this->sequenceStack[count($this->sequenceStack) - 1]->getVariables();          
          $result = $variables[$expression->getValue()];                              
        }
        break;

      case 'XmlImportAstFunction':      
        $result = $this->generateForFunction($expression);
		    break;
  	  
      case 'XmlImportAstMath':
          $result = $this->generateForMath($expression);
        break;

      case 'XmlImportAstSpintax':
          $result = $this->generateForSpintax($expression);
        break;
    }
    return $result;
  }

  /**
   * Generates code for a function
   *
   * @param XmlImportAstFunction $function
   * @return string
   */
  private function generateForFunction(XmlImportAstFunction $function)
  {    
    $result = $function->getName() . '(';
    $arguments = $function->getArguments();
    
    for($i = 0; $i < count($arguments); $i++)
    {		      
      $result .= $this->generateForExpression($arguments[$i], true);
      if ($i < (count($arguments) - 1))
        $result .= ', ';
    }
    $result .= ')';
	
    return $result;
  }
  
  /**
   * Generates code for a function
   *
   * @param XmlImportAstFunction $function
   * @return string
   */
  private function generateForMath(XmlImportAstMath $math)
  {
    $result = '';
    $arguments = $math->getArguments();
    for($i = 0; $i < count($arguments); $i++)
    {		
      $result .= $this->generateForExpression($arguments[$i], true);      	  
    }
    
    return 'number_format('.str_replace("\"", "", $result).',2)';
  }

  /**
   * Generates code for a function
   *
   * @param XmlImportAstSpintax $expression
   * @return string
   */
  private function generateForSpintax(XmlImportAstSpintax $spintax)
  {
    $result    = '';
    $arguments = $spintax->getArguments();
    $elements  = array();
    $buf       = array();

    for($i = 0; $i < count($arguments); $i++)
    {         

      if ($arguments[$i]->getValue() != '|') array_push($buf, $this->generateForExpression($arguments[$i], true)); else { array_push($elements, $buf); $buf = array();}
      
    }        

    array_push($elements, $buf);

    if (!empty($elements) and is_array($elements)){
      
      $spintax_arr = $this->generateVariation($elements);

      foreach ($spintax_arr as $key => $value) {
        $result .= "\"<p>\".".implode(".", $value).".\"</p>\""; if ($key != count($spintax_arr) - 1) $result .= ".";
      }

    }
    
    return $result;
  }

  function generateVariation($A, $i = 0)
  {
      $result = array();

      if ($i < count($A))
      {
          $variations = $this->generateVariation($A, $i + 1);

          for ($j = 0; $j < count($A[$i]); $j++)
          {
              if ($variations)
              {
                  foreach ($variations as $variation)
                  {
                      $result[] = array_merge(array($A[$i][$j]), $variation);
                  }
              }
              else
              {
                  $result[] = array($A[$i][$j]);
              }
          }
      }

      return $result;
  }
  /**
   * Add PHP open tag if needed
   *
   * @return string
   */
  private function openPhpTag()
  {
    $result = '';
    if (!$this->isPhpTagOpen)
    {
      $this->isPhpTagOpen = true;
      $result = '<?php ';
    }
    return $result;
  }

  /**
   * Adds PHP close tag if needed
   *
   * @return string
   */
  private function closePhpTag()
  {
    $result = '';
    if ($this->isPhpTagOpen)
    {
      $this->isPhpTagOpen = false;
      $result = '?>';
    }
    return $result;
  }

  /**
   * Gets escaped value
   *
   * @return string
   */
  private function getEscapedValue($value)
  {
    $escapedValue = strtr($value, array(
    	"\n" => "\\n",
    	"\t" => "\\t",
    	"\r" => "\\r",
    	"$" => "\\$",
    	"\"" => "\\\"",
    	"\\" => "\\\\",
    ));
    return $escapedValue;
  }
}