<?php

namespace Lazer\Classes;

use Lazer\Classes\Helpers\Validate;
use Lazer\Classes\Helpers\Config;
use Lazer\Classes\Database;
use Lazer\Classes\LazerException;

/**
 * Relation class of LAZER project.
 * 
 * @category Core
 * @author Grzegorz Kuźnik
 * @copyright (c) 2013, Grzegorz Kuźnik
 * @license http://opensource.org/licenses/MIT The MIT License
 * @link https://github.com/Greg0/Lazer-Database GitHub Repository
 */
abstract class Core_Relation {

    /**
     * Tables names
     * @var array tables
     */
    protected $tables = array(
        'local'   => null,
        'foreign' => null
    );

    /**
     * Relation keys names
     * @var array keys
     */
    protected $keys = array(
        'local'   => null,
        'foreign' => null
    );

    /**
     * Current relation type
     * @var string
     */
    protected $relationType;

    /**
     * All relations types
     * @var array
     */
    protected static $relations = array('belongsTo', 'hasMany', 'hasAndBelongsToMany');

    /**
     * Factory method
     * @param string $name Name of table
     * @return \Lazer\Classes\Relation
     */
    public static function table($name)
    {
        Validate::table($name)->exists();

        $self                  = new Relation;
        $self->tables['local'] = $name;

        return $self;
    }

    /**
     * Getter of junction table name in many2many relation
     * @return boolean|string Name of junction table or false
     */
    public function getJunction()
    {
        if ($this->relationType == 'hasAndBelongsToMany')
        {
            $tables = $this->tables;
            sort($tables);
            return implode('_', $tables);
        }
        return false;
    }

    /**
     * Set relation type to field
     * @param string $relation Name of relation
     */
    protected function setRelationType($relation)
    {
        Validate::relationType($relation);
        $this->relationType = $relation;
    }

    /**
     * Set table name
     * @param string $type local or foreign
     * @param string $name table name
     */
    protected function setTable($type, $name)
    {
        Validate::table($name)->exists();
        $this->tables[$type] = $name;
    }

    /**
     * Set key name
     * @param string $type local or foreign
     * @param string $key key name
     * @return \Lazer\Classes\Core_Relation
     * @throws LazerException First you must define tables name
     */
    protected function setKey($type, $key)
    {
        if (!in_array(null, $this->tables))
        {
            Validate::table($this->tables[$type])->field($key);

            $this->keys[$type] = $key;
            return $this;
        }

        throw new LazerException('First you must define tables name');
    }

    /**
     * Set local key name
     * @param string $key key name
     * @return \Lazer\Classes\Core_Relation
     * @throws LazerException First you must define tables name
     */
    public function localKey($key)
    {
        return $this->setKey('local', $key);
    }

    /**
     * Set foreign key name
     * @param string $key key name
     * @return \Lazer\Classes\Core_Relation
     * @throws LazerException First you must define tables name
     */
    public function foreignKey($key)
    {
        return $this->setKey('foreign', $key);
    }

    /**
     * Set relation one2many to table 
     * @param string $table Table name
     * @return \Lazer\Classes\Core_Relation
     */
    public function belongsTo($table)
    {
        $this->setTable('foreign', $table);
        $this->setRelationType(__FUNCTION__);

        return $this;
    }

    /**
     * Set relation many2one to table 
     * @param string $table Table name
     * @return \Lazer\Classes\Core_Relation
     */
    public function hasMany($table)
    {
        $this->setTable('foreign', $table);
        $this->setRelationType(__FUNCTION__);

        return $this;
    }

    /**
     * Set relation many2many to table 
     * @param string $table Table name
     * @return \Lazer\Classes\Core_Relation
     */
    public function hasAndBelongsToMany($table)
    {
        $this->setTable('foreign', $table);
        $this->setRelationType(__FUNCTION__);

        return $this;
    }

    /**
     * Use relation to table
     * @param string $table Table name
     * @return \Lazer\Classes\Core_Relation
     */
    public function with($table)
    {
        Validate::relation($this->tables['local'], $table);
        $this->setTable('foreign', $table);
        $this->setRelationType(Config::table($this->tables['local'])->relations($this->tables['foreign'])->type);
        $this->setKey('local', Config::table($this->tables['local'])->relations($this->tables['foreign'])->keys->local);
        $this->setKey('foreign', Config::table($this->tables['local'])->relations($this->tables['foreign'])->keys->foreign);

        return $this;
    }

    /**
     * Set specified relation
     * @throws LazerException Tables names or keys missing
     */
    public function setRelation()
    {
        if (!in_array(null, $this->tables) && !in_array(null, $this->keys))
        {
            $this->addRelation();
            return true;
        }
        else
        {
            throw new LazerException('Tables names or keys missing');
        }
    }

    /**
     * Get relation information
     * @return array relation information
     */
    public function getRelation()
    {
        return array(
            'tables' => $this->tables,
            'keys'   => $this->keys,
            'type'   => $this->relationType
        );
    }

    /**
     * Remove relation
     */
    public function removeRelation()
    {
        if ($this->relationType == 'hasAndBelongsToMany')
        {
            $junction = $this->getJunction();

            $this->deleteRelationData($junction, $this->tables['local']);
            $this->deleteRelationData($junction, $this->tables['foreign']);
        }
        $this->deleteRelationData($this->tables['local'], $this->tables['foreign']);
    }

    /**
     * Add data to configs and create all necessary files
     */
    protected function addRelation()
    {
        if ($this->relationType == 'hasAndBelongsToMany')
        {
            $junction = $this->getJunction();

            try
            {
                Validate::table($junction)->exists();
            }
            catch (LazerException $e)
            {
                Database::create($junction, array(
                    $this->tables['local'] . '_id'   => 'integer',
                    $this->tables['foreign'] . '_id' => 'integer',
                ));

                $this->insertRelationData($junction, $this->tables['local'], 'hasMany', array(
                    'local'   => $this->tables['local'] . '_id',
                    'foreign' => $this->keys['local']
                ));

                $this->insertRelationData($junction, $this->tables['foreign'], 'hasMany', array(
                    'local'   => $this->tables['foreign'] . '_id',
                    'foreign' => $this->keys['foreign']
                ));
            }
        }
        $this->insertRelationData($this->tables['local'], $this->tables['foreign'], $this->relationType, $this->keys);
    }

    /**
     * Inserts relation data to config file
     * @param string $from Local table
     * @param string $to Related table
     * @param string $type Relation type
     * @param array $keys Relationed keys
     */
    protected function insertRelationData($from, $to, $type, array $keys)
    {
        $config                    = Config::table($from);
        $content                   = $config->get();
        $content->relations->{$to} = array(
            'type' => $type,
            'keys' => $keys,
        );
        $config->put($content);
    }

    /**
     * Inserts relation data to config file
     * @param string $from Local table
     * @param string $to Related table
     */
    protected function deleteRelationData($from, $to)
    {
        $config  = Config::table($from);
        $content = $config->get();
        unset($content->relations->{$to});
        $config->put($content);
    }

    /**
     * Process query with joined data
     * @param object $row One row of data
     * @return Database
     */
    protected function join($row)
    {
        $keys['local']   = $this->keys['local'];
        $keys['foreign'] = $this->keys['foreign'];

        if ($this->relationType == 'hasAndBelongsToMany')
        {
            $join = Database::table($this->getJunction())
                    ->groupBy($this->tables['local'] . '_id')
                    ->where($this->tables['local'] . '_id', '=', $row->{$keys['local']})
                    ->findAll()
                    ->asArray(null, $this->tables['foreign'] . '_id');


            if (empty($join))
                return array();

            return Database::table($this->tables['foreign'])
                            ->where($keys['foreign'], 'IN', $join[$row->{$keys['local']}]);
        }

        return Database::table($this->tables['foreign'])
                        ->where($keys['foreign'], '=', $row->{$keys['local']});
    }

    /**
     * 
     * @param array $array
     * @param string $part
     * @return array
     */
    public function build(array $array, $part)
    {
        $return = array();
        foreach ($array as $key => $row)
        {
            if (is_object($row))
            {
                if ($row instanceof \stdClass)
                {
                    $part = ucfirst($part);

                    if (!isset($row->{$part}))
                    {
                        $query = $this->join($row);

                        if ($this->relationType == 'belongsTo')
                        {
                            $query = $query->findAll();
                            $query = reset($query)[0];
                        }

                        $row->{$part} = $query;
                    }

                    $array[$key] = $row->{$part};
                    $return[]    = $row->{$part};
                }
                else
                {
                    $row->with($part);
                }
            }
            else
            {
                $return = array_merge($return, $this->build($row, $part));
            }
        }
        return $return;
    }

    /**
     * Get relations types
     * @return array
     */
    public static function relations()
    {
        return self::$relations;
    }

}
