<?php		// mysql engine

	require_once __DIR__ . '/engine_generic.php';

/*
Unexpected Duplicate record errors:   table may be marked as Crashed, and need repair.

http://php.net/manual/en/book.mysqli.php

// newer mysqld has only_full_group_by enabled by default.
// set session sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';

*/

	if (!defined('MYSQL_BUFFER')) define('MYSQL_BUFFER', 1);

class engine_mysql extends engine_generic {

	private $options;
	private $failedonly;
	private $mysqli;
	const LABEL = 'mysql';
	protected $settings;

	function __construct() {
		$this->settings = $this->loadSettings('mysql');
	// connection string:  mysql://user:password@hostname/databasename
	//  password is encoded
		if (!empty($this->settings)) {
			$this->connection($this->settings);
		}
		$this->options = 0;
		$this->mysqli = NULL;
	}
	
	public function is_installed() {
		return function_exists('mysqli_connect');
	}

	public function is_open() {
		return is_object($this->mysqli);
	}

	function __destruct() {
		if ($this->is_open()) {
			$this->close();
		}
	}

	public function Engine() {
		return static::LABEL;
	}
	
	public function open($databasename = '') {
//		echo " Open "; var_export(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
		if ($databasename !='')  $this->db = $databasename;
		$this->close();			// close any currently open connection
		$this->mysqli = @new \mysqli($this->host, $this->user, base64_decode($this->password), $this->db);

		if ($this->mysqli->connect_errno) {
			return $this->SetError($this->mysqli->connect_errno, 'Could not connect to ' . $this->host. ': ' . $this->mysqli->connect_error);
		} else {
            $this->mysqli->set_charset('utf8mb4');	// not supported well using Amazon Aurora (can't set server charactersets)
			return $this->SetError(0);
		}
	}
	
	public function close() {
		if ($this->mysqli !== NULL) @$this->mysqli->close();
		$this->mysqli = NULL;
		return $this->SetError(0);
	}

	public function Connection() {
		if (!func_num_args()) {
			return parent::Connection();
		} 
		$value = func_get_arg(0);
		parent::Connection($value);
		if ($this->protocol == 'mysql') {
			//
		} else {
			$this->error = 'not a mysql connection string';
            error_log('[1] mysql - ' . $this->error);
		}
		return $this;
	}

	private function _updateField(&$values, $name, $value) {
		$name = $this->_name($name);
		if ($values != '') $values .= ',';
		$values .= "$name=$value";
	}

	// http://dev.mysql.com/doc/refman/5.1/en/string-syntax.html
	public function formatString($value, $sz = 0) {
	// format the value suitable for storing in the database
	// $value - string needs to be processed, $sz - max size of string (field size)
		if ($value === NULL) return 'NULL';
		if ($sz > 0) $value = mb_substr($value, 0, $sz);
		if (is_object($this->mysqli)) {
//			return '"' . $this->mysqli->real_escape_string($value) . '"';		// may not work correctly
			return '"' . str_replace(array("\\",   '"',   "\n",  "\r", "\t", "\0"),
								array("\\\\", '\\"', "\\n", "\\r", "\\t", "\\0"), $value) . '"';
		} else {
			return parent::FormatString($value);
		}
	}
	
	protected function asWhereItem($where_item) {
		if (!is_array($where_item)) return $where_item;
		reset($where_item);
		$key = key($where_item);
		if (is_numeric($key)) {
			return call_user_func_array([$this, 'perform_substitution'], $where_item);
		}
		$output = '';
		foreach($where_item as $key => $value) {
			if ($output != '') $output .= ' and ';
			$dvalue = $this->formatString($value);
			$key = $this->_name($key);
			$output .= "$key={$dvalue}";
		}
		return $output;
	}

	public function QuerySQL($sql /*, $sqlparams ... */) {		// prepare a query - don't execute it
		if (func_num_args() == 2) {
			$param1 = func_get_arg(1);
		} else {
			$param1 = '';
		}
		if (is_array($param1)) {
			array_unshift($param1, $sql);
			$sql = call_user_func_array([$this, 'perform_substitution'], $param1);
		} else {
			$sqlparam = func_get_args();
			if (count($sqlparam) > 1) $sql = call_user_func_array([$this, 'perform_substitution'], $sqlparam);
		}
		return $sql;
	}

	// adds a result limit to select sql.
	private function _getlimit($sql) {
	// this should be done last
		$value = $this->limit();
		if ($value) {		// must be non-zero
			$tok = $this->getVerb($sql);
			if ($tok == 'SELECT') {
				$sql .= ' limit ' . intval($value);
			}
		}
		return $sql;
	}
	
	private function _AddField(&$select, $field) {
		end($select);
		$k = key($select);
		$select[$k]['delim'] = ',';

		$select[] = [
			'expr_type' => 'colref',
			'base_expr' => $field,
			'delim' => FALSE,
		];
	}
	
	// if we have a select statement, we may have some custom joins, with additional fields
	private function _InsertCustomFields(&$sql) {
		$tok = $this->getVerb($sql);
		if ($tok != 'SELECT') return;
		
		$res = hook_execute('parser.mysql', NULL, $sql);
		// add fields
		foreach($this->extra_fields as $field) {
			$this->_AddField($res['SELECT'], $field);
		}
		$sql = hook_execute('parser.tostring', '', $res);
	}
	
	private function _displayEntry($entry) {
		$function = $entry['function'];
		if (!empty($entry['class'])) $function = $entry['class'] . '::' . $function;
		echo $function . " [{$entry['file']}:{$entry['line']}]<br>\n";
	}
	
	public function Execute($sql) {
		if (is_object($sql)) {
			$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
			foreach($bt as $bt_item) {
				echo $this->_displayEntry($bt_item);
			}
		}
		if (count($this->extra_fields)) $this->_InsertCustomFields($sql);
		//to do: WHERE: use AND if where already present. Insert before order-by or group-by
		$sql .= $this->where() . $this->groupby() . $this->orderby();
		$sql = $this->_getlimit($sql);

		$result = $this->ExecuteRaw($sql);
		return $result;
	}

	public function ExecuteRaw($sql) {
        if ($this->user == '') return FALSE;
        do {
            if ($this->mysqli === NULL || !is_object($this->mysqli)) {
                $this->open();
                if ($this->errnum) {
                    error_log("mysql - {$this->errmsg} [{$this->errnum}]");
                    die("\nDatabase Error: {$this->errmsg} [{$this->errnum}]\n");
                }
            }

            $time = microtime(TRUE);
            if ($this->options & MYSQL_BUFFER) {
                $result = $this->mysqli->Query($sql);
            } else {
				try {
					$result = $this->mysqli->Query($sql, MYSQLI_USE_RESULT);		// for large amounts of data
				} catch (\Exception $E) {
					// php 8.x
				}
            }
            $err = $this->mysqli->errno;
            if ($err == 2006 || $err == 1927) {      // Gone Away / Killed
                $this->mysqli = NULL;
                sleep(1);
            }
        } while ($err == 2006 || $err == 1927);

		$info = [
			'engine' => 'mysql',
			'sql' => $sql,
			'time' => microtime(TRUE) - $time,
			'errnum' => $this->errnum,
			'errmsg' => $this->errmsg,
		];
		$this->lastsql = $sql;
		hook_execute('db.log', $info);
		$res = $this->SetError($this->mysqli->errno, 'Error: ' . $this->mysqli->error);
		
		if (!$res) return $res;		// failed
		$tok = $this->getVerb($sql);
		if ($tok == 'SELECT' || $tok == 'SHOW') {
			return $result;
		} else {
			return TRUE;
		}
	}
	
	public function GetLastInsertId() {
		return $this->mysqli->insert_id;
	}
	
	private function _hook_format($hook_name, &$row) {
		$row = hook_execute($hook_name, $row);
	}
	
	public function callable($database_model, $sqlresult, $callable) {
		$hook_name = $this->hook_query();
		$node = new \database_record($database_model);
		while($row = $sqlresult->fetch_assoc()) {
			if ($hook_name != '') $this->_hook_format($hook_name, $row);
			$node->LoadFromSource($row);
			call_user_func($callable, $node);
		}
	}

	public function TableExists($tablename) {
		$result = $this->ExecuteRaw("select 1 from `{$tablename}`");
		return $this->errnum != 1146;
	}	
	
	public function AsCollection($sqlresult, $database_model = NULL){
		$hook_name = $this->hook_query();
		$collection = new \database_collection($database_model);
		$collection->Parent($this);
		while($row = $sqlresult->fetch_assoc()) {
			if ($hook_name != '') $this->_hook_format($hook_name, $row);
			$collection->Add($row);
		}
		return $collection;
	}
	
	public function LoadPrimaryKey($tablename) {
		$indexes = $this->LoadTableIndex($tablename);
		// find primary
		if (isset($indexes['PRIMARY'])) {
			return $indexes['PRIMARY']['fields'];
		}
		// find first Unique index
		foreach($indexes as $name => $info) {
			if ($info['type'] == 'unique') {
				return $info['fields'];
			}
		}
		return [];		// no indexes
	}
	
	protected function LoadTableIndex($tablename) {
		$result = [];
		$sql = $this->QuerySQL("show keys from `$tablename`");
		$sqlresult = $this->ExecuteRaw($sql);
		while($row = $sqlresult->fetch_assoc()) {
			$keyname = $row['Key_name'];
			if (!isset($result[$keyname])) {
				$non_unique = intval($row['Non_unique']);
				if (strtolower($keyname) == 'primary') {
					$type = 'primary';
					$keyname = strtoupper($keyname);
				} elseif ($non_unique) {
					$type = 'index';
				} else {
					$type = 'unique';
				}
				$result[$keyname] = [
					'type' => $type,
					'fields' => []
				];
			}
			$fieldname = $row['Column_name'];
			if (!is_null($row['Sub_part'])) $fieldname .= "({$row['Sub_part']})";		// index length
			$result[$keyname]['fields'][$row['Seq_in_index']] = $fieldname;
		}
		return $result;
	}
	
	// this writes to a temp table?
	public function GetTableDefinition($tablename) {
		$result = [ 'fields' => [], 'index' => [] ];
		$sql = "show full columns from `{$tablename}`";
		$sqlresult = $this->ExecuteRaw($sql);
		if ($sqlresult !== FALSE) {
			while($row = $sqlresult->fetch_assoc()) {
				$this->_ProcessColumnEntry($result['fields'], $row);
			}
			$result['index'] = $this->LoadTableIndex($tablename);
		}
		return $result;
	}
	
	private function _ProcessColumnEntry(&$result, $row) {
		unset($row['Privileges'], $row['Key'], $row['Collation']);
		// parse the returned info into our internal format
		$x = $this->_extractType($row['Type']);
		if ($x === FALSE) {
			var_dump($row);
			die("\nUnknown mysql data type: {$row['Type']}\n");
		}
		list($type, $length, $attr) = $x;
		$node = ['type' => $type];
		if (isset($row['Null'])) {
			$node['null'] = $row['Null'] == 'YES';
		}
		if ($length !== NULL) $node['size'] = $length;
		if ($row['Extra'] == 'auto_increment') $node['autoinc'] = TRUE;
		if (!is_null($row['Default'])) {
			$node['default'] = $row['Default'];
			if ($type == 'time' && $node['default'] == 'CURRENT_TIMESTAMP') $node['default'] = 'NOW';
		}
		if ($attr == 'unsigned') $node['unsigned'] = TRUE;
		if ($row['Comment'] !== '') $node['comment'] = $row['Comment'];
		$result[$row['Field']] = $node;
	}
	
	private function _extractType($type) {
		if (preg_match('/^(.*)\((.*?)\)\s*(unsigned|)$/', $type, $match)) {
			$type = strtolower($match[1]);
			$len  = $match[2];
			$attr = strtolower($match[3]);
		} else {
			$type = strtolower($type);
			$len = NULL;
			$attr = NULL; 
		}
		switch($type) {
			case 'varchar':
				$type = 'string';
				$len = intval($len, 10);
				break;
			case 'bigint':
			case 'int':
				$len = intval($len, 10);
				break;
			case 'enum':
                preg_match_all("~'(.*?)'(\,|)~", $len, $matches);
                $len = $matches[1];
				break;
			case 'double':
			case 'float':
                $type = 'float';
                break;
			case 'mediumtext':
				$type = 'text';
				break;
			case 'longblob':
			case 'blob':
				$type = 'blob';
				break;
			case 'datetime':
				$type = 'datetime';
				break;
			case 'timestamp':
				$type = 'time';
				break;
			case 'tinyint':
				if ($len == 1) {
					$type = 'boolean';
					break;
				}
			default:
				return FALSE;
		}
		return array($type, $len, $attr);
	}

    // to do: drop  indexes
	public function SetTableDefinition($tablename, $result, $isNew) {
		$sql = '';
		foreach($result as $field_item) {
			$def = '';
            $isExist = !empty($field_item['exist']);
            if ($field_item['type'] == 'column') {
                if ($isNew) {
                    $def = "`{$field_item['name']}` {$field_item['def']}";
                } else {
                    if ($isExist) {
                        $def = 'change column ';
                        if (!is_null($field_item['old'])) {
                            $def .= "`{$field_item['old']}` `{$field_item['name']}`";
                        } else {
                            $def .= "`{$field_item['name']}` `{$field_item['name']}`";
                        }
                    } else {
                        $def = 'add column `' . $field_item['name'] . '`';
                    }
                    $def .= ' ' . $field_item['def'];
                }
				if ($field_item['null'] !== NULL) {
					$def .= $field_item['null'] ? ' null' : ' not null';
				}
                if ($field_item['default'] !== NULL) {
					if ($field_item['def'] == 'mediumtext') {
						// cannot have default value
					} elseif ($field_item['def'] == 'timestamp' && $field_item['default'] == 'NOW') {
						$def .= ' default current_timestamp';
					} else {
						$def .= ' default ' . $this->formatString($field_item['default']);
					}
                }
                if (array_key_exists('comment', $field_item) && $field_item['comment'] != '') {
                    $def .= ' comment ' . $this->formatString($field_item['comment']);
                }
            } elseif ($field_item['type'] == 'dropcolumn') {
				if (!$isNew) {
					$def = "drop column `{$field_item['name']}`";
				}
            } elseif ($field_item['type'] == 'index') {
                $keys = explode(',', $field_item['fields']);
                foreach($keys as &$key_item) {
                    if (preg_match('~^(.*?)\((\d+)\)~', $key_item, $match)) {
                        $key_item = $match[1];
                        $size = '(' . $match[2] . ')';
                    } else {
                        $size = '';
                    }
                    $key_item = "`$key_item`" . $size;
                }
                $keys = join(',', $keys);
                if ($isNew) {
                    switch($field_item['subtype']) {
                        case 'primary':
                            $def = "primary key ($keys)";
                            break;
                        case 'unique':
                            $def = "unique key `{$field_item['name']}` ($keys)";
                            break;
                        case 'index':
                            $def = "index `{$field_item['name']}` ($keys)";
                            break;
                    }
                } else {       
                    if ($isExist) {     // update is a drop and add:
                        switch($field_item['subtype']) {
                            case 'primary':
                                $def = "drop primary key";
                                break;
                            case 'unique':
                            case 'index':
                                $def = "drop index `{$field_item['name']}`";
                                break;
                        }
                        if ($sql != '') $sql .= ",\n";
                        $sql .= '  ' . $def;
                    }
                    switch($field_item['subtype']) {        // add index
                        case 'primary':
                            $def = "add primary key ($keys)";
                            break;
                        case 'unique':
                            $def = "add unique `{$field_item['name']}` ($keys)";
                            break;
                        case 'index':
                            $def = "add index `{$field_item['name']}` ($keys)";
                            break;
                    }
                }
            }
            
			if ($def != '') {
				if ($sql != '') $sql .= ",\n";
				$sql .= '  ' . $def;
			}
		}
		if ($isNew) {
			$sql = "create table `$tablename` ($sql)";
		} else {
			$sql = "alter table `$tablename` $sql";
		}
//      echo "<pre>". htmlspecialchars($sql) . "</pre>";
        error_log("Database Migration: $sql");
		$this->ExecuteRaw($sql);
		return $this->SetError($this->mysqli->errno, "error: " . $this->mysqli->error);
	}

}

/*

CREATE TABLE `users` (
  `user_id` varchar(32) COLLATE utf8_bin NOT NULL,
  `user_hash` int(9) NOT NULL,
  `enumeration` enum('one','two','three','four','five','six','seven') COLLATE utf8_bin DEFAULT 'four',
  `johnny` int(11) UNSIGNED NOT NULL COMMENT 'a surplus field'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

ALTER TABLE `users`
  ADD PRIMARY KEY (`johnny`),
  ADD UNIQUE KEY `user_id` (`user_id`(10));

ALTER TABLE `users`
  MODIFY `johnny` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'a surplus field';COMMIT;

  */