<?php

/**
* SmartGatewayクラス
*
* PHP version 5
*
* @package    core
* @author     stk2k <stk2k@sazysoft.com>
* @copyright  2008 stk2k, sazysoft
*/
class SmartGateway extends Object
{
	private $_source;
	private $_builder;
	private $_models;

	/*
	 *	コンストラクタ
	 */
	public function __construct()
	{
		$this->refresh();
	}

	/*
	 *	データソース
	 */
	public function getDataSource()
	{
		return $this->_source;
	}

	/*
	 *	SQLビルダ
	 */
	public function getSQLBuilder()
	{
		return $this->_builder;
	}

	/*
	 *	接続情報を再設定
	 */
	public function refresh()
	{
		// データソースの設定読み込み
		$data_source_name = Profile::getString( s('DB_DATA_SOURCE') );
		$this->_source = Factory::createDataSource( $data_source_name );

		// SQLビルダの設定読み込み
		$builder_name = $this->_source->getBackend();
		if ( $builder_name && !$builder_name->isEmpty() ){
			$this->_builder = Factory::createSQLBuilder( $builder_name );
		}
	}

	/*
	 *	テーブルモデルの取得
	 */
	public function getModel( String $model_name )
	{
		// キャッシュにあればそれを返す
		if ( isset($this->_models["$model_name"]) ){
			return $this->_models["$model_name"];
		}
		// ないので作る
		$model = Factory::createTableModel( $model_name );

		$this->_models["$model_name"] = $model;

		return $model;
	}

	/*
	 *	保存
	 */
	public  function save( String $model_name, DTO $dto, String $converter_str = NULL )
	{
		$conv = $converter_str ? EncodingConverter::fromString($converter_str) : NULL;

		// テーブルモデル取得
		$model = $this->getModel( $model_name );

		try{
			// UPDATEとINSERTの判定
			$pk = $model->getPrimaryKey();

			if ( isset($dto["$pk"]) ){
				// プライマリキーで検索してみてレコードがなければINSERT、あればUPDATE
				$db_dto = $this->findByID( $model_name, s($dto["$pk"]) );
				$is_new = ($db_dto === NULL);
			}
			else{
				// プライマリキーの値が指定されていなければINSERT
				$is_new = TRUE;
			}


			// SQLを作成
			if ( $is_new ){
				list( $sql, $params ) = $this->_builder->buildInsertSQL( $model, $dto );
			}
			else{
				list( $sql, $params ) = $this->_builder->buildUpdateSQL( $model, $dto );
			}

			// 文字コード変換とmagic_quote_gpcによる自動エスケープ解除
			if ( $conv ){
				foreach( $params as $key => $value ){
					if ( is_string($value) ){
						$value = $conv->convertEncoding( $value );
						if ( get_magic_quotes_gpc() ) {
							$value = stripslashes( $value );
						}
						$params[$key] = $value;
					}
				}
			}

			// SQL実行
			$this->_source->prepareExecute( s($sql), v($params) );

			// IDを返す
			if ( $is_new ){
				$sql = $this->_builder->buildLastIdSQL();
				
				// 実行
				$result = $this->_source->prepareExecute( s($sql) );

				// フェッチ
				$row = $this->_source->fetchArray( $result );

				$new_id = $row[0];
			}
			else{
				$new_id = $dto["$pk"];
			}

			return $new_id;
		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#save failed" ) );
		}
	}

	/*
	 *	SQL実行(INSERT/DELETE/UPDATE)
	 */
	public  function execute( String $sql, Vector $params = NULL )
	{
		try{
			// 実行
			$this->_source->prepareExecute( $sql, $params );
		}
		catch ( Exception $e ) 
		{
			// 上位にthrow
			_throw( new DBException( "exec_SQL failed" ) );
		}
	}

	/*
	 *	SQL実行(SELECT)
	 */
	public  function query( String $sql, Vector $params = NULL, String $converter_str = NULL )
	{
		try{

			$conv = $converter_str ? EncodingConverter::fromString($converter_str) : NULL;

			// コンテナからDataSourceコンポーネントを取得
			$ds = $this->_source;

			$a = array();

			// 実行
			$result = $ds->prepareExecute( $sql, $params );

			// フェッチ
			while( $row = $ds->fetchAssoc( $result ) ){
				foreach( $row as $key => $value ){
					// 文字列ならエンコーディング変換
					if ( $conv && is_string($value) ){
						$value = $conv->convertEncoding($value);
						$row[$key] = $value;
					}
				}
				$a[] = $row;
			}
		}
		catch ( Exception $e ) 
		{
			// 上位にthrow
			_throw( new DBException( "exec_SQL failed" ) );
		}

		return $a;
	}

	/*
	 *	SQL検索
	 */
	public  function findBySQL( String $model_name, $sql, $params = NULL, String $converter_str = NULL, HashMap $model_count = NULL ) 
	{
		try{
			$conv = $converter_str ? EncodingConverter::fromString($converter_str) : NULL;

			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			if ( $model_count === NULL ){
				$model_id = $model->getModelID();
				$model_count = new HashMap( array( "$model_id" => 1 ) );
			}
			else{
				$model_id = $model->getModelID();
				$model_count->set( $model_id, 1 );
			}

			$a = new Vector;

			// 関連一覧を取得
			$relation_list = $model->getRelationList();
			$relation_map = array();
			foreach ($relation_list as $field) 
			{
				// @targetを取得
				$target = $model->getAnnotationValue( s($field), s('target') );
				if ( !$target ){
					_throw( new DBException( "@target is missing for @relation: field=$field" ) );
				}
				$target = $this->getModel( $target );

				// @foreign_keyを取得
				$foreign_key = $model->getAnnotationValue( s($field), s('foreign_key') );

				// @key_fieldを取得
				$key_field = $model->getAnnotationValue( s($field), s('key_field') );

				// @linkageを取得
				$linkage = $model->getAnnotationValue( s($field), s('linkage'), s('inner') );
				if ( !$linkage->equals( s('inner') ) && !$linkage->equals( s('outer') ) ){
					_throw( new DBException( "Invalid linkage anottation : $linkage" ) );
				}

				// @extractを取得
				$extract = $model->getAnnotationValue( s($field), s('extract') );

				// 外部キーとキーフィールドが省略された場合の処理
				if ( !$foreign_key ){
					if ( $linkage->equals( s('inner') ) ){
						$foreign_key = $model->getForeignKey( $target->getModelID() );
					}
					else if ( $linkage->equals( s('outer') ) ){
						$foreign_key = $target->getForeignKey( $model->getModelID() );
					}
				}
				if ( !$key_field ){
					if ( $linkage->equals( s('inner') ) ){
						$key_field = $target->getPrimaryKey();
					}
					else if ( $linkage->equals( s('outer') ) ){
						$key_field = $model->getPrimaryKey();
					}
				}

				// マップに保存しておく

				$relation_map[ $field ] = new DBRelation( $target, $foreign_key, $key_field, $linkage, $extract );
			}

			// DataSourceを取得
			$ds = $this->_source;

			// フィールド一覧
			$field_list = $model->getFieldList();

			// 実行
			$result = $ds->prepareExecute( s($sql), v($params) );

			// フェッチ
			while( $row = $ds->fetchAssoc( $result ) )
			{
				$dto = $model->createDTO();

				// SQL結果セットからDTOに値をコピー
				foreach ($field_list as $name) 
				{
					// フィールド値
					$value = isset($row[$name]) ? $row[ $name ] : NULL;
					// 文字列ならエンコーディング変換
					if ( $conv && is_string($value) ){
						$value = $conv->convertEncoding($value);
					}
					// 返却用配列に設定
					$dto->$name = $value;
				}

				// プライマリキーを取得
				$pk = $model->getPrimaryKey();
				$id = $row[ "$pk" ];
				// 関連を取得
				foreach ($relation_list as $field) 
				{
					// 関連情報を取得
					$relation = $relation_map[ $field ];
					// 関連先テーブルモデル
					$target = $relation->getTarget();
					// 外部キー名
					$foreign_key = $relation->getForeignKey();
					// キーフィールド名
					$key_field = $relation->getKeyField();
					// リンケージ指定
					$linkage = $relation->getLinkage();
					// 展開フィールド
					$extract = $relation->getExtract();
					// 関連テーブル名
					$table_name = $target->getTableName();

					// SQL作成
					switch ( $linkage ){
					case 'inner':
						{
							// 内部リンケージ : 外部キーが検索中テーブル側にある
							$key_value = $row[ "$foreign_key" ];
							$sql = "select * from $table_name where $key_field = ?";
						}
						break;
					case 'outer':
						{
							// 外部リンケージ : 外部キーが関連テーブル側にある
							$key_value = $row[ "$key_field" ];
							$sql = "select * from $table_name where $foreign_key = ?";
						}
						break;
					default:
						{
							// ここにはこないはず
							_throw( new DBException( "Invalid linkage anottation : $linkage" ) );
						}
						break;
					}
					// 再帰ループチェック
					$target_model_id = $target->getModelID();
					$cnt = $model_count->get( $target_model_id );
					if ( $cnt === NULL ){
						// 結果をDTOにマージ
						$child_result = $this->findBySQL( $target->getModelID(), $sql, array($key_value), $converter_str, $model_count );
						switch ( $extract ){
						case 'field':
							{
								// フィールドに展開
								if ( $child_result ){
									$child = $child_result->shift();
									if ( $child ){
										// アノテーションフィールド自体にはDTOをセット
										$dto->$field = $child;
										// @extractの処理
										foreach( $child as $key => $value ){
											// @extractで指定されているフィールドに展開
											$extract_field = $model->getExtractField( $target->getModelID(), s($key) );
											if ( $extract_field ){
												$dto->$extract_field = $value;
											}
										}
									}
								}
							}
							break;
						case 'array':
						default:
							{
								// 配列にセット
								$dto->$field = $child_result;
							}
							break;
						}
					}
				}
				$a->add( $dto );
			}
		}
		catch ( Exception $e ) 
		{
			_catch( $e );

			// 上位にthrow
			_throw( new DBException( "findBySQL failed" ) );
		}

		return $a;
	}

	/*
	 *	最初の１件取得
	 */
	public  function findFirst( String $model_name, SQLCriteria $criteria = NULL, String $converter_str = NULL ) 
	{
		try{
			if ( !$criteria ){
				$criteria = new SQLCriteria();
			}

			// LIMIT=1に
			$criteria->setLimit( i(1) );

			// 検索実行
			$result = $this->findAll( $model_name, $criteria, $converter_str );

			return $result ? $result->shift() : NULL;
		}
		catch ( Exception $e )
		{
			_catch( $e );

			_throw( new DBException( "SmartGateway#findFirst failed" ) );
		}
	}

	/*
	 *	全件取得
	 */
	public  function findAll( String $model_name, SQLCriteria $criteria = NULL, String $converter_str = NULL ) 
	{
		try{
			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			// テーブル名
			$table = $model->getTableName();

			// SQLビルダを取得
			$builder = $this->_builder;

			// SQLの作成
			$sql = $builder->buildSelectSQL( $model, $criteria );

			// パラメータ
			$params = $criteria ? $criteria->getParams() : NULL;

			// 検索実行
			$result = $this->findBySQL( $model_name, $sql, $params, $converter_str );

			return $result;
		}
		catch ( Exception $e )
		{
			_catch( $e );

			_throw( new DBException( "SmartGateway#findAll failed" ) );
		}
	}

	/*
	 *	１つのフィールドのみで検索
	 */
	public  function findAllBy( String $model_name, $field, $value, SQLCriteria $criteria = NULL, String $converter_str = NULL )
	{
		try{
			if ( !$criteria ){
				$criteria = new SQLCriteria();
			}

			$criteria->setWhereClause( "$field = ?" );
			$criteria->setParams( array( $value ) );

			$a = $this->findAll( $model_name, $criteria, $converter_str );

			return $a;
		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#findAllBy failed" ) );
		}
	}

	/*
	 *	IDから検索(１つだけ指定した場合)
	 */
	public  function findByID( String $model_name, String$id, String $converter_str = NULL ) 
	{
		try{
			// １つだけ指定した場合
			$where_clause = s('id = ?');
			$params = v(array( $id ));

			$criteria = new SQLCriteria( $where_clause, $params );

			return $this->findFirst( $model_name, $criteria, $converter_str );
		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#findByID failed" ) );
		}
	}

	/*
	 *	IDから検索
	 */
	public  function findAllByID( String $model_name, Vector $id_array ) 
	{
		try{
			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			// 複数個指定した場合
			$where = array();
			$params = array();
			
			foreach( $id_array as $id ){
				$where[] = "?";
				$params[] = $id;
			}

			$where_clause = "id in (" . implode(",",$where) . ")";

			$condition = new SQLCriteria( $where_clause, $params );

			$a = $this->findAll( $dto, $condition );

			return $a;
		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#findAllByID failed" ) );
		}
	}

	/*
	 *	削除（１つだけ指定した場合）
	 */
	public  function destroyById( String $model_name, String $id ) 
	{
		try{
			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			// キーフィールド名
			$pk = $model->getPrimaryKey();

			// SQLパラメータ
			$params = array( "$id" );

			// 条件
			$criteria = new SQLCriteria();
			$criteria->setWhereClause( s("$pk = ?") );
			$criteria->setParams( v($params) );

			// SQLビルダを使いSQLを生成
			$sql = $this->_builder->buildDeleteSQL( $model, $criteria );

			$this->execute( s($sql), v($params) );
		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#destroyById failed" ) );
		}
	}

	/*
	 *	削除（複数を指定した場合）
	 */
	public  function destroyAllById( String $model_name, Vector $id_array ) 
	{
		try{
			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			// テーブル名
			$table = self::getTableNameFromDTO( $dto );

			// 複数個指定した場合
			$where = array();
			$params = array();
			
			foreach( $id_array as $id ){
				$where[] = "?";
				$params[] = $id;
			}

			$sql = "delete from $table where id in (" . implode(",",$where) . ")";

			$this->execute( $sql, $params );
		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#destroyById failed" ) );
		}
	}

	/*
	 *	１つのフィールドに合致するレコードを全削除
	 */
	public  function destroyBy( String $model_name, String $field, $value )
	{
		$where_clause = "$field = ?";
		$params = array( $value );

		$this->destroyAll( $dto, $where_clause, $params );
	}


	/*
	 *	条件に合致するレコードを全削除
	 */
	public  function destroyAll( String $model_name, String $where_clause = NULL, Vector $params = NULL ) 
	{
		try{
			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			// テーブル名
			$table = self::getTableNameFromDTO( $dto );

			$sql = "delete from $table where $where_clause";

			$this->execute( $sql, $params );
		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#destroyAll failed" ) );
		}
	}

	/*
	 *	テーブルのレコード数を取得
	 */
	private function _checkSQLBuilder() 
	{
		// オブジェクトがなければエラー
		if ( !$this->_builder ){
			_throw( new DBException( 'No SQL Builder Specified' ) );
		}
	}

	/*
	 *	テーブルのレコード数を取得
	 */
	public  function count( String $model_name, String $where_clause = NULL, Vector $params = NULL ) 
	{
		try{
			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			// SQLビルダのチェック
			$this->_checkSQLBuilder();

			// WHERE句の生成
			if ( $where_clause ){
				$criteria = new SQLCriteria();
				$criteria->setWhereClause( $where_clause );
				$criteria->setParams( $params );
			}
			else{
				$criteria = NULL;
			}

			// SQL
			$sql = $this->_builder->buildCountSQL( $model, $criteria );

			// SQL実行
			$result = $this->_source->prepareExecute( s($sql), v($params) );

			// フェッチ
			$rows = $this->_source->fetchArray( $result );

			return $rows[0];
		}
		catch ( Exception $e )
		{
			_catch( $e );
			_throw( new DBException( "SmartGateway#count failed" ) );
		}
	}

	/*
	 *	特定フィールドの最大値を取得
	 */
	public  function max( String $model_name, String $field, String $where_clause = NULL, Vector $params = NULL ) 
	{
		try{
			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			if ( $where_clause ){
				$criteria = new SQLCriteria();
				$criteria->setWhereClause( $where_clause );
				$criteria->setParams( $params );
			}
			else{
				$criteria = NULL;
			}

			// SQL
			$sql = $this->_builder->buildMaxSQL( $model, $field, $criteria );

			// SQL実行
			$result = $this->_source->prepareExecute( $sql, $params );

			// フェッチ
			$rows = $this->_source->fetchArray( $result );

			return $rows[0];
		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#count failed" ) );
		}
	}


	/*
	 *	DBを作成
	 */
	public  function createDatabase( String $db_name, String $charset ) 
	{
		try{
			// SQLビルダを使いSQLを生成
			$sql = $this->_builder->buildCreateDatabaseSQL( $db_name, $charset );

			// SQL実行
			$this->_source->execute( s($sql) );

		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#createDatabase failed" ) );
		}
	}

	/*
	 *	テーブルを作成
	 */
	public  function createTable( String $model_name ) 
	{
		try{
			// テーブルモデル取得
			$model = $this->getModel( $model_name );

			// SQLビルダを使いSQLを生成
			$sql = $this->_builder->buildCreateTableSQL( $model );

			// SQL実行
			$this->_source->execute( s($sql) );

		}
		catch ( Exception $e )
		{
			_throw( new DBException( "SmartGateway#createTable failed" ) );
		}
	}
}
return __FILE__;
