<?php
/**
 * Undocumented
 */
class SQL_Relation extends AbstractModel
{
    public $f1 = null;            // Foreign Table (actual name)
    // short_name = Foreign alias

    public $t = null;             // Join kind (left|right|inner|cross etc.)

    public $expr = null;          // Using expression when joining

    public $f2 = null;            // Foreign field
    public $m2 = null;            // Master field

    public $fa = null;            // short name / field alias ?

    public $m1 = null;            // Master table (defaults to owner->table / owner->table_alias)
    // $m1 == $relation->f1
    public $relation = null;

    public $delete_behaviour = 'cascade';          // cascade, setnull, ignore

    public $table_alias;
    public $dsql;
    public $id;

    // {{{ type-hint inherited properties

    /** @var SQL_Model */
    public $owner;
    
    // }}}

    public function init()
    {
        parent::init();
        $this->table_alias = $this->short_name;
    }

    /** Second argument to addField() will specify how the field is really called */
    public function addField($n, $actual_field = null)
    {
        $f = $this->owner->addField($n, $actual_field)->from($this);

        return $f;
    }
    public function addExpression($n, $expression = null)
    {
        $f = $this->owner->addExpression($n, $expression)->from($this);

        return $f;
    }
    public function join($foreign_table, $master_field = null, $join_kind = null, $_foreign_alias = null)
    {
        return $this->owner->join($foreign_table, $master_field, $join_kind, $_foreign_alias, $this);
    }
    public function leftJoin($foreign_table, $master_field = null, $join_kind = null, $_foreign_alias = null)
    {
        return $this->owner->leftJoin($foreign_table, $master_field, $join_kind, $_foreign_alias, $this);
    }
    public function hasOne($model, $our_field = null, $display_field = null)
    {
        return $this->owner->hasOne($model, $our_field, $display_field)->from($this);
    }
    public function hasMany($model, $their_field = null, $our_field = null, $as_field = null)
    {
        return $this->owner->hasMany($model, $their_field, $our_field, $as_field)->from($this);
    }
    // TODO: hasMany()

    public function set($foreign_table, $master_field = null, $join_kind = null, $relation = null)
    {
        // http://dev.mysql.com/doc/refman/5.0/en/join.html
        $join_types = array(
            'left', 'right', 'inner', 'cross', 'natural',
            'left outer', 'right outer', 'natural left',
            'natural right', 'natural left outer', 'natural right outer'
        );
        if ($join_kind && !in_array(strtolower($join_kind), $join_types)) {
            throw $this->exception('Specify reasonable SQL join type.')
                ->addMoreInfo('Specified', $join_kind)
                ->addMoreInfo('Allowed', implode(', ', $join_types));
        }

        $this->relation = $relation;

        // Split and deduce fields
        list($f1, $f2) = explode('.', $foreign_table, 2);

        $m1 = $m2 = null;
        if (is_object($master_field)) {
            $this->expr = $master_field;
        } else {
            $m1 = $this->relation ?: $this->owner;
            $m1 = $m1->table_alias ?: $m1->table;

            // Split and deduce primary table
            $m2 = $master_field;

            // Identify fields we use for joins
            if (is_null($f2) && is_null($m2)) {
                $m2 = $f1.'_id';
            }
            if (is_null($m2)) {
                $m2 = $this->owner->id_field;
            }
            $this->f1 = $f1;
            $this->m1 = $m1;
            $this->m2 = $m2;
        }
        if (is_null($f2)) {
            $f2 = 'id';
        }
        $this->f2 = $f2;

        $this->t = $join_kind ?: 'inner';
        $this->fa = $this->short_name;

        // Use the real ID field as defined by the model as default
        $this->owner->_dsql()->join($foreign_table, $this->expr ?: ($m1.'.'.$m2), $this->t, $this->short_name);

        // If our ID field is NOT used, must insert record in OTHER table first and use their primary value in OUR field
        if ($this->m2 && $this->m2 != $this->owner->id_field) {
            // user.contactinfo_id = contactinfo.id
            $this->owner->addHook('beforeInsert', array($this, 'beforeInsert'), array(), -5);
            $this->owner->addHook('beforeModify', array($this, 'beforeModify'), array(), -5);
            $this->owner->addHook('afterDelete', array($this, 'afterDelete'), array(), -5);
        } elseif ($this->m2) {
            // author.id = book.author_id
            $this->owner->addHook('afterInsert', array($this, 'afterInsert'));
            $this->owner->addHook('beforeModify', array($this, 'beforeModify'));
            $this->owner->addHook('beforeDelete', array($this, 'beforeDelete'));
        }// else $m2 is not set, expression is used, so don't try to do anything unnecessary

        $this->owner->addHook('beforeSave', array($this, 'beforeSave'));
        $this->owner->addHook('beforeLoad', array($this, 'beforeLoad'));
        $this->owner->addHook('afterLoad', array($this, 'afterLoad'));

        return $this;
    }
    public function beforeSave($m)
    {
        $this->dsql = $this->owner->_dsql()->dsql()->table($this->f1);
        if ($this->owner->_dsql()->debug) {
            $this->dsql->debug();
        }
    }
    public function beforeInsert($m, $q)
    {
        // Insert related table data and add ID into the main query
        // TODO: handle cases when $this->m1 != $this->owner->table?:$this->owner->table_alias
        if ($this->delete_behaviour == 'ignore') {
            return;
        }

        if ($this->owner->hasElement($this->m2) && $this->owner->get($this->m2) !== null) {
            return; // using existing element
        }

        $this->dsql->set($this->f2, null);
        $this->id = $this->dsql->insert();

        if ($this->relation) {
            $q = $this->relation->dsql;
        }

        $q->set($this->m2, $this->id);
    }
    public function afterInsert($m, $id)
    {
        if ($this->delete_behaviour == 'ignore') {
            return;
        }

        $this->id = $this->dsql->set($this->f2, $this->relation ? $this->relation->id ?: $id : $id)->insert();
    }
    public function beforeModify($m, $q)
    {
        if ($this->dsql->args['set']) {
            $this->dsql->where($this->f2, $this->id)->update();
        }
    }
    public function beforeDelete($m, $id)
    {
        // Let's hope that cascading sorts this
        $this->dsql = $this->owner->_dsql()->dsql()->table($this->f1);
        if ($this->owner->_dsql()->debug) {
            $this->dsql->debug();
        }

        if ($this->delete_behaviour == 'cascade') {
            $this->dsql->del('field')->where($this->f2, $this->id)->delete();
        } elseif ($this->delete_behaviour == 'ignore') {
            // $this->dsql->del('field')->set($this->f2,null)->where($this->f2,$this->id)->debug()->update();
            null;
        }
    }
    public function afterDelete($m)
    {
        $this->dsql = $this->owner->_dsql()->dsql()->table($this->f1);
        if ($this->owner->_dsql()->debug) {
            $this->dsql->debug();
        }

        if ($this->delete_behaviour == 'cascade') {
            try {
                $this->dsql->del('field')->where($this->f2, $this->id)->delete();
            } catch (Exception $e) {
                $this->app->caughtException($e);
            }
        } elseif ($this->delete_behaviour == 'ignore') {
            //$this->dsql->del('field')->set($this->f2,null)->where($this->f2,$this->id)->update();
            null;
        }
    }

    /** Add query for the relation's ID, but then remove it from results. Remove ID when unloading. */
    public function beforeLoad($m, $q = null)
    {
        if (is_null($q)) {
            return;
        }  // manual load

        if ($this->m2 && $this->m2 != $this->owner->id_field) {
            $q->field($this->m2, $this->m1, $this->short_name);
        } elseif ($this->m2) {
            $q->field($this->f2, $this->fa ?: $this->f1, $this->short_name);
        }
    }
    public function afterLoad($m)
    {
        $this->id = $m->data[$this->short_name];
        unset($m->data[$this->short_name]);
    }
    public function afterUnload($m)
    {
        $this->id = null;
    }

    public function fieldExpr($f)
    {
        return $this->owner->_dsql()->expr(
            $this->owner->_dsql()->bt($this->short_name).
            '.'.
            $this->owner->_dsql()->bt($f)
        );
    }
}
