#!/usr/bin/env php5 <?php

//Controls the PHP-process on the PHP-side. class php_process{

//Opens stdin and stdout for processing. Sets various helper-variables.
function __construct(){
  $this->sock_stdin = fopen("php://stdin", "r");
  $this->sock_stdout = fopen("php://stdout", "w");
  $this->objects = array();
  $this->objects_spl = array();
  $this->objects_count = 0;
  $this->created_functions = array();
  $this->proxy_to_func = array("call_created_func", "constant_val", "create_func", "func", "get_var", "memory_info", "object_cache_info", "object_call", "require_once_path", "set_var", "static_method_call", "unset_ids");
  $this->func_specials = array("constant", "define", "die", "exit", "require", "require_once", "include", "include_once");
  $this->send_count = 0;

  print "php_script_ready:" . getmypid() . "\n";
}

//Starts listening in stdin for new instructions. Calls 'handle_line' for every line gotten.
function start_listening(){
  while(true){
    $line = fgets($this->sock_stdin, 1048576);
    $this->handle_line($line);
  }
}

//Writes the given data to stdout. Serializes and encodes it as well and increases the 'send_count'-variable.
function send($data){
  $id = $this->send_count;
  $this->send_count++;
  $data_packed = trim(base64_encode(serialize($data)));
  if (!fwrite($this->sock_stdout, "send:" . $id . ":" . $data_packed . "\n")){
    throw new exception("Could not write to stdout.");
  }

  //return $this->read_answer($id);
}

//Handles the given instruction. It parses it and then calls the relevant method.
function handle_line($line){
  $data = explode(":", $line);
  $type = $data[0];
  $id = intval($data[1]);
  $args = unserialize(base64_decode($data[2]));
  if ($args === false){
    throw new exception("The args-data was not unserializeable: " . base64_decode($data[2]));
  }

  try{
    if ($type == "send"){
      if ($args["type"] == "eval"){
        $res = eval($args["eval_str"] . ";");
        $this->answer($id, $res);
      }elseif($args["type"] == "new"){
        $this->new_object($id, $args);
      }elseif(in_array($args["type"], $this->proxy_to_func)){
        $this->$args["type"]($id, $args);
      }else{
        throw new exception("Unknown send-type: " . $args["type"] . " (" . implode(", ", array_keys($args)) . ") (" . base64_decode($data[2]) . ")");
      }
    }else{
      throw new exception("Invalid type: " . $type);
    }
  }catch(exception $e){
    $this->answer($id, array("type" => "error", "msg" => $e->getMessage(), "bt" => $e->getTraceAsString()));
  }
}

//Parses objects into special arrays, which again will be turned into proxy-objects on the Ruby-side. Recursivly scans arrays to do the same.
function parse_data($data){
  if (is_array($data)){
    foreach($data as $key => $val){
      if (is_object($val)){
        $data[$key] = $this->parse_data($val);
      }
    }

    return $data;
  }elseif(is_object($data)){
    $spl = spl_object_hash($data);

    if (array_key_exists($spl, $this->objects_spl)){
      $id = $this->objects_spl[$spl];
    }else{
      $id = $this->objects_count;
      $this->objects_count++;

      if (array_key_exists($id, $this->objects)){
        throw new exception("Object with that ID already exists: " . $id);
      }

      $this->objects[$id] = array("obj" => $data, "spl" => $spl);
      $this->objects_spl[$spl] = $id;
    }

    $ret = array("proxyobj", $id);
    return $ret;
  }else{
    return $data;
  }
}

//Recursivly read the given data from the Ruby-side. Changing special arrays into the objects they refer to.
function read_parsed_data($data){
  if (is_array($data) and array_key_exists("type", $data) and $data["type"] == "proxyobj" and array_key_exists("id", $data)){
    $object = $this->objects[$data["id"]]["obj"];
    if (!$object){
      throw new exception("No object by that ID: " . $data["id"]);
    }

    return $object;
  }elseif(is_array($data) and array_key_exists("type", $data) and $data["type"] == "php_process_created_function" and array_key_exists("id", $data)){
    $func = $this->created_functions[$data["id"]]["func"];
    return $func;
  }elseif(is_array($data)){
    foreach($data as $key => $val){
      $data[$key] = $this->read_parsed_data($val);
    }

    return $data;
  }else{
    return $data;
  }
}

//Answers a request ID with the given data. Writes it to stdout.
function answer($id, $data){
  if (!fwrite($this->sock_stdout, "answer:" . $id . ":" . base64_encode(serialize($this->parse_data($data))) . "\n")){
    throw new exception("Could not write to socket.");
  }
}

//Ruby wants spawn an object. Cache it and return (Ruby will tell us when to unset it automatically).
function new_object($id, $args){
  $class = $args["class"];
  $new_args = $this->read_parsed_data($args["args"]);

  $klass = new ReflectionClass($class);
  $object = $klass->newInstanceArgs($new_args);

  $this->answer($id, $object);
}

//Ruby wants to set an instance variable on an object. Do that and answer with 'true'.
function set_var($id, $args){
  $object = $this->objects[$args["id"]]["obj"];
  if (!$object){
    throw new exception("No object by that ID: " . $args["id"]);
  }

  $object->$args["name"] = $args["val"];
  $this->answer($id, true);
}

//Ruby wants to read an instance variable on an object. Return the variable's value.
function get_var($id, $args){
  $object = $this->objects[$args["id"]]["obj"];
  if (!$object){
    throw new exception("No object by that ID: " . $args["id"]);
  }

  $this->answer($id, $object->$args["name"]);
}

//Ruby wants to call a method on an object. Do that and return the result.
function object_call($id, $args){
  if (!array_key_exists($args["id"], $this->objects)){
    throw new exception("No object by that ID: " . $args["id"]);
  }

  $object = $this->objects[$args["id"]]["obj"];
  $call_arr = array($object, $args["method"]);

  //Error handeling.
  if (!$object){
    throw new exception("No object by that ID: " . $args["id"]);
  }elseif(!method_exists($object, $args["method"])){
    throw new exception("No such method: " . get_class($object) . "->" . $args["method"] . "()");
  }elseif(!is_callable($call_arr)){
    throw new exception("Not callable: " . get_class($object) . "->" . $args["method"] . "()");
  }

  $res = call_user_func_array($call_arr, $this->read_parsed_data($args["args"]));
  $this->answer($id, $res);
}

//Ruby wants to call a function. Do that and return the result.
function func($id, $args){
  //These functions cant be called normally. Hack them with eval instead.
  $newargs = $this->read_parsed_data($args["args"]);

  if (in_array($args["func_name"], $this->func_specials)){
    $eval_str = $args["func_name"] . "(";
    $count = 0;
    foreach($newargs as $key => $val){
      if (!is_numeric($key)){
        throw new exception("Invalid key: '" . $key . "'.");
      }

      if ($count > 0){
        $eval_str .= ",";
      }

      $eval_str .= "\$args['args'][" . $count . "]";
      $count++;
    }
    $eval_str .= ");";

    $res = eval($eval_str);
  }else{
    $res = call_user_func_array($args["func_name"], $newargs);
  }

  $this->answer($id, $res);
}

//Ruby has given us various object-IDs to unset. Unset them from cache and return 'true'.
function unset_ids($id, $args){
  foreach($args["ids"] as $obj_id){
    if (!array_key_exists($obj_id, $this->objects)){
      continue;
    }

    $spl = $this->objects[$obj_id]["spl"];

    if (!array_key_exists($spl, $this->objects_spl)){
      throw new exception("SPL could not be found: " . $spl);
    }

    unset($this->objects_spl[$spl]);
    unset($this->objects[$obj_id]);
  }

  $this->answer($id, true);
}

//Ruby wants information about the object-cache on the PHP-side. Return that in an array.
function object_cache_info($id, $args){
  $types = array();
  foreach($this->objects as $key => $val){
    if (is_object($val)){
      $types[] = "object: " . get_class($val);
    }else{
      $types[] = gettype($val);
    }
  }

  $this->answer($id, array(
    "count" => count($this->objects),
    "types" => $types
  ));
}

//Ruby wants to call a static method. Answers with the result.
function static_method_call($id, $args){
  $call_arr = array($args["class_name"], $args["method_name"]);

  if (!class_exists($args["class_name"])){
    throw new exception("Class does not exist: '" . $args["class_name"] . "'.");
  }elseif(!method_exists($args["class_name"], $args["method_name"])){
    throw new exception("Such a static method does not exist: " . $args["class_name"] . "::" . $args["method_name"] . "()");
  }elseif(!is_callable($call_arr)){
    throw new exception("Invalid class-name (" . $args["class_name"] . ") or method-name (" . $args["method_name"] . "). It was not callable.");
  }

  $newargs = $this->read_parsed_data($args["args"]);
  $res = call_user_func_array($call_arr, $newargs);

  $this->answer($id, $res);
}

//Creates a function which can be used for callbacks on the Ruby-side.
function create_func($id, $args){
  $cb_id = $args["callback_id"];
  $func = create_function("", "global \$php_process; \$php_process->call_back_created_func(" . $cb_id . ", func_get_args());");
  if (!$func){
    throw new exception("Could not create function.");
  }

  $this->created_functions[$cb_id] = array("func" => $func);
  $this->answer($id, true);
}

//This function is called, when a create-function is called. It then callbacks to Ruby, where a 'Proc' will be executed.
function call_back_created_func($func_id, $args){
  $this->send(array(
    "type" => "call_back_created_func",
    "func_id" => $func_id,
    "args" => $args
  ));
}

//Ruby wants to call a created function. Pretty much just executes that function. This is useually done for debugging callbacks.
function call_created_func($id, $args){
  $func = $this->created_functions[$args["id"]]["func"];
  if (!$func){
    throw new exception("No created function by that ID: '" . $args["id"] . "'.\n\n" . print_r($args, true) . "\n\n" . print_r($this->created_functions, true));
  }

  $eval_str = "\$func(";
  $count = 0;

  foreach($args["args"] as $key => $val){
    if ($count > 0){
      $eval_str .= ", ";
    }

    $eval_str .= "\$args['args'][" . $count . "]";
    $count++;
  }

  $eval_str .= ");";
  $res = eval($eval_str);
  $this->answer($id, $res);
}

//Ruby wants to read a constant. This is not done by 'func', because of keeping caching posibility open and not wanting to eval.
function constant_val($id, $args){
  $this->answer($id, constant($args["name"]));
}

//Returns various information about the object-cache.
function memory_info($id, $args){
  $this->answer($id, array(
    "objects" => count($this->objects),
    "objects_spl" => count($this->objects_spl),
    "created_functions" => count($this->created_functions)
  ));
}

//Makes errors being thrown as exceptions instead.
function error_handler($errno, $errmsg, $filename, $linenum, $vars, $args = null){
  $errortypes = array (  
    E_ERROR => 'Error',  
    E_WARNING => 'Warning',  
    E_PARSE => 'Parsing Error',  
    E_NOTICE => 'Notice',  
    E_CORE_ERROR => 'Core Error',  
    E_CORE_WARNING => 'Core Warning',  
    E_COMPILE_ERROR => 'Compile Error',  
    E_COMPILE_WARNING => 'Compile Warning',  
    E_USER_ERROR => 'User Error',  
    E_USER_WARNING => 'User Warning',  
    E_USER_NOTICE => 'User Notice',  
    E_STRICT => 'Runtime Notice'  
  );

  if ($errno == E_STRICT or $errno == E_NOTICE){
    return null;
  }

  throw new exception("Error " . $errortypes[$errno] . ": " . $errmsg . " in \"" . $filename . ":" . $linenum);
}

}

//Spawn the main object. $php_process = new php_process();

//Set error-level and make warnings and errors being thrown as exceptions. set_error_handler(array($php_process, “error_handler”)); error_reporting(E_ALL ^ E_NOTICE ^ E_STRIC);

//Start listening for instructions from host process. $php_process->start_listening();