<?php

	require_once __DIR__ . '/database_record.php';

class engine_generic {
	
	// load db settings - user / pass / host
	protected $host;
	protected $user;
	protected $password;
	protected $db;
	protected $protocol;
	protected $limit = FALSE;
	protected $where = [];		// array of 'and' conditions
	protected $orderby = [];
	protected $groupby = [];
	protected $_debug = 0;
	protected $hook_query = FALSE;
	protected $extra_fields = [];
	protected $hook;
	protected $collection = FALSE;
	protected $_autoinc = FALSE;
    public $lastsql;
    private $connection = NULL;
	private $definitions = [];
	private $__tablename;
	public $errnum;
	public $errmsg;
	
	public function loadSettings($engine = 'null') {
	}

	public function __debugInfo() {
		$x = [
			'host' => $this->host,
			'db' => $this->db,
			'user' => $this->user,
			'where' => $this->where,
			'limit' => $this->limit,
			'extra' => $this->extra_fields,
		];
		return $x;
	}
	
	static public function __callStatic($name, $args) {
		$n = new static;
		return call_user_func_array([$n, $name], $args);
	}

	// format the value suitable for storing in the database
	// $value - string needs to be processed, $sz - max size of string (field size)
	public function formatString($value, $sz = 0) {
	// should be overwritten
		if ($value === NULL) return 'NULL';
		if ($sz > 0) $value = mb_substr($value, 0, $sz);
		$value = str_replace(array("\\",   '"',   "\n",  "\r", "\t", "\0"),
					   array("\\\\", '\\"', "\\n", "\\r", "\\t", "\\0"), $value);
		return "\"$value\"";
	}
	
	public function close() {
	// should be overwritten
	}

	public function open() {
	// should be overwritten
	}
	
	public function limit() {
		if (func_num_args()) {
			$this->limit = func_get_arg(0);
			return $this;
		}
		return $this->limit;
	}

	public function debug() {
		if (func_num_args()) {
			$this->_debug = func_get_arg(0);
			return $this;
		}
		return $this->_debug;
	}

	public function hook_query() {
		if (func_num_args()) {
			$this->hook_query = func_get_arg(0);
			return $this;
		}
		return $this->hook_query;
	}
	
	public function tablename() {
		if (func_num_args()) {
			$this->__tablename = func_get_arg(0);
			return $this;
		}
		return $this->__tablename;
	}
	
	public function autoinc() {
		if (func_num_args()) {
			$this->_autoinc = func_get_arg(0);
			return $this;
		}
		return $this->_autoinc;
	}
	
/*	protected function asWhereItem($where_item) {
	// should be overwritten
	}
*/

	protected function assembleWhere($where) {
		$output = '';
		foreach($where as $where_item) {
			if ($output != '') $output .= ' and ';
			$output .= '(' . $this->asWhereItem($where_item) . ')';
		}
		if ($output != '') $output = ' where ' . $output;
		return $output;	
	}

	public function where() {
		if (func_num_args() > 1) {
			$this->where[] = func_get_args();
			return $this;
		} elseif (func_num_args()) {
			$this->where[] = func_get_arg(0);
			return $this;
		}
		return $this->assembleWhere($this->where);
	}
	
	public function whereOr($param1, $param2) {		// takes at least 2 arguments
		$args = func_get_args();
		$res = [];
		for($i = count($args) - 1; $i >= 0; $i--) {
			$p = array_pop($this->where);
			array_unshift($res, $p);
		}
		$output = '';
		foreach($res as $where_item) {
			if ($output != '') $output .= ' or ';
			$output .= '(' . $this->asWhereItem($where_item) . ')';
		}
		$this->where[] = '(' .$output . ')';
		return $this;
	}
	
	// this is for mysql only :(
	protected function _name($name) {
		// this doesn't handle 'name as name2'
		$elements = explode('.', $name);
		foreach($elements as &$item) {
			$item = "`$item`";
		}
		return join('.', $elements);
	}
	
	public function wherein($fieldname, $valuelist) {
		foreach($valuelist as &$value_item) {
			$value_item = $this->formatString($value_item);
		}
		$valuelist = join(',', $valuelist);
		if ($valuelist == '') {
			$this->where[] = '1=0';
		} else {
			$fieldname = $this->_name($fieldname);
			$this->where[] = "$fieldname in ($valuelist)";
		}
		return $this;
	}

	public function orderby() {
		$c = func_num_args();
		if ($c) {
			$param = func_get_arg(0);
			$direction = $c >= 2 ? func_get_arg(1) : 'asc';
			if (strtolower($direction) == 'desc') $param .= ' desc';
			$this->orderby[] = $param;
			return;
		}
		if (count($this->orderby)) {
			return ' order by ' . join(', ', $this->orderby);
		} else {
			return '';
		}
	}

	public function groupby() {
		if (func_num_args()) {
			$param = func_get_arg(0);
			$this->groupby[] = $param;
			return;
		}
		if (count($this->groupby)) {
			return ' group by ' . join(', ', $this->groupby);
		} else {
			return '';
		}
	}

	public function Collection() {
		if (func_num_args()) {
			$this->collection = func_get_arg(0);
			return $this;
		}
		return $this->collection;
	}
	
	public function Query() {
		$params = func_get_args();
		$this->Collection(FALSE); 		// begin new query => clear out the old
		$this->_state();				// reset Where/Limit/OrderBy/GroupBy
		$this->lastsql = call_user_func_array([$this, 'QuerySQL'], $params);
		return $this;
	}
	
	public function Get() {
		$tablename = $this->tablename();
		if ($this->lastsql === FALSE && !empty($tablename) ) $this->lastsql = "select * from `{$tablename}`";
		$hook = $this->hook_query();
		if ($hook !== FALSE && hook_exists($hook)) {
			hook_execute($hook, $this);
		}
		$sqlresult = $this->execute($this->lastsql);
		if (is_object($sqlresult)) {
			$this->collection = $this->AsCollection($sqlresult, $this);
			$sqlresult->free();
			return $this->collection;
		}
		return NULL;
	}

	public function first() {
		if ($this->collection === FALSE) {
			$this->limit(1)->get();
		}
		if ($this->collection === FALSE) {
			$res =  NULL;
		} else {
			$res = $this->collection->First();
		}
		if (is_null($res)) $res = new \database_record($this->collection);
		return $res;
	}
	
	
	// save/restore current state (query, where, etc)
	public function _state() {
		if (func_num_args()) {
			$x = func_get_arg(0);
			$this->where = $x['where'];
			$this->limit = $x['limit'];
			$this->orderby = $x['order'];
			$this->groupby = $x['group'];
			$this->hook = $x['hook'];
			$this->extra_fields = $x['fields'];
			return $this;
		} else {
			$x = [
				'where' => $this->where,
				'limit' => $this->limit,
				'order' => $this->orderby,
				'group' => $this->groupby,
				'hook' => $this->hook_query,
				'fields' => $this->extra_fields,
			];
			$this->where = [];
			$this->orderby = [];
			$this->groupby = [];
			$this->limit = FALSE;
			$this->hook_query = FALSE;
			$this->extra_fields = [];
			return $x;
		}
	}
	
	public function _Query($sql /*[, param1[, param2, [...]]] */) {		// prepare a query - don't execute it
		// state is reset in the trait
		$sqlparam = func_get_args();
		array_shift($sqlparam);	// drop sql
		if (count($sqlparam) > 0) $sql = $this->perform_substitution($sql, $sqlparam);
		$this->query = $sql;
		return $this;
	}

	public function UpdateSql_AddField($field) {
		$this->extra_fields[] = $field;
	}
	
	// Prevent sql injection vulnerabilities
	protected function perform_substitution($sql /*, $sqlparam ... */) {
		$sqlparam = func_get_args();
		array_shift($sqlparam);
//		if (!is_array($sqlparam)) $sqlparam = array($sqlparam);		// single item - does not have to be in an array
		$p = 0;
		while (($q = strpos($sql, '%', $p)) !== FALSE) {
			$macro = strtolower(substr($sql, $q, 2));
			$value = (count($sqlparam)) ? array_shift($sqlparam) : '';
			switch($macro) {
				case '%s':
					$value = $this->FormatString($value);
					break;
				case '%d':
					$value = intval($value);
					break;
				case '%f':
					$value = floatval($value);
					break;
				default:
					$value = '';
			}
			$sql = substr($sql, 0, $q) . $value . substr($sql, $q + 2);
			$p = $q + strlen($value);
		}
		return $sql;
	}
	
	public function SetError($errnum = 0, $errmsg = FALSE) {
		$this->errnum = $errnum;
		$this->errmsg = $errmsg;
        if ($errnum) {
            error_log("Query: $this->lastsql");
            error_log("mysql - {$this->errmsg} [{$this->errnum}]");
        }
		return !$this->errnum;	// return TRUE if ok
	}

	// Lookup only returns the first record matching the criteria
	public function lookup($sql, $sqlparam = NULL) {
		return $this->_query($sql, $sqlparam)->limit(1)->first();
	}
	
	public function getVerb($sql) {
		$temp = trim($sql);
		if (substr($temp, 0, 2) == '/*') {		// remove any embedded comments
			$p = strpos($temp, '*/');
			$temp = trim(substr($temp, $p + 2));
		}
		if (substr($temp, 0, 1) == '(') {
			$temp = trim(substr($temp, 1));
		}
		return strtoupper(strtok($temp, " \n\t"));
	}
	
	function __construct() {
		$this->query = FALSE;
	}

	function __destruct() {
		$this->close();
	}
	
	public function connection() {
		if (func_num_args()) {
			$value = func_get_arg(0);
			$this->_setConnection($value);
			return $this;
		}
		return $this->connection;
	}
	
	private function _setConnection($connection_string = '') {
		if (preg_match('~(.+)://(.+)/(.*?)$~', $connection_string, $elements)) {
			$this->protocol = $elements[1];
			$host = $elements[2];
			$this->db = $elements[3];
			if (preg_match('~^(.*):(.*)@(.*)$~', $host, $elements)) {
				$this->user = $elements[1];
				$this->password = $elements[2];	// base64 encoded
				$this->host = $elements[3];
			} else {
				$this->host = $host;	// leave existing username unchanged
			}
//			$this->connection = $connection_string;
		} else {
//			$this->connection = FALSE;
		}
	}
	
	public function TableExists($tablename) {
		$dtable = strtolower($tablename);
		$result = $this->ExecuteRaw("select 1 from `{$dtable}`");
		return $this->errnum != 1146;
	}	
	
	// Insert
	public function InsertRecord($database_record) {
		$tablename = $database_record->tablename();
		if (empty($tablename)) {
			return $this->SetError(9998, 'Insert Error - tablename property is missing from class');
		}
		$names = $values = [];
		foreach($database_record as $name => $value) {
			if (substr($name, 0, 2) != '__') {
				$names[] = "`{$name}`";
				$values[] = $this->formatString($value);
			}
		}
		$names = join(',', $names);
		$values = join(',', $values);
		$sql = "insert into `{$tablename}` ($names) values ($values)";
		$result = $this->ExecuteRaw($sql);
		if (($autoinc = $database_record->autoinc()) !== FALSE) {
			$x = $this->GetLastInsertId();
			$database_record->$autoinc = $x;
		}
		return $result;
	}

	public function UpdateRecord($database_record, $key) {
		$where = $this->assembleWhere([$key]);
		$tablename = $database_record->tablename();
		if (empty($tablename)) {
			return $this->SetError(9998, 'Update Error - tablename property is missing from class');
		}
		if (empty($where)) {
			return $this->SetError(9997, 'missing key in updaterecord');
		}
		// only update changed records
		$output = '';
		$changed = $database_record->ChangedFields();
		foreach($changed as $name) {
			$value = $database_record->$name;
			$name = "`{$name}`";
			$value = $this->formatString($value);
			if ($output != '') $output .= ',';
			$output .= $name . '=' . $value;
		}
		if ($output == '') return TRUE;		// nothing changed
		
		$sql = "update `{$tablename}` set $output $where";
		$result = $this->ExecuteRaw($sql);
		return $result;
	}
	
	public function DeleteRecord($database_record, $key) {
		$where = $this->assembleWhere([$key]);
		$tablename = $database_record->tablename();
		$sql = "delete from `{$tablename}` $where";
		$result = $this->ExecuteRaw($sql);
		return $result;
	}

	// $keys could be "fieldname=value and fieldname2=value"
	private function _array_join($keys, $payload) {
		if (!is_array($keys)) throw new \Exception('keys must be an array' .var_export($keys, 1));
		reset($keys);
		$x = key($keys);
		if (is_numeric($keys)) throw new \Exception('keys must be an associative array (fieldname => value)');
		return $keys + $payload;
	}

    // defaults used only when a new record
	public function InsertOrUpdate($key, $payload, $default = NULL) {
		$state = $this->_State();
		$tablename = $this->tablename();
//		if ($tablename == 'users_source') var_dump($key);
        if (is_array($key) && !count($key)) {
            $data = NULL;
        } else {
			$data = $this->query("select * from `{$tablename}`")->where($key)->first();
        }
		
		if ($data === NULL) $data = new \database_record($this);
		if ($this->_autoinc) $data->autoinc($this->_autoinc);
		$data->tablename($tablename);
		if ($data->empty()) {	// not found - create record, but key must be an associative array
			$payload = $this->_array_join($key, $payload);
            if (!is_null($default)) $payload = $payload + $default;
			$data->LoadFromSource($payload);
			$data->empty(TRUE);
		} else {				// update existing record
			$data->LoadFromSource($payload, TRUE);
		}
		$data->SaveRecord($key);
		$this->_State($state);
		return $data;
	}
	
}

function GetEngine($engine = '') {
	if ($engine == '') $engine = 'auto';
	return hook_execute('db.load', NULL,  $engine);
}

function is_query($result) {
	return is_object($result) && method_exists($result, 'UpdateSql_AddField');
}
