<?php namespace Laravel\Database\Eloquent;

use Laravel\Str;
use Laravel\Inflector;
use Laravel\Paginator;
use Laravel\Database\Manager as DB;

abstract class Model {

	/**
	 * The connection that should be used for the model.
	 *
	 * @var string
	 */
	public static $connection;

	/**
	 * Indicates if the model has creation and update timestamps.
	 *
	 * @var bool
	 */
	public static $timestamps = false;

	/**
	 * The name of the auto-incrementing sequence associated with the model.
	 *
	 * @var string
	 */
	public static $sequence = null;

	/**
	 * The model query instance.
	 *
	 * @var Query
	 */
	public $query;

	/**
	 * Indicates if the model exists in the database.
	 *
	 * @var bool
	 */
	public $exists = false;

	/**
	 * The model's attributes. 
	 *
	 * Typically, a model has an attribute for each column on the table.
	 *
	 * @var array
	 */
	public $attributes = array();

	/**
	 * The model's dirty attributes.
	 *
	 * @var array
	 */
	public $dirty = array();

	/**
	 * The model's ignored attributes.
	 *
	 * Ignored attributes will not be saved to the database, and are
	 * primarily used to hold relationships.
	 *
	 * @var array
	 */
	public $ignore = array();

	/**
	 * The relationships that should be eagerly loaded.
	 *
	 * @var array
	 */
	public $includes = array();

	/**
	 * The relationship type the model is currently resolving.
	 *
	 * @var string
	 */
	public $relating;

	/**
	 * The foreign key of the "relating" relationship.
	 *
	 * @var string
	 */
	public $relating_key;

	/**
	 * The table name of the model being resolved. 
	 *
	 * This is used during many-to-many eager loading.
	 *
	 * @var string
	 */
	public $relating_table;

	/**
	 * Create a new Eloquent model instance.
	 *
	 * @param  array  $attributes
	 * @return void
	 */
	public function __construct($attributes = array())
	{
		$this->fill($attributes);
	}

	/**
	 * Set the attributes of the model using an array.
	 *
	 * @param  array  $attributes
	 * @return Model
	 */
	public function fill($attributes)
	{
		foreach ($attributes as $key => $value)
		{
			$this->$key = $value;
		}

		return $this;
	}

	/**
	 * Set the eagerly loaded models on the queryable model.
	 *
	 * @return Model
	 */
	private function _with()
	{
		$this->includes = func_get_args();

		return $this;
	}

	/**
	 * Factory for creating queryable Eloquent model instances.
	 *
	 * @param  string  $class
	 * @return object
	 */
	public static function query($class)
	{
		$model = new $class;

		// Since this method is only used for instantiating models for querying
		// purposes, we will go ahead and set the Query instance on the model.
		$model->query = DB::connection(static::$connection)->table(static::table($class));

		return $model;
	}

	/**
	 * Get the table name for a model.
	 *
	 * @param  string  $class
	 * @return string
	 */
	public static function table($class)
	{
		if (property_exists($class, 'table')) return $class::$table;

		return strtolower(Inflector::plural(static::model_name($class)));
	}

	/**
	 * Get an Eloquent model name without any namespaces.
	 *
	 * @param  string|Model  $model
	 * @return string
	 */
	public static function model_name($model)
	{
		$class = (is_object($model)) ? get_class($model) : $model;

		$segments = array_reverse(explode('\\', $class));

		return $segments[0];
	}

	/**
	 * Get all of the models from the database.
	 *
	 * @return array
	 */
	public static function all()
	{
		return Hydrator::hydrate(static::query(get_called_class()));
	}

	/**
	 * Get a model by the primary key.
	 *
	 * @param  int  $id
	 * @return mixed
	 */
	public static function find($id)
	{
		return static::query(get_called_class())->where('id', '=', $id)->first();
	}

	/**
	 * Get an array of models from the database.
	 *
	 * @return array
	 */
	private function _get()
	{
		return Hydrator::hydrate($this);
	}

	/**
	 * Get the first model result
	 *
	 * @return mixed
	 */
	private function _first()
	{
		return (count($results = $this->take(1)->_get()) > 0) ? reset($results) : null;
	}

	/**
	 * Get paginated model results as a Paginator instance.
	 *
	 * @param  int        $per_page
	 * @return Paginator
	 */
	private function _paginate($per_page = null)
	{
		$total = $this->query->count();

		// The number of models to show per page may be specified as a static property
		// on the model. The models shown per page may also be overriden for the model
		// by passing the number into this method. If the models to show per page is
		// not available via either of these avenues, a default number will be shown.
		if (is_null($per_page))
		{
			$per_page = (property_exists(get_class($this), 'per_page')) ? static::$per_page : 20;
		}

		return Paginator::make($this->for_page(Paginator::page($total, $per_page), $per_page)->get(), $total, $per_page);
	}

	/**
	 * Retrieve the query for a 1:1 relationship.
	 *
	 * @param  string  $model
	 * @param  string  $foreign_key
	 * @return mixed
	 */
	public function has_one($model, $foreign_key = null)
	{
		$this->relating = __FUNCTION__;

		return $this->has_one_or_many($model, $foreign_key);
	}

	/**
	 * Retrieve the query for a 1:* relationship.
	 *
	 * @param  string  $model
	 * @param  string  $foreign_key
	 * @return mixed
	 */
	public function has_many($model, $foreign_key = null)
	{
		$this->relating = __FUNCTION__;

		return $this->has_one_or_many($model, $foreign_key);
	}

	/**
	 * Retrieve the query for a 1:1 or 1:* relationship.
	 *
	 * The default foreign key for has one and has many relationships is the name
	 * of the model with an appended _id. For example, the foreign key for a
	 * User model would be user_id. Photo would be photo_id, etc.
	 *
	 * @param  string  $model
	 * @param  string  $foreign_key
	 * @return mixed
	 */
	private function has_one_or_many($model, $foreign_key)
	{
		$this->relating_key = (is_null($foreign_key)) ? strtolower(static::model_name($this)).'_id' : $foreign_key;

		return static::query($model)->where($this->relating_key, '=', $this->id);
	}

	/**
	 * Retrieve the query for a 1:1 belonging relationship.
	 *
	 * The default foreign key for belonging relationships is the name of the
	 * relationship method name with _id. So, if a model has a "manager" method
	 * returning a belongs_to relationship, the key would be manager_id.
	 *
	 * @param  string  $model
	 * @param  string  $foreign_key
	 * @return mixed
	 */
	public function belongs_to($model, $foreign_key = null)
	{
		$this->relating = __FUNCTION__;

		if ( ! is_null($foreign_key))
		{
			$this->relating_key = $foreign_key;
		}
		else
		{
			list(, $caller) = debug_backtrace(false);

			$this->relating_key = $caller['function'].'_id';
		}

		return static::query($model)->where('id', '=', $this->attributes[$this->relating_key]);
	}

	/**
	 * Retrieve the query for a *:* relationship.
	 *
	 * The default foreign key for many-to-many relations is the name of the model
	 * with an appended _id. This is the same convention as has_one and has_many.
	 *
	 * @param  string  $model
	 * @param  string  $table
	 * @param  string  $foreign_key
	 * @param  string  $associated_key
	 * @return mixed
	 */
	public function has_and_belongs_to_many($model, $table = null, $foreign_key = null, $associated_key = null)
	{
		$this->relating = __FUNCTION__;

		$this->relating_table = (is_null($table)) ? $this->intermediate_table($model) : $table;

		// Allowing the overriding of the foreign and associated keys provides
		// the flexibility for self-referential many-to-many relationships.
		$this->relating_key = (is_null($foreign_key)) ? strtolower(static::model_name($this)).'_id' : $foreign_key;

		// The associated key is the foreign key name of the related model. 
		// If the related model is "Role", the key would be "role_id".
		$associated_key = (is_null($associated_key)) ? strtolower(static::model_name($model)).'_id' : $associated_key;

		return static::query($model)
                             ->select(array(static::table($model).'.*'))
                             ->join($this->relating_table, static::table($model).'.id', '=', $this->relating_table.'.'.$associated_key)
                             ->where($this->relating_table.'.'.$this->relating_key, '=', $this->id);
	}

	/**
	 * Determine the intermediate table name for a given model.
	 *
	 * By default, the intermediate table name is the plural names of the models
	 * arranged alphabetically and concatenated with an underscore.
	 *
	 * @param  string  $model
	 * @return string
	 */
	private function intermediate_table($model)
	{
		$models = array(Inflector::plural(static::model_name($model)), Inflector::plural(static::model_name($this)));

		sort($models);

		return strtolower($models[0].'_'.$models[1]);
	}

	/**
	 * Save the model to the database.
	 *
	 * @return bool
	 */
	public function save()
	{
		// If the model does not have any dirty attributes, there is no reason
		// to save it to the database.
		if ($this->exists and count($this->dirty) == 0) return true;

		$model = get_class($this);

		// Since the model was instantiated using "new", a query instance has not been set.
		// Only models being used for querying have their query instances set by default.
		$this->query = DB::connection(static::$connection)->table(static::table($model));

		if (property_exists($model, 'timestamps') and $model::$timestamps)
		{
			$this->timestamp();
		}

		// If the model already exists in the database, we will just update it.
		// Otherwise, we will insert the model and set the ID attribute.
		if ($this->exists)
		{
			$success = ($this->query->where_id($this->attributes['id'])->update($this->dirty) === 1);
		}
		else
		{
			$success = is_numeric($this->attributes['id'] = $this->query->insert_get_id($this->attributes, static::$sequence));
		}

		($this->exists = true) and $this->dirty = array();

		return $success;
	}

	/**
	 * Set the creation and update timestamps on the model.
	 *
	 * @return void
	 */
	private function timestamp()
	{
		$this->updated_at = date('Y-m-d H:i:s');

		if ( ! $this->exists) $this->created_at = $this->updated_at;
	}

	/**
	 * Delete a model from the database.
	 *
	 * @param  int  $id
	 * @return int
	 */
	public function delete($id = null)
	{
		// If the delete method is being called on an existing model, we only want to delete
		// that model. If it is being called from an Eloquent query model, it is probably
		// the developer's intention to delete more than one model, so we will pass the
		// delete statement to the query instance.
		if ( ! $this->exists) return $this->query->delete();

		$table = static::table(get_class($this));

		return DB::connection(static::$connection)->table($table)->delete($this->id);
	}

	/**
	 * Magic method for retrieving model attributes.
	 */
	public function __get($key)
	{
		if (array_key_exists($key, $this->attributes))
		{
			return $this->attributes[$key];
		}
		// Is the requested item a model relationship that has already been loaded?
		// All of the loaded relationships are stored in the "ignore" array.
		elseif (array_key_exists($key, $this->ignore))
		{
			return $this->ignore[$key];
		}
		// Is the requested item a model relationship? If it is, we will dynamically
		// load it and return the results of the relationship query.
		elseif (method_exists($this, $key))
		{
			$query = $this->$key();

			return $this->ignore[$key] = (in_array($this->relating, array('has_one', 'belongs_to'))) ? $query->first() : $query->get();
		}
	}

	/**
	 * Magic Method for setting model attributes.
	 */
	public function __set($key, $value)
	{
		// If the key is a relationship, add it to the ignored attributes.
		// Ignored attributes are not stored in the database.
		if (method_exists($this, $key))
		{
			$this->ignore[$key] = $value;
		}
		else
		{
			$this->attributes[$key] = $value;
			$this->dirty[$key] = $value;
		}
	}

	/**
	 * Magic Method for determining if a model attribute is set.
	 */
	public function __isset($key)
	{
		return (array_key_exists($key, $this->attributes) or array_key_exists($key, $this->ignore));
	}

	/**
	 * Magic Method for unsetting model attributes.
	 */
	public function __unset($key)
	{
		unset($this->attributes[$key], $this->ignore[$key], $this->dirty[$key]);
	}

	/**
	 * Magic Method for handling dynamic method calls.
	 */
	public function __call($method, $parameters)
	{
		// To allow the "with", "get", "first", and "paginate" methods to be called both
		// staticly and on an instance, we need to have private, underscored versions
		// of the methods and handle them dynamically.
		if (in_array($method, array('with', 'get', 'first', 'paginate')))
		{
			return call_user_func_array(array($this, '_'.$method), $parameters);
		}

		// All of the aggregate and persistance functions can be passed directly to the query
		// instance. For these functions, we can simply return the response of the query.
		if (in_array($method, array('insert', 'update', 'increment', 'decrement', 'abs', 'count', 'sum', 'min', 'max', 'avg')))
		{
			return call_user_func_array(array($this->query, $method), $parameters);
		}

		// Pass the method to the query instance. This allows the chaining of methods
		// from the query builder, providing the same convenient query API as the
		// query builder itself.
		call_user_func_array(array($this->query, $method), $parameters);

		return $this;
	}

	/**
	 * Magic Method for handling dynamic static method calls.
	 */
	public static function __callStatic($method, $parameters)
	{
		// Just pass the method to a model instance and let the __call method take care of it.
		return call_user_func_array(array(static::query(get_called_class()), $method), $parameters);
	}

}