<?php
/**
 * CleverReach WordPress Integration.
 *
 * @package CleverReach
 */
namespace CleverReach\WordPress\Components\Repositories;

use CleverReach\WordPress\Components\Utility\Database;
use CleverReach\WordPress\IntegrationCore\Infrastructure\ORM\Entity;
use CleverReach\WordPress\IntegrationCore\Infrastructure\ORM\Exceptions\QueryFilterInvalidParamException;
use CleverReach\WordPress\IntegrationCore\Infrastructure\ORM\Interfaces\RepositoryInterface;
use CleverReach\WordPress\IntegrationCore\Infrastructure\ORM\QueryFilter\QueryCondition;
use CleverReach\WordPress\IntegrationCore\Infrastructure\ORM\QueryFilter\QueryFilter;
use CleverReach\WordPress\IntegrationCore\Infrastructure\ORM\Utility\IndexHelper;

/**
 * Class Base_Repository
 *
 * @package CleverReach\WordPress\Components\Repositories
 */
class Base_Repository implements RepositoryInterface {

	/**
	 * Entity class FQN.
	 *
	 * @var string
	 */
	protected $entity_class;
	/**
	 * Database session object.
	 *
	 * @var \wpdb
	 */
	protected $db;
	/**
	 * Table name.
	 *
	 * @var string
	 */
	protected $table_name;

	/**
	 * Base_Repository constructor.
	 */
	public function __construct() {
		global $wpdb;
		$this->db         = $wpdb;
		$this->table_name = $this->db->prefix . Database::ENTITY_TABLE;
	}

	/**
	 * Returns full class name.
	 *
	 * @return string Full class name.
	 */
	public static function getClassName() {
		return __CLASS__;
	}

	/**
	 * Sets repository entity
	 *
	 * @noinspection PhpDocMissingThrowsInspection
	 *
	 * @param string $entity_class Entity class.
	 */
	public function setEntityClass( $entity_class ) {
		$this->entity_class = $entity_class;
	}

	/**
	 * Executes select query.
	 *
	 * @param QueryFilter $filter Filter for query.
	 *
	 * @return Entity[] A list of found entities ot empty array.
	 * @throws QueryFilterInvalidParamException If filter condition is invalid.
	 */
	public function select( QueryFilter $filter = null ) {
		$this->table_name = $this->db->prefix . Database::ENTITY_TABLE;
		/**
		 * Entity object.
		 *
		 * @var Entity $entity
		 */
		$entity = new $this->entity_class();
		$type   = $entity->getConfig()->getType();

		$query = "SELECT * FROM {$this->table_name} WHERE type = '$type' ";
		if ( $filter ) {
			$query .= $this->apply_query_filter( $filter, IndexHelper::mapFieldsToIndexes( $entity ) );
		}

		$raw_results = $this->db->get_results( $query, ARRAY_A );

		return $this->translateToEntities( $raw_results );
	}

	/**
	 * Executes select query and returns first result.
	 *
	 * @param QueryFilter $filter Filter for query.
	 *
	 * @return Entity|null First found entity or NULL.
	 * @throws QueryFilterInvalidParamException If filter condition is invalid.
	 */
	public function selectOne( QueryFilter $filter = null ) {
		if ( ! $filter ) {
			$filter = new QueryFilter();
		}

		$filter->setLimit( 1 );
		$results = $this->select( $filter );

		return ! empty( $results ) ? $results[0] : null;
	}

	/**
	 * Executes insert query and returns ID of created entity. Entity will be updated with new ID.
	 *
	 * @param Entity $entity Entity to be saved.
	 *
	 * @return int Identifier of saved entity.
	 */
	public function save( Entity $entity ) {
		if ( $entity->getId() ) {
			$this->update( $entity );

			return $entity->getId();
		}

		return $this->save_entity_to_storage( $entity );
	}

	/**
	 * Executes update query and returns success flag.
	 *
	 * @param Entity $entity Entity to be updated.
	 *
	 * @return bool TRUE if operation succeeded; otherwise, FALSE.
	 */
	public function update( Entity $entity ) {
		$this->table_name = $this->db->prefix . Database::ENTITY_TABLE;

		$item = $this->prepare_entity_for_storage( $entity );

		// Only one record should be updated.
		return 1 === $this->db->update( $this->table_name, $item, array( 'id' => $entity->getId() ) );
	}

	/**
	 * Executes delete query and returns success flag.
	 *
	 * @param Entity $entity Entity to be deleted.
	 *
	 * @return bool TRUE if operation succeeded; otherwise, FALSE.
	 */
	public function delete( Entity $entity ) {
		$this->table_name = $this->db->prefix . Database::ENTITY_TABLE;

		return false !== $this->db->delete( $this->table_name, array( 'id' => $entity->getId() ) );
	}

	/**
	 * Removes records by a given query filter.
	 *
	 * @param \CleverReach\WordPress\IntegrationCore\Infrastructure\ORM\QueryFilter\QueryFilter|null $filter
	 *
	 * @return int Count of deleted rows
	 *
	 * @throws QueryFilterInvalidParamException
	 */
	public function deleteBy( QueryFilter $filter = null ) {
		$this->table_name = $this->db->prefix . Database::ENTITY_TABLE;
		/**
		 * Entity object.
		 *
		 * @var Entity $entity
		 */
		$entity = new $this->entity_class();
		$type   = $entity->getConfig()->getType();

		$query = "DELETE FROM {$this->table_name} WHERE type = '$type' ";
		if ( $filter ) {
			$query .= $this->apply_query_filter( $filter, IndexHelper::mapFieldsToIndexes( $entity ) );
		}

		return $this->db->query( $query );
	}

	/**
	 * Counts records that match filter criteria.
	 *
	 * @param QueryFilter $filter Filter for query.
	 *
	 * @return int Number of records that match filter criteria.
	 * @throws QueryFilterInvalidParamException If filter condition is invalid.
	 */
	public function count( QueryFilter $filter = null ) {
		$this->table_name = $this->db->prefix . Database::ENTITY_TABLE;

		/**
		 * Entity object.
		 *
		 * @var Entity $entity
		 */
		$entity = new $this->entity_class();
		$type   = $entity->getConfig()->getType();

		$query = "SELECT COUNT(*) as `total` FROM {$this->table_name} WHERE type = '$type' ";
		if ( $filter ) {
			$query .= $this->apply_query_filter( $filter, IndexHelper::mapFieldsToIndexes( $entity ) );
		}

		$result = $this->db->get_results( $query, ARRAY_A );

		return empty( $result ) ? 0 : (int) $result[0]['total'];
	}

	/**
	 * Escapes provided value.
	 *
	 * @param mixed $value Value to be escaped.
	 *
	 * @return string Escaped value.
	 */
	protected function escape( $value ) {
		return $this->db->_real_escape( $value );
	}

	/**
	 * Checks if value exists and escapes it if it's not.
	 *
	 * @param mixed $value Value to be escaped.
	 *
	 * @return string Escaped value.
	 */
	protected function escape_value( $value ) {
		return null === $value ? 'NULL' : "'" . $this->escape( $value ) . "'";
	}

	/**
	 * Builds WHERE part of select query.
	 *
	 * @param array $filter_by Filter conditions in query.
	 *
	 * @return string Where condition.
	 */
	protected function build_condition( $filter_by ) {
		if ( empty( $filter_by ) ) {
			return '';
		}

		$where = array();
		foreach ( $filter_by as $key => $value ) {
			if ( null === $value ) {
				$where[] = "`$key` IS NULL";
			} else {
				$where[] = "`$key` = '" . $this->escape( $value ) . "'";
			}
		}

		return ' WHERE ' . implode( ' AND ', $where );
	}

	/**
	 * Converts filter value to index string representation.
	 *
	 * @param QueryCondition $condition Query condition.
	 *
	 * @return string|null Converted value.
	 */
	protected function convert_value( QueryCondition $condition ) {
		$value = IndexHelper::castFieldValue( $condition->getValue(), $condition->getValueType() );
		switch ( $condition->getValueType() ) {
			case 'integer':
			case 'dateTime':
			case 'boolean':
			case 'double':
				$value = $this->escape_value( $value );
				break;
			case 'string':
				$value = $this->escape_value( $condition->getValue() );
				break;
			case 'array':
				$values         = $condition->getValue();
				$escaped_values = array();
				foreach ( $values as $value ) {
					$escaped_values[] = is_string( $value ) ? $this->escape_value( $value ) : $value;
				}

				$value = '(' . implode( ', ', $escaped_values ) . ')';
				break;
		}

		return $value;
	}

	/**
	 * Builds query filter part of the query.
	 *
	 * @param QueryFilter $filter Query filter object.
	 * @param array       $field_index_map Property to index number map.
	 *
	 * @return string Query filter addendum.
	 * @throws QueryFilterInvalidParamException If filter condition is invalid.
	 */
	protected function apply_query_filter( QueryFilter $filter, array $field_index_map = array() ) {
		$query      = '';
		$conditions = $filter->getConditions();
		if ( ! empty( $conditions ) ) {
			$query .= ' AND (';
			$first  = true;
			foreach ( $conditions as $condition ) {
				$this->validate_index_column( $condition->getColumn(), $field_index_map );
				$chain_op = $first ? '' : $condition->getChainOperator();
				$first    = false;
				$column   = 'id' === $condition->getColumn() ? 'id' : 'index_' . $field_index_map[ $condition->getColumn() ];
				$operator = $condition->getOperator();
				$query   .= " $chain_op $column $operator " . $this->convert_value( $condition );
			}

			$query .= ')';
		}

		if ( $filter->getOrderByColumn() ) {
			$this->validate_index_column( $filter->getOrderByColumn(), $field_index_map );
			$order_index = 'id' === $filter->getOrderByColumn() ? 'id' : 'index_' . $field_index_map[ $filter->getOrderByColumn() ];
			$query      .= ' ORDER BY ' . $order_index . ' ' . $this->validate_order_direction( $filter->getOrderDirection() );
		}

		if ( $filter->getLimit() ) {
			$offset = (int) $filter->getOffset();
			$limit  = (int) $filter->getLimit();
			$query .= " LIMIT {$offset}, {$limit}";
		}

		return $query;
	}

	/**
	 * Transforms raw database query rows to entities.
	 *
	 * @param array $result Raw database query result.
	 *
	 * @return Entity[] Array of transformed entities.
	 */
	protected function translateToEntities( array $result ) {
		/**
		 * Array of decoded entities.
		 *
		 * @var Entity[] $entities
		 */
		$entities = array();
		foreach ( $result as $item ) {
			/**
			 * Entity object.
			 *
			 * @var Entity $entity
			 */
			$data   = json_decode( $item['data'], true );
			$entity = isset( $data['class_name'] ) ? new $data['class_name']() : new $this->entity_class();
			$entity->inflate( $data );
			$entity->setId( $item['id'] );

			$entities[] = $entity;
		}

		return $entities;
	}

	/**
	 * Saves entity to system storage.
	 *
	 * @param Entity $entity Entity to be stored.
	 *
	 * @return int Inserted entity identifier.
	 */
	protected function save_entity_to_storage( Entity $entity ) {
		$this->table_name = $this->db->prefix . Database::ENTITY_TABLE;
		$storage_item     = $this->prepare_entity_for_storage( $entity );

		$this->db->insert( $this->table_name, $storage_item );

		$insert_id = (int) $this->db->insert_id;
		$entity->setId( $insert_id );

		return $insert_id;
	}

	/**
	 * Prepares entity in format for storage.
	 *
	 * @param Entity $entity Entity to be stored.
	 *
	 * @return array Item prepared for storage.
	 */
	protected function prepare_entity_for_storage( Entity $entity ) {
		$indexes      = IndexHelper::transformFieldsToIndexes( $entity );
		$storage_item = array(
			'type'    => $entity->getConfig()->getType(),
			'index_1' => null,
			'index_2' => null,
			'index_3' => null,
			'index_4' => null,
			'index_5' => null,
			'index_6' => null,
			'index_7' => null,
			'data'    => wp_json_encode( $entity->toArray() ),
		);

		foreach ( $indexes as $index => $value ) {
			$storage_item[ 'index_' . $index ] = $value;
		}

		return $storage_item;
	}

	/**
	 * Validates if column can be filtered or sorted by.
	 *
	 * @param string $column Column name.
	 * @param array  $index_map Index map.
	 *
	 * @throws QueryFilterInvalidParamException If filter condition is invalid.
	 */
	protected function validate_index_column( $column, array $index_map ) {
		if ( 'id' !== $column && ! array_key_exists( $column, $index_map ) ) {
			throw new QueryFilterInvalidParamException( __( 'Column is not id or index.', 'cleverreach-wp' ) );
		}
	}

	/**
	 * Validates and sanitizes ORDER BY direction.
	 *
	 * @param string $direction Sort direction.
	 *
	 * @return string Validated direction (ASC or DESC).
	 */
	protected function validate_order_direction( $direction ) {
		$direction = strtoupper( $direction );
		return in_array( $direction, array( 'ASC', 'DESC' ), true ) ? $direction : 'ASC';
	}

}
