<?php
// last edit: 12.12.2018

/*
помимо указанных ниже готовых возможностей класса, в объекте так же будут на автомате подхватываться методы со следующими именами:

	->init()
	  	вызывается в __construct, после выполнения всех необходимых предварительных настроек,
	  	позволяя неперекрывать этот метод, но выполнить донастройки, если такие требуются

	->get_<name>()
		вызывается вместо метода get(<name>), при обращении к полю <name>. 
		При необходимости позволяет подкорректировать данные и вернуть их вместо исходных значений.
		Для получения же исходных значений внутри данного метода рекомендуется использовать метод get.

Краткий перечень функционала:
	::$fields

	::from($array_data)
	::sql($sql_select)
	::load([$where, $other_sql])
	::one([$where, $other_sql])
	::count([$where, $other_sql])
	::pages($limit [, $where, $other_sql])

	->isNull()
	->getRow()
	->get($field_name)
	
	->#field_name
	->#name
		=> get_#name()

#### системный функционал
	->__get($field_name)
	
*/


class DBJoinModel{

	const JOIN_TYPE_ARRAY = ['LEFT', 'LEFT UOTER', 'RIGHT', 'RIGHT OUTER', 'INNER', 'FULL', 'CROSS', 'FULL OUTER', 'NATURAL', 'NATURAL INNER', 'NATURAL LEFT', 'NATURAL RIGHT','STRAIGHT_JOIN'];

	static $default_join_type = 'INNER';

	/**
	* массив полей, которые необходимо создавать при формировании новой записи (для вставки в insert)
	* @var array
	*/
	static $fields;
	static $fields_as_tables;

	/**
	* массив таблиц, которые должны состыковываться через JOIN, каждую связку можно задавать одним из следующих примеров:
	* - <table_name>
	* - <table_name> => <as_table_name>
	* - [<type_join>,<table_name>]
	* - [<type_join>,<table_name>] => <as_table_name>
	*
	* <type_join> - принимает одно из значений константы JOIN_TYPE_ARRAY
	*
	* @var aaray
	*/
	static $tables;

	/**
	* массив условий соединения таблиц через ON, каждое условие должно выглядеть следующим образом:
	* - <table_name> => <table_right_field>                            // <table_name>.id = <table_right_field>
	* - <table_name> => [<str_compare>]                                // <str_compare>
	* - <table_name> => [<left_field>,<table_right_field>]             // <table_name>.<left_field> = <table_right_field>
	* - <table_name> => [<left_field>,<table_compare>,<right_compare>] // <table_name>.<left_field> = <table_compare>.<right_compare>
	*
	* @var array
	*/
	static $tablesON =[];

	/**
	* массив условий соединения таблиц через ON, каждое условие должно выглядеть следующим образом:
	* - <table_name> => <field_compare>
	* - <table_name> => [<field_compare1>,<field_compare2>,...]
	*
	* @var array
	*/
	static $tablesUSING =[];

	


	static $compile_sql_join = false;

	/**
	* Преобразование массива строк таблицы БД, в массив объектов текущей модели
	* @param array $arr массива строк таблицы БД
	* @return array
	*/
	static function from($arr){
		return array_map(function($row){
			$temp = new static($row);
			return $temp;
		}, $arr);		
	}

	/**
	* Делает SQL-запрос, который предпологает получения таблицы, и сразу преобразует результат в массив объектов текущей модели
	* @param $sqlselect  SQL-запрос
	* @return array
	*/
	static function sql($sqlselect){
		return static::from(DB::table($sqlselect));
	}


	static function get_sql_join_from(){
		if(!static::$compile_sql_join){
			$sql = '';
			foreach (static::$tables as $table => $asName) {
				if(is_numeric($table)){
					$table = $asName;
					$asName = '';
				}
				
				if(is_array($table)){
					$type_join = $table[0];
					$table = $table[1];
				}else $type_join = static::$default_join_type;

				if($sql == '')
					$type_join = '';
				elseif(!in_array($type_join, static::JOIN_TYPE_ARRAY))
					$type_join = 'INNER';


				if($sql != ''){
					if(isset(static::$tablesON[$table]) || ($asName && isset(static::$tablesON[$asName]))){
						$cmp = static::$tablesON[$table] ?? static::$tablesON[$asName];
						if(is_array($cmp)){
							if(count($cmp)==2)
								$compare = ' ON '.($asName ?: $table).'.'.$cmp[0].'='.$cmp[1].' ';
							elseif(cout($cmp)>2)
								$compare = ' ON '.($asName ?: $table).'.'.$cmp[0].'='.$cmp[1].'.'.$cmp[2].' ';
							else $compare = ' ON '.$cmp;
						}else $compare = ' ON '.($asName ?: $table).'.id='.$cmp;
					}elseif(isset(static::$tablesUSING[$table]) || ($asName && isset(static::$tablesUSING[$asName]))){
						$fields = static::$tablesUSING[$table] ?? static::$tablesUSING[$asName];
						if(is_array($fields))
							$compare = ' USING('.join(',',$fields).') ';
						else 
							$compare = ' USING('.$fields.') ';
					}else $compare = '';

				}else $compare = '';

				$sql .= ($type_join == '' ? ($sql == '' ? ' ' : ',') : ' '.$type_join.' JOIN ').$table.' '.$asName.$compare;
			}

			static::$compile_sql_join = $sql;

			if(!isset(static::$fields_as_tables)){
				static::$fields_as_tables = static::$fields;

				foreach (static::$fields as &$field){
					$temp = strtolower($field);
					if(strpos($temp, 'as'))
						$temp = explode('as', $field);
					else
						$temp = explode('.', $field);
					$field = trim(array_pop($temp));
				}
			}
		}
			
		return static::$compile_sql_join;
	}

	/**
	* Делает выборку строк и возвращает массив объектов текущей модели
	* @param string|array $where  условие выборки
	* @param string       $other  дополнительные элементы запроса
	* @return array
	*/
	static function load($where = '', $other = ''){
		return static::from(DB::select(static::get_sql_join_from(),static::$fields_as_tables,$where,$other));
	}

	/**
	* Делает выборку строк и возвращает объект текущей модели
	* @param string|array $where  условие выборки
	* @param string       $other  дополнительные элементы запроса
	* @return object
	*/
	static function one($where = '', $other = ''){
		return new static(DB::select_one(static::get_sql_join_from(),static::$fields_as_tables,$where,$other));
	}


	static function count($where = '', $other = ''){
		return DB::count(static::get_sql_join_from(), $where, $other);
	}

	static function pages($limit, $where = '', $other = ''){
		return DB::count_page(static::get_sql_join_from(), $limit, $where, $other);
	}


	private
		$row, $null;

	/**
	* Конструктор объекта модели
	* @param false|array $row  набор данных, или набор полей БД, или false (даст понять, что поля определены в текущем классе, и стоит брать их для формирования новой строки)
	* @return object
	*/
	function __construct($row = false){
		$isNew = $row === false || count($row) == 0 || isset($row[0]);
		$this->null = $isNew;
		$this->row = $isNew ? array_fill_keys($isNew ? (static::$fields ?: []) :$row, null) : $row;

		if(method_exists($this, 'init'))
			$this->init();
	}


	/**
	* возвращает флаг признака новой строки
	* @return boolean
	*/
	function isNull(){
		return $this->null;
	}

	/**
	* возвращает данные в виде массива
	* @return array
	*/
	function getRow(){
		return $this->row;
	}

	/**
	* формирует удобную отладочную таблицу текущего состояния данных объекта модели
	* @return string
	*/
	function __toString(){
		$rUp = [];
		$rDown = [];
		foreach($this->row as $field => $value){
			$valStr = ''.str_replace(["\n","\r"], ['#10','#13'], $value);
			$lenVal = strlen( $valStr ); 
			$diff = mb_strlen($valStr);

			$field_a = $field;

			$len = max(strlen($field_a), $diff);
			$diff = $lenVal - $diff;

			$rUp[]   = str_pad($field_a,  $len, ' ', STR_PAD_BOTH);
			$rDown[] = str_pad($valStr, $len < $lenVal ? $len + $diff : $len, ' ', STR_PAD_LEFT);
		}
		$rUp = implode('|',$rUp);
		$rDown = implode('|',$rDown);
		return $rUp . "\n" . str_repeat('-', strlen($rUp)) . "\n" . $rDown;
	}

	/**
	* Возвращает текущее значение указанного поля
	* @param string $name  имя поля
	* @return mixed
	*/
	function get($name){
		return $this->row[$name];
	}

	/**
	* Возвращает текущее значение указанного поля, при этом может вызвать метод дополнительной обработки
	* @param string $name  имя поля
	* @return mixed
	*/
	function __get($name){
		$func ='get_'.$name;
		if(method_exists($this, $func))
			return $this->$func();
		else return $this->get($name);
	}

	/**
	* Проверяет поле на существование
	* @param string $name  имя поля
	* @return boolean
	*/
	function __isset($name){
		return isset($this->row[$name]) || method_exists($this,'get_'.$name);
	}

}